增强 React 应用的 8 种神奇方法

2025-05-25

增强 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

使用 memoizers 的 React 技巧

计算机程序有时会自以为比我们聪明,从而逃脱这种糟糕的行为。为了解决这个问题,我们可以这样设置:只要包含 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
}
view raw memoize1.jsx hosted with ❤ by GitHub
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
}
view raw memoize1.jsx hosted with ❤ by GitHub

如果我们现在运行这个函数,它仍然会发送 8 个请求。这是因为虽然我们记住了urls数组,但我们还需要记住promises钩子内部的变量,因为钩子运行时,这些变量也会实例化:

const promises = React.useMemo(() => {
return urls.map((url) => axios.get(url))
}, [urls])
view raw memoize1b.js hosted with ❤ by GitHub
const promises = React.useMemo(() => {
return urls.map((url) => axios.get(url))
}, [urls])
view raw memoize1b.js hosted with ❤ by GitHub

现在,我们的代码运行时应该只会发送 4 个请求。太棒了!

reactjs 使用 usememo memoizer 的技巧

2. 将 Props 合并到 Children

有时,我们可能会遇到这样的情况:在渲染之前,我们想偷偷地将一个 prop 与子元素合并。React 允许你查看任何 React 元素以及其他元素的 prop,例如公开其key

我们可以用一个新组件包装子元素并从那里注入新的道具,或者我们可以使用此方法合并新的道具。

例如,假设我们有一个App使用钩子的组件,useModal该钩子通过提供诸如openclose和 之类的控件来提供一些方便的实用程序来管理模态窗口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>
)
view raw mergeProps.jsx hosted with ❤ by GitHub
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>
)
view raw mergeProps.jsx hosted with ❤ by GitHub

VisibilityControl确保其子进程正常使用之前activated已启用。如果在秘密路由中使用了此功能,则提供阻止未激活用户查看秘密内容的功能。trueopenedVisibilityControl

3. 组合 Reducer 来创建一个庞大的 Reducer

有时你需要将应用中的两个或多个 Reducer 组合起来,形成一个更大的 Reducer。这种方法与combineReducersReact-redux 中的工作方式类似。

假设我们计划制作一个巨大的微服务应用程序,我们最初计划指定应用程序中的每个部分负责自己的上下文/状态,但后来我们想到了一个价值百万美元的应用程序创意,它需要将状态统一为一个大状态,这样我们就可以在同一环境中管理它们。

我们有authReducer.jsownersReducer.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
view raw authReducer.js hosted with ❤ by GitHub
const authReducer = (state, action) => {
switch (action.type) {
case 'set-authenticated':
return { ...state, authenticated: action.authenticated }
default:
return state
}
}
export default authReducer
view raw authReducer.js hosted with ❤ by GitHub

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
view raw frogsReducer.js hosted with ❤ by GitHub
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
view raw frogsReducer.js hosted with ❤ by GitHub

我们将它们导入到我们的主文件中并在那里定义状态结构:

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 />
view raw App.jsx hosted with ❤ by GitHub
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 />
view raw App.jsx hosted with ❤ by GitHub

然后,您只需像平常一样使用钩子调用dispatch,将匹配type和参数传递给指定的减速器。

需要查看的最重要的部分是rootReducer

function rootReducer(state, action) {
return {
auth: authReducer(state.auth, action),
owners: ownersReducer(state.owners, action),
frogs: frogsReducer(state.frogs, action),
}
}
view raw rootReducer.js hosted with ❤ by GitHub
function rootReducer(state, action) {
return {
auth: authReducer(state.auth, action),
owners: ownersReducer(state.owners, action),
frogs: frogsReducer(state.frogs, action),
}
}
view raw rootReducer.js hosted with ❤ by GitHub

4. Sentry 错误报告

Sentry与 React 集成后,项目受益匪浅。将所有详细的错误报告一次性发送到中心位置进行分析,这可谓一项至关重要的工具!

一旦您npm install @sentry/browser为您的 React 应用程序设置好它,您就可以在创建帐户后登录sentry.io并在项目的仪表板中分析错误报告。

这些报告非常详细,因此您将受益匪浅,感觉自己像一名 FBI 特工,获得大量信息来帮助您解决这些错误,例如了解用户的设备、浏览器、发生错误的 URL、用户的 IP 地址、错误的堆栈跟踪、错误是否被处理、函数名称、源代码、显示导致错误的网络操作痕迹的有用面包屑列表、标题等。

以下是可能的屏幕截图:

使用 sentry 的 React 提示,用于 React 应用程序的错误报告服务

您还可以让几个团队成员对不同的事情发表评论,这样它也可以成为一个协作环境。

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' }}>
&nbsp;&nbsp;
<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,
})
}
view raw toast.js hosted with ❤ by GitHub
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' }}>
&nbsp;&nbsp;
<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,
})
}
view raw toast.js hosted with ❤ by GitHub

现在请耐心听我说完,我知道这可能看起来不太吸引人。但我保证两分钟后就会好起来。

我们在一个单独的组件中实现了这个功能,用于检查屏幕上是否已经存在一个 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 />
}
view raw root.js hosted with ❤ by GitHub
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 />
}
view raw root.js hosted with ❤ by GitHub

这工作正常——然而,我们整个应用程序中还有其他 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' }}>
&nbsp;&nbsp;
<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
view raw toast.js hosted with ❤ by GitHub
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' }}>
&nbsp;&nbsp;
<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
view raw toast.js hosted with ❤ by GitHub

与其逐一检查每个文件,最简单的解决方案是创建一个高阶函数。这样做可以让我们“反转”角色,这样我们就不用逐一检查文件,而是将 Toast 消息直接发送到我们的高阶函数

这样,文件中的代码就不会被修改或触及。它们仍然正常运行,而且我们能够删除重复的 Toast,而不必在最后编写不必要的代码。这节省了时间

结论

这篇文章到此结束!希望你觉得它有用,并期待未来有更多精彩内容!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/8-miraculous-ways-to-bolster-your-react-apps-273
PREV
React 中的 8 种可能导致应用崩溃的做法
NEXT
让你落后的 7 个错误