增强 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 会将其视为已更改的值。urlsApp
计算机程序有时会自以为比我们聪明,从而逃脱这种糟糕的行为。为了解决这个问题,我们可以这样设置:只要包含 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已启用。如果在秘密路由中使用了此功能,则提供阻止未激活用户查看秘密内容的功能。trueopenedVisibilityControl
3. 组合 Reducer 来创建一个庞大的 Reducer
有时你需要将应用中的两个或多个 Reducer 组合起来,形成一个更大的 Reducer。这种方法与combineReducersReact-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
后端开发教程 - Java、Spring Boot 实战 - msg200.com



