来自翻译者:
我提供了一篇有关如何实现有效的解决方案以用React上下文和挂钩替换Redux的文章的免费翻译。 欢迎指出翻译或文本中的错误。 观赏愉快。
自从React 16.3.0中发布新的Context API以来,许多人问自己新的API是否足够好以至于可以替代Redux? 我也有同样的想法,但是即使在发布带有钩子的16.8.0版本后,我还是不完全理解。 我尝试使用流行的技术,但并不总是了解它们所解决的所有问题,因此我也习惯于Redux。
碰巧,我签了
肯特·多德斯(Kent C. Dodds)的时事通讯,并发现了几封有关上下文和状态管理的电子邮件。 我开始阅读.... 并阅读...,并在5篇博客文章中点击了某些内容。
要了解其背后的所有基本概念,我们将单击一个按钮,单击该按钮,我们将收到与
icanhazdadjoke一起的笑话并显示它们。 这是一个很小但足够的例子。
为了准备,让我们从两个看似随机的技巧开始。
首先,让我介绍一下我的朋友
console.count
:
console.count('Button')
我们将向每个组件添加一个
console.count
调用,以查看其渲染次数。 太酷了吧?
其次,当重新渲染React组件时,它
不会重新渲染作为
children
元素传递的内容。
function Parent({ children }) { const [count, setCount] = React.useState(0) console.count('Parent') return ( <div> <button type="button" onClick={() => { setCount(count => count + 1) }}> Force re-render </button> {children} </div> ) } function Child() { console.count('Child') return <div /> } function App() { return ( <Parent> <Child /> </Parent> ) }
在按钮上单击几下后,您应该在控制台中看到以下内容:
Parent: 1 Child: 1 Parent: 2 Parent: 3 Parent: 4
请记住,这是提高应用程序性能的一种经常被忽视的方法。
现在我们已经准备好,让我们创建应用程序的框架:
import React from 'react' function Button() { console.count('Button') return ( <button type="button"> Fetch dad joke </button> ) } function DadJoke() { console.count('DadJoke') return ( <p>Fetched dad joke</p> ) } function App() { console.count('App') return ( <div> <Button /> <DadJoke /> </div> ) } export default App
Button
应该接收一个动作生成器(大约是Action Creator。翻译取自
Redux文档的俄语 ),这将得到一个笑话。
DadJoke
应该获取状态,并且
App
使用Provider上下文显示两个组件。
现在创建一个自定义组件并将其
DadJokeProvider
,它将在内部管理状态并将子组件包装在Context Provider中。 请记住,由于上述React中的子级优化,更新其状态不会重新渲染整个应用程序。
因此,创建一个文件并将其命名为
contexts/dad-joke.js
:
import React from 'react' const DadJokeContext = React.createContext() export function DadJokeContextProvider({ children }) { const state = { dadJoke: null } const actions = { fetchDadJoke: () => {}, } return ( <DadJokeContext.Provider value={{ state, actions }}> {children} </DadJokeContext.Provider> ) }
我们还导出2个钩子,以从上下文中获取值。
export function useDadJokeState() { return React.useContext(DadJokeContext).state } export function useDadJokeActions() { return React.useContext(DadJokeContext).actions }
现在我们可以实现这一点:
import React from 'react' import { DadJokeProvider, useDadJokeState, useDadJokeActions, } from './contexts/dad-joke' function Button() { const { fetchDadJoke } = useDadJokeActions() console.count('Button') return ( <button type="button" onClick={fetchDadJoke}> Fetch dad joke </button> ) } function DadJoke() { const { dadJoke } = useDadJokeState() console.count('DadJoke') return ( <p>{dadJoke}</p> ) } function App() { console.count('App') return ( <DadJokeProvider> <Button /> <DadJoke /> </DadJokeProvider> ) } export default App
在这里! 借助API,我们使用了钩子。 在整个帖子中,我们将不再对此文件进行任何更改。
让我们从
DadJokeProvider
的状态开始,向上下文文件添加功能。 是的,我们可以只使用
useState
钩子,但是让我们通过简单地向我们添加众所周知的和受欢迎的
Redux
功能来通过
reducer
管理我们的状态。
function reducer(state, action) { switch (action.type) { case 'SET_DAD_JOKE': return { ...state, dadJoke: action.payload, } default: return new Error(); } }
现在,我们可以将此减速器传递给
useReducer
钩子,并通过API获得笑话:
export function DadJokeProvider({ children }) { const [state, dispatch] = React.useReducer(reducer, { dadJoke: null }) async function fetchDadJoke() { const response = await fetch('https://icanhazdadjoke.com', { headers: { accept: 'application/json', }, }) const data = await response.json() dispatch({ type: 'SET_DAD_JOKE', payload: data.joke, }) } const actions = { fetchDadJoke, } return ( <DadJokeContext.Provider value={{ state, actions }}> {children} </DadJokeContext.Provider> ) }
应该工作! 点击按钮应该会收到并显示笑话!
让我们检查控制台:
App: 1 Button: 1 DadJoke: 1 Button: 2 DadJoke: 2 Button: 3 DadJoke: 3
每次更新状态时都会重新渲染这两个组件,但实际上只有其中一个使用它。 想象一个真正的应用程序,其中数百个组件仅使用动作。 如果我们能够提供所有这些可选的重新渲染,这会很好吗?
在这里,我们进入相对平等的领域,下面提醒一下:
const obj = {}
每次此上下文的值更改时,都会重新渲染使用该上下文的组件。 让我们看看上下文提供者的含义:
<DadJokeContext.Provider value={{ state, actions }}>
在这里,我们在每个渲染器期间创建一个新对象,但这是不可避免的,因为每次执行操作(
dispatch
)时都会创建一个新对象,因此根本无法缓存(
memoize
)此值。
一切似乎都结束了,对吧?
如果我们看
fetchDadJoke
函数,它从外部范围使用的唯一东西就是
dispatch
,对吗? 通常,我将告诉您有关
useReducer
和
useState
创建的函数的一些秘密。 为简便起见,我将使用
useState
作为示例:
let prevSetCount function Counter() { const [count, setCount] = React.useState() if (typeof prevSetCount !== 'undefined') { console.log(setCount === prevSetCount) } prevSetCount = setCount return ( <button type="button" onClick={() => { setCount(count => count + 1) }}> Increment </button> ) }
多次单击按钮,然后查看控制台:
true true true
您会注意到,
setCount
对于每个渲染
setCount
相同的功能。 这也适用于我们的
dispatch
功能。
这意味着我们的
fetchDadJoke
函数
fetchDadJoke
依赖于随时间变化的任何东西,也不依赖于任何其他动作生成器,因此动作对象只需要在第一次渲染时创建一次:
const actions = React.useMemo(() => ({ fetchDadJoke, }), [])
现在我们有了一个带有操作的缓存对象,我们可以优化上下文值吗? 实际上,不,因为无论我们如何优化值对象,由于状态变化,每次仍然需要创建一个新对象。 但是,如果我们将一个动作对象从现有上下文移动到新的上下文,该怎么办? 谁说我们只能有一个背景?
const DadJokeStateContext = React.createContext() const DadJokeActionsContext = React.createContext()
我们可以在我们的
DadJokeProvider
组合这两种上下文:
return ( <DadJokeStateContext.Provider value={state}> <DadJokeActionsContext.Provider value={actions}> {children} </DadJokeActionsContext.Provider> </DadJokeStateContext.Provider> )
并调整我们的钩子:
export function useDadJokeState() { return React.useContext(DadJokeStateContext) } export function useDadJokeActions() { return React.useContext(DadJokeActionsContext) }
我们完成了! 认真地,下载尽可能多的笑话,自己看看。
App: 1 Button: 1 DadJoke: 1 DadJoke: 2 DadJoke: 3 DadJoke: 4 DadJoke: 5
因此,您已经实现了自己的优化状态管理解决方案! 您可以使用两个上下文模板来创建不同的提供程序,以创建您的应用程序,但不仅如此,还可以多次渲染同一个提供程序组件! 什么 是的,请在多个位置尝试
DadJokeProvider
渲染,并查看状态管理实现如何轻松扩展!
释放您的想象力,并回顾为什么您真正需要
Redux
。
感谢Kent C. Dodds提供有关两个上下文模板的文章。 我从未在任何地方见过他,在我看来,这改变了比赛规则。
阅读以下Kent博客文章,以获取有关我所讨论的概念的更多信息:
何时使用useMemo和useCallback如何优化背景价值如何有效使用React Context在React中管理应用程序状态在React中优化重新渲染的一个简单技巧