带有React Context和hook的Redux内置替代方案

来自翻译者:

我提供了一篇有关如何实现有效的解决方案以用React上下文和挂钩替换Redux的文章的免费翻译。 欢迎指出翻译或文本中的错误。 观赏愉快。



自从React 16.3.0中发布新的Context API以来,许多人问自己新的API是否足够好以至于可以替代Redux? 我也有同样的想法,但是即使在发布带有钩子的16.8.0版本后,我还是不完全理解。 我尝试使用流行的技术,但并不总是了解它们所解决的所有问题,因此我也习惯于Redux。

碰巧,我签了肯特·多德斯(Kent C. Dodds)的时事通讯,并发现了几封有关上下文和状态管理的电子邮件。 我开始阅读.... 并阅读...,并在5篇博客文章中点击了某些内容。

要了解其背后的所有基本概念,我们将单击一个按钮,单击该按钮,我们将收到与icanhazdadjoke一起的笑话并显示它们。 这是一个很小但足够的例子。

为了准备,让我们从两个看似随机的技巧开始。

首先,让我介绍一下我的朋友console.count

 console.count('Button') // Button: 1 console.count('Button') // Button: 2 console.count('App') // App: 1 console.count('Button') // Button: 3 

我们将向每个组件添加一个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 = {} //       console.log(obj === obj) // true //        //  2   console.log({} === {}) // false 

每次此上下文的值更改时,都会重新渲染使用该上下文的组件。 让我们看看上下文提供者的含义:

 <DadJokeContext.Provider value={{ state, actions }}> 

在这里,我们在每个渲染器期间创建一个新对象,但这是不可避免的,因为每次执行操作( dispatch )时都会创建一个新对象,因此根本无法缓存( memoize )此值。

一切似乎都结束了,对吧?

如果我们看fetchDadJoke函数,它从外部范围使用的唯一东西就是dispatch ,对吗? 通常,我将告诉您有关useReduceruseState创建的函数的一些秘密。 为简便起见,我将使用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中优化重新渲染的一个简单技巧

Source: https://habr.com/ru/post/zh-CN463607/


All Articles