增强 React 应用的 8 种神奇方法
在Medium上找到我
有时,当我们使用 React 构建应用时,很容易错过改进的机会,这可能是因为当应用运行良好、运行速度很快时,我们为了追求完美而容忍它。作为开发者,我们可能会认为,如果项目结果在我们看来很正常,那么在用户看来也应该很正常。当我们这样想时,可能会忽略代码中可以优化以获得更好结果的部分。
本文将介绍增强 React 应用程序的 8 种神奇方法。
1. 热爱你的身份
增强你的反应应用程序的第一个方法是热爱你的身份。
重要的是要记住,您可以包装变量和函数,React.useMemo
因为您可以授予它们记忆自身的能力,以便 React 知道它们在未来的渲染中保持不变。
否则,如果您不记住它们,它们的引用将在以后的渲染中消失。这可能会伤害它们的感情,所以您可以通过记住它们来向它们表明您爱它们,并希望保留它们。如果您爱它们,它们也会回报您的爱,确保它们在它们所处的情况下,帮助避免浪费操作,从而照顾好您和您的应用。
例如,假设我们正在创建一个自定义钩子,它接受一个列表urls
作为参数,以便将它们累积到一个 Promise 数组中,以便用 进行解析Promise.all
。结果将被插入到状态中,并App
在组件完成后立即传递给组件。我们的 Promise 列表将映射到urls
包含 4 个不同 URL 的数组上:
import React from 'react' | |
import axios from 'axios' | |
export const useApp = ({ urls }) => { | |
const [results, setResults] = React.useState(null) | |
const promises = urls.map(axios.get) | |
React.useEffect(() => { | |
Promise.all(promises).then(setResults) | |
}, []) | |
return { results } | |
} | |
const App = () => { | |
const urls = [ | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=a', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=u', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=y', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=z', | |
] | |
useApp({ urls }) | |
return null | |
} | |
export default App |
import React from 'react' | |
import axios from 'axios' | |
export const useApp = ({ urls }) => { | |
const [results, setResults] = React.useState(null) | |
const promises = urls.map(axios.get) | |
React.useEffect(() => { | |
Promise.all(promises).then(setResults) | |
}, []) | |
return { results } | |
} | |
const App = () => { | |
const urls = [ | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=a', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=u', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=y', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=z', | |
] | |
useApp({ urls }) | |
return null | |
} | |
export default App |
我们的任务是从这 4 个链接获取数据,因此理想情况下只应发送 4 个请求。但是,如果我们查看Chrome浏览器内的“网络”标签页,就会发现它实际上发送了 8 个请求。这是因为参数不再像以前那样保持原样,因为每次重新渲染时,它都会实例化一个新数组——因此 React 会将其视为已更改的值。urls
App
计算机程序有时会自以为比我们聪明,从而逃脱这种糟糕的行为。为了解决这个问题,我们可以这样设置:只要包含 URL 的数组不变,React.useMemo
promise 数组就不会在每次渲染时重新计算。
让我们重构代码来应用这个概念:
const useApp = ({ urls }) => { | |
const [results, setResults] = React.useState(null) | |
const promises = urls.map((url) => axios.get(url)) | |
React.useEffect(() => { | |
Promise.all(promises).then(setResults) | |
}, []) | |
return { results } | |
} | |
const App = () => { | |
const urls = React.useMemo(() => { | |
return [ | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=a', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=u', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=y', | |
] | |
}, []) | |
const { results } = useApp({ urls }) | |
return null | |
} |
const useApp = ({ urls }) => { | |
const [results, setResults] = React.useState(null) | |
const promises = urls.map((url) => axios.get(url)) | |
React.useEffect(() => { | |
Promise.all(promises).then(setResults) | |
}, []) | |
return { results } | |
} | |
const App = () => { | |
const urls = React.useMemo(() => { | |
return [ | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=a', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=u', | |
'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?terms=y', | |
] | |
}, []) | |
const { results } = useApp({ urls }) | |
return null | |
} |
如果我们现在运行这个函数,它仍然会发送 8 个请求。这是因为虽然我们记住了urls
数组,但我们还需要记住promises
钩子内部的变量,因为钩子运行时,这些变量也会实例化:
const promises = React.useMemo(() => { | |
return urls.map((url) => axios.get(url)) | |
}, [urls]) |
const promises = React.useMemo(() => { | |
return urls.map((url) => axios.get(url)) | |
}, [urls]) |
现在,我们的代码运行时应该只会发送 4 个请求。太棒了!
2. 将 Props 合并到 Children
有时,我们可能会遇到这样的情况:在渲染之前,我们想偷偷地将一个 prop 与子元素合并。React 允许你查看任何 React 元素以及其他元素的 prop,例如公开其key
。
我们可以用一个新组件包装子元素并从那里注入新的道具,或者我们可以使用此方法合并新的道具。
例如,假设我们有一个App
使用钩子的组件,useModal
该钩子通过提供诸如open
、close
和 之类的控件来提供一些方便的实用程序来管理模态窗口opened
。我们希望将这些道具传递给VisibilityControl
组件,因为它将在将模态数据传递给子组件之前提供其他功能:
import React from 'react' | |
const UserContext = React.createContext({ | |
user: { | |
firstName: 'Kelly', | |
email: 'frogLover123@gmail.com', | |
}, | |
activated: true, | |
}) | |
const VisibilityControl = ({ children, opened, close }) => { | |
const ctx = React.useContext(UserContext) | |
return React.cloneElement(children, { | |
opened: ctx.activated ? opened : false, | |
onClick: close, | |
}) | |
} | |
export const useModal = ({ urls } = {}) => { | |
const [opened, setOpened] = React.useState(false) | |
const open = () => setOpened(true) | |
const close = () => setOpened(false) | |
return { | |
opened, | |
open, | |
close, | |
} | |
} | |
const App = ({ children }) => { | |
const modal = useModal() | |
return ( | |
<div> | |
<button type="button" onClick={modal.opened ? modal.close : modal.open}> | |
{modal.opened ? 'Close' : 'Open'} the Modal | |
</button> | |
<VisibilityControl {...modal}>{children}</VisibilityControl> | |
</div> | |
) | |
} | |
const Window = ({ opened }) => { | |
if (!opened) return null | |
return ( | |
<div style={{ border: '1px solid teal', padding: 12 }}> | |
<h2>I am a window</h2> | |
</div> | |
) | |
} | |
export default () => ( | |
<App> | |
<Window /> | |
</App> | |
) |
import React from 'react' | |
const UserContext = React.createContext({ | |
user: { | |
firstName: 'Kelly', | |
email: 'frogLover123@gmail.com', | |
}, | |
activated: true, | |
}) | |
const VisibilityControl = ({ children, opened, close }) => { | |
const ctx = React.useContext(UserContext) | |
return React.cloneElement(children, { | |
opened: ctx.activated ? opened : false, | |
onClick: close, | |
}) | |
} | |
export const useModal = ({ urls } = {}) => { | |
const [opened, setOpened] = React.useState(false) | |
const open = () => setOpened(true) | |
const close = () => setOpened(false) | |
return { | |
opened, | |
open, | |
close, | |
} | |
} | |
const App = ({ children }) => { | |
const modal = useModal() | |
return ( | |
<div> | |
<button type="button" onClick={modal.opened ? modal.close : modal.open}> | |
{modal.opened ? 'Close' : 'Open'} the Modal | |
</button> | |
<VisibilityControl {...modal}>{children}</VisibilityControl> | |
</div> | |
) | |
} | |
const Window = ({ opened }) => { | |
if (!opened) return null | |
return ( | |
<div style={{ border: '1px solid teal', padding: 12 }}> | |
<h2>I am a window</h2> | |
</div> | |
) | |
} | |
export default () => ( | |
<App> | |
<Window /> | |
</App> | |
) |
VisibilityControl
确保其子进程正常使用之前activated
已启用。如果在秘密路由中使用了此功能,则提供阻止未激活用户查看秘密内容的功能。true
opened
VisibilityControl
3. 组合 Reducer 来创建一个庞大的 Reducer
有时你需要将应用中的两个或多个 Reducer 组合起来,形成一个更大的 Reducer。这种方法与combineReducers
React-redux 中的工作方式类似。
假设我们计划制作一个巨大的微服务应用程序,我们最初计划指定应用程序中的每个部分负责自己的上下文/状态,但后来我们想到了一个价值百万美元的应用程序创意,它需要将状态统一为一个大状态,这样我们就可以在同一环境中管理它们。
我们有authReducer.js
、ownersReducer.js
和,frogsReducer.js
我们想将它们合并起来:
authReducer.js
const authReducer = (state, action) => { | |
switch (action.type) { | |
case 'set-authenticated': | |
return { ...state, authenticated: action.authenticated } | |
default: | |
return state | |
} | |
} | |
export default authReducer |
const authReducer = (state, action) => { | |
switch (action.type) { | |
case 'set-authenticated': | |
return { ...state, authenticated: action.authenticated } | |
default: | |
return state | |
} | |
} | |
export default authReducer |
ownersReducer.js
const ownersReducer = (state, action) => { | |
switch (action.type) { | |
case 'add-owner': | |
return { | |
...state, | |
profiles: [...state.profiles, action.owner], | |
} | |
case 'add-owner-id': | |
return { ...state, ids: [...state.ids, action.id] } | |
default: | |
return state | |
} | |
} | |
export default ownersReducer |
const ownersReducer = (state, action) => { | |
switch (action.type) { | |
case 'add-owner': | |
return { | |
...state, | |
profiles: [...state.profiles, action.owner], | |
} | |
case 'add-owner-id': | |
return { ...state, ids: [...state.ids, action.id] } | |
default: | |
return state | |
} | |
} | |
export default ownersReducer |
frogsReducer.js
const frogsReducer = (state, action) => { | |
switch (action.type) { | |
case 'add-frog': | |
return { | |
...state, | |
profiles: [...state.profiles, action.frog], | |
} | |
case 'add-frog-id': | |
return { ...state, ids: [...state.ids, action.id] } | |
default: | |
return state | |
} | |
} | |
export default frogsReducer |
const frogsReducer = (state, action) => { | |
switch (action.type) { | |
case 'add-frog': | |
return { | |
...state, | |
profiles: [...state.profiles, action.frog], | |
} | |
case 'add-frog-id': | |
return { ...state, ids: [...state.ids, action.id] } | |
default: | |
return state | |
} | |
} | |
export default frogsReducer |
我们将它们导入到我们的主文件中并在那里定义状态结构:
App.js
import React from 'react' | |
import authReducer from './authReducer' | |
import ownersReducer from './ownersReducer' | |
import frogsReducer from './frogsReducer' | |
const initialState = { | |
auth: { | |
authenticated: false, | |
}, | |
owners: { | |
profiles: [], | |
ids: [], | |
}, | |
frogs: { | |
profiles: [], | |
ids: [], | |
}, | |
} | |
function rootReducer(state, action) { | |
return { | |
auth: authReducer(state.auth, action), | |
owners: ownersReducer(state.owners, action), | |
frogs: frogsReducer(state.frogs, action), | |
} | |
} | |
const useApp = () => { | |
const [state, dispatch] = React.useReducer(rootReducer, initialState) | |
const addFrog = (frog) => { | |
dispatch({ type: 'add-frog', frog }) | |
dispatch({ type: 'add-frog-id', id: frog.id }) | |
} | |
const addOwner = (owner) => { | |
dispatch({ type: 'add-owner', owner }) | |
dispatch({ type: 'add-owner-id', id: owner.id }) | |
} | |
React.useEffect(() => { | |
console.log(state) | |
}, [state]) | |
return { | |
...state, | |
addFrog, | |
addOwner, | |
} | |
} | |
const App = () => { | |
const { addFrog, addOwner } = useApp() | |
const onAddFrog = () => { | |
addFrog({ | |
name: 'giant_frog123', | |
id: 'jakn39eaz01', | |
}) | |
} | |
const onAddOwner = () => { | |
addOwner({ | |
name: 'bob_the_frog_lover', | |
id: 'oaopskd2103z', | |
}) | |
} | |
return ( | |
<> | |
<div> | |
<button type="button" onClick={onAddFrog}> | |
add frog | |
</button> | |
<button type="button" onClick={onAddOwner}> | |
add owner | |
</button> | |
</div> | |
</> | |
) | |
} | |
export default () => <App /> |
import React from 'react' | |
import authReducer from './authReducer' | |
import ownersReducer from './ownersReducer' | |
import frogsReducer from './frogsReducer' | |
const initialState = { | |
auth: { | |
authenticated: false, | |
}, | |
owners: { | |
profiles: [], | |
ids: [], | |
}, | |
frogs: { | |
profiles: [], | |
ids: [], | |
}, | |
} | |
function rootReducer(state, action) { | |
return { | |
auth: authReducer(state.auth, action), | |
owners: ownersReducer(state.owners, action), | |
frogs: frogsReducer(state.frogs, action), | |
} | |
} | |
const useApp = () => { | |
const [state, dispatch] = React.useReducer(rootReducer, initialState) | |
const addFrog = (frog) => { | |
dispatch({ type: 'add-frog', frog }) | |
dispatch({ type: 'add-frog-id', id: frog.id }) | |
} | |
const addOwner = (owner) => { | |
dispatch({ type: 'add-owner', owner }) | |
dispatch({ type: 'add-owner-id', id: owner.id }) | |
} | |
React.useEffect(() => { | |
console.log(state) | |
}, [state]) | |
return { | |
...state, | |
addFrog, | |
addOwner, | |
} | |
} | |
const App = () => { | |
const { addFrog, addOwner } = useApp() | |
const onAddFrog = () => { | |
addFrog({ | |
name: 'giant_frog123', | |
id: 'jakn39eaz01', | |
}) | |
} | |
const onAddOwner = () => { | |
addOwner({ | |
name: 'bob_the_frog_lover', | |
id: 'oaopskd2103z', | |
}) | |
} | |
return ( | |
<> | |
<div> | |
<button type="button" onClick={onAddFrog}> | |
add frog | |
</button> | |
<button type="button" onClick={onAddOwner}> | |
add owner | |
</button> | |
</div> | |
</> | |
) | |
} | |
export default () => <App /> |
然后,您只需像平常一样使用钩子调用dispatch
,将匹配type
和参数传递给指定的减速器。
需要查看的最重要的部分是rootReducer
:
function rootReducer(state, action) { | |
return { | |
auth: authReducer(state.auth, action), | |
owners: ownersReducer(state.owners, action), | |
frogs: frogsReducer(state.frogs, action), | |
} | |
} |
function rootReducer(state, action) { | |
return { | |
auth: authReducer(state.auth, action), | |
owners: ownersReducer(state.owners, action), | |
frogs: frogsReducer(state.frogs, action), | |
} | |
} |
4. Sentry 错误报告
Sentry与 React 集成后,项目受益匪浅。将所有详细的错误报告一次性发送到中心位置进行分析,这可谓一项至关重要的工具!
一旦您npm install @sentry/browser
为您的 React 应用程序设置好它,您就可以在创建帐户后登录sentry.io并在项目的仪表板中分析错误报告。
这些报告非常详细,因此您将受益匪浅,感觉自己像一名 FBI 特工,获得大量信息来帮助您解决这些错误,例如了解用户的设备、浏览器、发生错误的 URL、用户的 IP 地址、错误的堆栈跟踪、错误是否被处理、函数名称、源代码、显示导致错误的网络操作痕迹的有用面包屑列表、标题等。
以下是可能的屏幕截图:
您还可以让几个团队成员对不同的事情发表评论,这样它也可以成为一个协作环境。
5. 使用 axioswindow.fetch
除非你不在乎 IE 用户,否则不要window.fetch
在你的 React 应用中使用它,因为除非你提供 polyfill,否则所有 IE 浏览器都不window.fetch
支持它。Axios 不仅支持 IE,还带来了一些额外的功能,比如中途取消请求。这window.fetch
实际上适用于任何 Web 应用,而非 React 独有。它之所以出现在这个列表中,是因为它window.fetch
在如今的 React 应用中并不罕见。而且,由于 React 应用会根据配置的工具经历转译/编译阶段,因此很容易让人误以为它已经转译了window.fetch
。
6. 观察 DOM 节点时,使用回调引用而不是对象引用
尽管React.useRef
它是附加和控制 DOM 节点引用的新手,但它并不总是最好的选择。
有时您可能需要对 DOM 节点进行更多控制,以便提供额外的功能。
例如,React 文档展示了一种情况,你需要使用回调引用来确保即使当前引用值发生变化,外部组件仍然可以收到更新通知。这就是回调引用相对于 的优势useRef
。
Material-ui 利用这个强大的概念在其组件模块中附加了额外的功能。最棒的是,清理功能会自然而然地从这种行为中浮现出来。太棒了!
7.useWhyDidYouUpdate
这是一个自定义钩子,用于暴露导致组件重新渲染的变更。有时,当像高阶组件这样的记忆器React.memo
不够用时,你可以使用这个方便的钩子来查找需要考虑记忆的 props:(感谢Bruno Lemos)
import React from 'react' | |
function useWhyDidYouUpdate(name, props) { | |
// Get a mutable ref object where we can store props ... | |
// ... for comparison next time this hook runs. | |
const previousProps = useRef() | |
useEffect(() => { | |
if (previousProps.current) { | |
// Get all keys from previous and current props | |
const allKeys = Object.keys({ ...previousProps.current, ...props }) | |
// Use this object to keep track of changed props | |
const changesObj = {} | |
// Iterate through keys | |
allKeys.forEach((key) => { | |
// If previous is different from current | |
if (previousProps.current[key] !== props[key]) { | |
// Add to changesObj | |
changesObj[key] = { | |
from: previousProps.current[key], | |
to: props[key], | |
} | |
} | |
}) | |
// If changesObj not empty then output to console | |
if (Object.keys(changesObj).length) { | |
console.log('[why-did-you-update]', name, changesObj) | |
} | |
} | |
// Finally update previousProps with current props for next hook call | |
previousProps.current = props | |
}) | |
} | |
export default useWhyDidYouUpdate |
import React from 'react' | |
function useWhyDidYouUpdate(name, props) { | |
// Get a mutable ref object where we can store props ... | |
// ... for comparison next time this hook runs. | |
const previousProps = useRef() | |
useEffect(() => { | |
if (previousProps.current) { | |
// Get all keys from previous and current props | |
const allKeys = Object.keys({ ...previousProps.current, ...props }) | |
// Use this object to keep track of changed props | |
const changesObj = {} | |
// Iterate through keys | |
allKeys.forEach((key) => { | |
// If previous is different from current | |
if (previousProps.current[key] !== props[key]) { | |
// Add to changesObj | |
changesObj[key] = { | |
from: previousProps.current[key], | |
to: props[key], | |
} | |
} | |
}) | |
// If changesObj not empty then output to console | |
if (Object.keys(changesObj).length) { | |
console.log('[why-did-you-update]', name, changesObj) | |
} | |
} | |
// Finally update previousProps with current props for next hook call | |
previousProps.current = props | |
}) | |
} | |
export default useWhyDidYouUpdate |
然后你可以像这样使用它:
const MyComponent = (props) => { | |
useWhyDidYouUpdate('MyComponent', props) | |
return <Dashboard {...props} /> | |
} |
const MyComponent = (props) => { | |
useWhyDidYouUpdate('MyComponent', props) | |
return <Dashboard {...props} /> | |
} |
8. 让函数找到你
这段话将从我之前的文章中引用,因为它有点长,而且非常适合放在这篇文章里。内容如下:
让我举一个现实生活中的例子,因为我想更加强调这一点。
高阶函数的最大好处之一是,如果使用得当,它将为您和您周围的人节省大量时间。
在我的工作中,我们使用react-toastify来显示通知。我们到处都在用它。此外,它还能为快速的用户体验决策提供绝佳的应急方案:“我们应该如何处理这个错误?显示一个 toast 通知就行!” 搞定。
然而,我们开始注意到,随着应用规模的扩大和复杂程度的不断攀升,我们的 Toast 通知变得过于频繁。这没什么问题——只是我们没有办法防止重复通知。这意味着,即使某些 Toast 通知与上方的 Toast 通知完全相同,它们也会在屏幕上显示多次。
因此,我们最终利用库提供的 api 来帮助使用toast.dismiss()通过id删除活动的 toast 通知。
为了解释前面的部分,在继续之前显示我们要导入 toasts 的文件可能是个好主意:
import React from 'react' | |
import { GoCheck, GoAlert } from 'react-icons/go' | |
import { FaInfoCircle } from 'react-icons/fa' | |
import { MdPriorityHigh } from 'react-icons/md' | |
import { toast } from 'react-toastify' | |
/* | |
Calling these toasts most likely happens in the UI 100% of the time. | |
So it is safe to render components/elements as toasts. | |
*/ | |
// Keeping all the toast ids used throughout the app here so we can easily manage/update over time | |
// This used to show only one toast at a time so the user doesn't get spammed with toast popups | |
export const toastIds = { | |
// APP | |
internetOnline: 'internet-online', | |
internetOffline: 'internet-offline', | |
retryInternet: 'internet-retry', | |
} | |
// Note: this toast && is a conditional escape hatch for unit testing to avoid an error. | |
const getDefaultOptions = (options) => ({ | |
position: toast && toast.POSITION.BOTTOM_RIGHT, | |
...options, | |
}) | |
const Toast = ({ children, success, error, info, warning }) => { | |
let componentChildren | |
// Sometimes we are having an "object is not valid as a react child" error and children magically becomes an API error response, so we must use this fallback string | |
if (!React.isValidElement(children) && typeof children !== 'string') { | |
componentChildren = 'An error occurred' | |
} else { | |
componentChildren = children | |
} | |
let Icon = GoAlert | |
if (success) Icon = GoCheck | |
if (error) Icon = GoAlert | |
if (info) Icon = FaInfoCircle | |
if (warning) Icon = MdPriorityHigh | |
return ( | |
<div style={{ paddingLeft: 10, display: 'flex', alignItems: 'center' }}> | |
<div style={{ width: 30, height: 30 }}> | |
<Icon style={{ color: '#fff', width: 30, height: 30 }} /> | |
</div> | |
<div style={{ padding: 8, display: 'flex', alignItems: 'center' }}> | |
| |
<span style={{ color: '#fff' }}>{componentChildren}</span> | |
</div> | |
</div> | |
) | |
} | |
export const success = (msg, opts) => { | |
return toast.success(<Toast success>{msg}</Toast>, { | |
className: 'toast-success', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} | |
export const error = (msg, opts) => { | |
return toast.error(<Toast error>{msg}</Toast>, { | |
className: 'toast-error', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} | |
export const info = (msg, opts) => { | |
return toast.info(<Toast info>{msg}</Toast>, { | |
className: 'toast-info', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} | |
export const warn = (msg, opts) => { | |
return toast.warn(<Toast warning>{msg}</Toast>, { | |
className: 'toast-warn', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} | |
export const neutral = (msg, opts) => { | |
return toast(<Toast warning>{msg}</Toast>, { | |
className: 'toast-default', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} |
import React from 'react' | |
import { GoCheck, GoAlert } from 'react-icons/go' | |
import { FaInfoCircle } from 'react-icons/fa' | |
import { MdPriorityHigh } from 'react-icons/md' | |
import { toast } from 'react-toastify' | |
/* | |
Calling these toasts most likely happens in the UI 100% of the time. | |
So it is safe to render components/elements as toasts. | |
*/ | |
// Keeping all the toast ids used throughout the app here so we can easily manage/update over time | |
// This used to show only one toast at a time so the user doesn't get spammed with toast popups | |
export const toastIds = { | |
// APP | |
internetOnline: 'internet-online', | |
internetOffline: 'internet-offline', | |
retryInternet: 'internet-retry', | |
} | |
// Note: this toast && is a conditional escape hatch for unit testing to avoid an error. | |
const getDefaultOptions = (options) => ({ | |
position: toast && toast.POSITION.BOTTOM_RIGHT, | |
...options, | |
}) | |
const Toast = ({ children, success, error, info, warning }) => { | |
let componentChildren | |
// Sometimes we are having an "object is not valid as a react child" error and children magically becomes an API error response, so we must use this fallback string | |
if (!React.isValidElement(children) && typeof children !== 'string') { | |
componentChildren = 'An error occurred' | |
} else { | |
componentChildren = children | |
} | |
let Icon = GoAlert | |
if (success) Icon = GoCheck | |
if (error) Icon = GoAlert | |
if (info) Icon = FaInfoCircle | |
if (warning) Icon = MdPriorityHigh | |
return ( | |
<div style={{ paddingLeft: 10, display: 'flex', alignItems: 'center' }}> | |
<div style={{ width: 30, height: 30 }}> | |
<Icon style={{ color: '#fff', width: 30, height: 30 }} /> | |
</div> | |
<div style={{ padding: 8, display: 'flex', alignItems: 'center' }}> | |
| |
<span style={{ color: '#fff' }}>{componentChildren}</span> | |
</div> | |
</div> | |
) | |
} | |
export const success = (msg, opts) => { | |
return toast.success(<Toast success>{msg}</Toast>, { | |
className: 'toast-success', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} | |
export const error = (msg, opts) => { | |
return toast.error(<Toast error>{msg}</Toast>, { | |
className: 'toast-error', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} | |
export const info = (msg, opts) => { | |
return toast.info(<Toast info>{msg}</Toast>, { | |
className: 'toast-info', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} | |
export const warn = (msg, opts) => { | |
return toast.warn(<Toast warning>{msg}</Toast>, { | |
className: 'toast-warn', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} | |
export const neutral = (msg, opts) => { | |
return toast(<Toast warning>{msg}</Toast>, { | |
className: 'toast-default', | |
...getDefaultOptions(), | |
...opts, | |
}) | |
} |
现在请耐心听我说完,我知道这可能看起来不太吸引人。但我保证两分钟后就会好起来。
我们在一个单独的组件中实现了这个功能,用于检查屏幕上是否已经存在一个 Toast。如果存在,它会尝试移除该 Toast 并重新显示新的 Toast。
import { toast } from 'react-toastify' | |
import { | |
info as toastInfo, | |
success as toastSuccess, | |
toastIds, | |
} from 'util/toast' | |
import App from './App' | |
const Root = () => { | |
const onOnline = () => { | |
if (toast.isActive(toastIds.internetOffline)) { | |
toast.dismiss(toastIds.internetOffline) | |
} | |
if (toast.isActive(toastIds.retryInternet)) { | |
toast.dismiss(toastIds.retryInternet) | |
} | |
if (!toast.isActive(toastIds.internetOnline)) { | |
toastSuccess('You are now reconnected to the internet.', { | |
position: 'bottom-center', | |
toastId: toastIds.internetOnline, | |
}) | |
} | |
} | |
const onOffline = () => { | |
if (!toast.isActive(toastIds.internetOffline)) { | |
toastInfo('You are disconnected from the internet right now.', { | |
position: 'bottom-center', | |
autoClose: false, | |
toastId: toastIds.internetOffline, | |
}) | |
} | |
} | |
useInternet({ onOnline, onOffline }) | |
return <App /> | |
} |
import { toast } from 'react-toastify' | |
import { | |
info as toastInfo, | |
success as toastSuccess, | |
toastIds, | |
} from 'util/toast' | |
import App from './App' | |
const Root = () => { | |
const onOnline = () => { | |
if (toast.isActive(toastIds.internetOffline)) { | |
toast.dismiss(toastIds.internetOffline) | |
} | |
if (toast.isActive(toastIds.retryInternet)) { | |
toast.dismiss(toastIds.retryInternet) | |
} | |
if (!toast.isActive(toastIds.internetOnline)) { | |
toastSuccess('You are now reconnected to the internet.', { | |
position: 'bottom-center', | |
toastId: toastIds.internetOnline, | |
}) | |
} | |
} | |
const onOffline = () => { | |
if (!toast.isActive(toastIds.internetOffline)) { | |
toastInfo('You are disconnected from the internet right now.', { | |
position: 'bottom-center', | |
autoClose: false, | |
toastId: toastIds.internetOffline, | |
}) | |
} | |
} | |
useInternet({ onOnline, onOffline }) | |
return <App /> | |
} |
这工作正常——然而,我们整个应用程序中还有其他 Toast 消息需要以同样的方式进行修改。我们不得不检查每个显示 Toast 消息通知的文件,以删除重复的内容。
当我们想到在 2019 年检查所有文件时,我们立刻意识到这不是解决方案。所以我们查看了util/toast.js
文件,并对其进行了重构,以解决问题。重构后的样子如下:
src/util/toast.js
import React, { isValidElement } from 'react' | |
import isString from 'lodash/isString' | |
import isFunction from 'lodash/isFunction' | |
import { GoCheck, GoAlert } from 'react-icons/go' | |
import { FaInfoCircle } from 'react-icons/fa' | |
import { MdPriorityHigh } from 'react-icons/md' | |
import { toast } from 'react-toastify' | |
/* | |
Calling these toasts most likely happens in the UI 100% of the time. | |
So it is safe to render components/elements as toasts. | |
*/ | |
// Keeping all the toast ids used throughout the app here so we can easily manage/update over time | |
// This used to show only one toast at a time so the user doesn't get spammed with toast popups | |
export const toastIds = { | |
// APP | |
internetOnline: 'internet-online', | |
internetOffline: 'internet-offline', | |
retryInternet: 'internet-retry', | |
} | |
// Note: this toast && is a conditional escape hatch for unit testing to avoid an error. | |
const getDefaultOptions = (options) => ({ | |
position: toast && toast.POSITION.BOTTOM_RIGHT, | |
...options, | |
}) | |
const Toast = ({ children, success, error, info, warning }) => { | |
let componentChildren | |
// Sometimes we are having an "object is not valid as a react child" error and children magically becomes an API error response, so we must use this fallback string | |
if (!isValidElement(children) && !isString(children)) { | |
componentChildren = 'An error occurred' | |
} else { | |
componentChildren = children | |
} | |
let Icon = GoAlert | |
if (success) Icon = GoCheck | |
if (error) Icon = GoAlert | |
if (info) Icon = FaInfoCircle | |
if (warning) Icon = MdPriorityHigh | |
return ( | |
<div style={{ paddingLeft: 10, display: 'flex', alignItems: 'center' }}> | |
<div style={{ width: 30, height: 30 }}> | |
<Icon style={{ color: '#fff', width: 30, height: 30 }} /> | |
</div> | |
<div style={{ padding: 8, display: 'flex', alignItems: 'center' }}> | |
| |
<span style={{ color: '#fff' }}>{componentChildren}</span> | |
</div> | |
</div> | |
) | |
} | |
const toaster = (function() { | |
// Attempt to remove a duplicate toast if it is on the screen | |
const ensurePreviousToastIsRemoved = (toastId) => { | |
if (toastId) { | |
if (toast.isActive(toastId)) { | |
toast.dismiss(toastId) | |
} | |
} | |
} | |
// Try to get the toast id if provided from options | |
const attemptGetToastId = (msg, opts) => { | |
let toastId | |
if (opts && isString(opts.toastId)) { | |
toastId = opts.toastId | |
} else if (isString(msg)) { | |
// We'll just make the string the id by default if its a string | |
toastId = msg | |
} | |
return toastId | |
} | |
const handleToast = (type) => (msg, opts) => { | |
const toastFn = toast[type] | |
if (isFunction(toastFn)) { | |
const toastProps = {} | |
let className = '' | |
const additionalOptions = {} | |
const toastId = attemptGetToastId(msg, opts) | |
if (toastId) additionalOptions.toastId = toastId | |
// Makes sure that the previous toast is removed by using the id, if its still on the screen | |
ensurePreviousToastIsRemoved(toastId) | |
// Apply the type of toast and its props | |
switch (type) { | |
case 'success': | |
toastProps.success = true | |
className = 'toast-success' | |
break | |
case 'error': | |
toastProps.error = true | |
className = 'toast-error' | |
break | |
case 'info': | |
toastProps.info = true | |
className = 'toast-info' | |
break | |
case 'warn': | |
toastProps.warning = true | |
className - 'toast-warn' | |
break | |
case 'neutral': | |
toastProps.warning = true | |
className - 'toast-default' | |
break | |
default: | |
className = 'toast-default' | |
break | |
} | |
toastFn(<Toast {...toastProps}>{msg}</Toast>, { | |
className, | |
...getDefaultOptions(), | |
...opts, | |
...additionalOptions, | |
}) | |
} | |
} | |
return { | |
success: handleToast('success'), | |
error: handleToast('error'), | |
info: handleToast('info'), | |
warn: handleToast('warn'), | |
neutral: handleToast('neutral'), | |
} | |
})() | |
export const success = toaster.success | |
export const error = toaster.error | |
export const info = toaster.info | |
export const warn = toaster.warn | |
export const neutral = toaster.neutral |
import React, { isValidElement } from 'react' | |
import isString from 'lodash/isString' | |
import isFunction from 'lodash/isFunction' | |
import { GoCheck, GoAlert } from 'react-icons/go' | |
import { FaInfoCircle } from 'react-icons/fa' | |
import { MdPriorityHigh } from 'react-icons/md' | |
import { toast } from 'react-toastify' | |
/* | |
Calling these toasts most likely happens in the UI 100% of the time. | |
So it is safe to render components/elements as toasts. | |
*/ | |
// Keeping all the toast ids used throughout the app here so we can easily manage/update over time | |
// This used to show only one toast at a time so the user doesn't get spammed with toast popups | |
export const toastIds = { | |
// APP | |
internetOnline: 'internet-online', | |
internetOffline: 'internet-offline', | |
retryInternet: 'internet-retry', | |
} | |
// Note: this toast && is a conditional escape hatch for unit testing to avoid an error. | |
const getDefaultOptions = (options) => ({ | |
position: toast && toast.POSITION.BOTTOM_RIGHT, | |
...options, | |
}) | |
const Toast = ({ children, success, error, info, warning }) => { | |
let componentChildren | |
// Sometimes we are having an "object is not valid as a react child" error and children magically becomes an API error response, so we must use this fallback string | |
if (!isValidElement(children) && !isString(children)) { | |
componentChildren = 'An error occurred' | |
} else { | |
componentChildren = children | |
} | |
let Icon = GoAlert | |
if (success) Icon = GoCheck | |
if (error) Icon = GoAlert | |
if (info) Icon = FaInfoCircle | |
if (warning) Icon = MdPriorityHigh | |
return ( | |
<div style={{ paddingLeft: 10, display: 'flex', alignItems: 'center' }}> | |
<div style={{ width: 30, height: 30 }}> | |
<Icon style={{ color: '#fff', width: 30, height: 30 }} /> | |
</div> | |
<div style={{ padding: 8, display: 'flex', alignItems: 'center' }}> | |
| |
<span style={{ color: '#fff' }}>{componentChildren}</span> | |
</div> | |
</div> | |
) | |
} | |
const toaster = (function() { | |
// Attempt to remove a duplicate toast if it is on the screen | |
const ensurePreviousToastIsRemoved = (toastId) => { | |
if (toastId) { | |
if (toast.isActive(toastId)) { | |
toast.dismiss(toastId) | |
} | |
} | |
} | |
// Try to get the toast id if provided from options | |
const attemptGetToastId = (msg, opts) => { | |
let toastId | |
if (opts && isString(opts.toastId)) { | |
toastId = opts.toastId | |
} else if (isString(msg)) { | |
// We'll just make the string the id by default if its a string | |
toastId = msg | |
} | |
return toastId | |
} | |
const handleToast = (type) => (msg, opts) => { | |
const toastFn = toast[type] | |
if (isFunction(toastFn)) { | |
const toastProps = {} | |
let className = '' | |
const additionalOptions = {} | |
const toastId = attemptGetToastId(msg, opts) | |
if (toastId) additionalOptions.toastId = toastId | |
// Makes sure that the previous toast is removed by using the id, if its still on the screen | |
ensurePreviousToastIsRemoved(toastId) | |
// Apply the type of toast and its props | |
switch (type) { | |
case 'success': | |
toastProps.success = true | |
className = 'toast-success' | |
break | |
case 'error': | |
toastProps.error = true | |
className = 'toast-error' | |
break | |
case 'info': | |
toastProps.info = true | |
className = 'toast-info' | |
break | |
case 'warn': | |
toastProps.warning = true | |
className - 'toast-warn' | |
break | |
case 'neutral': | |
toastProps.warning = true | |
className - 'toast-default' | |
break | |
default: | |
className = 'toast-default' | |
break | |
} | |
toastFn(<Toast {...toastProps}>{msg}</Toast>, { | |
className, | |
...getDefaultOptions(), | |
...opts, | |
...additionalOptions, | |
}) | |
} | |
} | |
return { | |
success: handleToast('success'), | |
error: handleToast('error'), | |
info: handleToast('info'), | |
warn: handleToast('warn'), | |
neutral: handleToast('neutral'), | |
} | |
})() | |
export const success = toaster.success | |
export const error = toaster.error | |
export const info = toaster.info | |
export const warn = toaster.warn | |
export const neutral = toaster.neutral |
与其逐一检查每个文件,最简单的解决方案是创建一个高阶函数。这样做可以让我们“反转”角色,这样我们就不用逐一检查文件,而是将 Toast 消息直接发送到我们的高阶函数。
这样,文件中的代码就不会被修改或触及。它们仍然正常运行,而且我们能够删除重复的 Toast,而不必在最后编写不必要的代码。这节省了时间。
结论
这篇文章到此结束!希望你觉得它有用,并期待未来有更多精彩内容!
在Medium上找到我
文章来源:https://dev.to/jsmanifest/8-miraculous-ways-to-bolster-your-react-apps-273