编者注 :本文于 2022 年 9 月 23 日更新,添加了关于为什么我们需要在 React 中进行状态管理的信息,添加了之前未包含在文章中的其他状态管理工具,例如 Jotai、MobX 和 Zusand,并添加了有关哪些状态管理的信息工具是 React 的最佳选择。
状态管理是每个开发人员在构建 React 应用程序时面临的基本挑战——而且它不是一个小问题。 在 React 中有很多有效的管理状态的方法,每一种都解决了一组突出的问题。
作为开发人员,重要的是不仅要了解不同的方法、工具和模式,还要了解它们的用例和权衡。
考虑状态管理的一个有用方法是根据我们在项目中解决的问题。 在本文中,我们将介绍在 React 中管理状态的常见用例,并了解何时应该考虑使用每种解决方案。 我们将通过构建一个简单的计数器应用程序来实现这一点。
为什么我需要在 React 中进行状态管理?
React 中的本地组件状态
React 中的组件道具
React 中的路由
使用 React 的 Context API useReducer
使用 Redux 进行状态管理
带有 Recoil 的原子状态
带有 XState 的状态机
使用 Jotai 进行原始和灵活的状态管理
使用 MobX 进行简单、可扩展的状态管理
使用 Zustand 进行状态管理
使用 React Query 获取数据
哪种状态管理工具最适合 React?
首先,让我们讨论一下状态管理的重要性。 React 中的状态是一个 JavaScript 对象,它可以根据用户的操作来改变组件的行为。 状态也可以被认为是组件的内存。
React 应用程序是使用管理自己状态的组件构建的。 这适用于小型应用程序,但随着应用程序复杂性的增加,处理组件之间的共享状态变得越来越复杂和成问题。
这是一个简单的示例,说明金融科技应用程序中的成功交易如何影响其他几个组件:
新交易将更改首页显示的可用余额
新交易现在将显示为用户总交易历史中的最新交易
这就是为什么在开发可扩展的 React 应用程序时状态管理必不可少的原因。 从长远来看,如果状态管理不正确,应用程序无疑会遇到问题。 像这样不断地对应用程序进行故障排除和重建可能会变得乏味。
实现计数器的最简单方法是使用本地组件状态和 useState钩。
import { useState } from 'react' const Counter = () => { const [count, setCount] = useState(0) const increaseCount = () => { setCount(count + 1) } const decreaseCount = () => { if (count > 0) { setCount(count - 1) } } return () } export default Counter{count}
所以我们完成了,对吧? 文章结束? 不完全的。
超过 20 万开发人员使用 LogRocket 来创造更好的数字体验 了解更多 →
如果这是一个真正的项目,那么将来我们可能会在应用程序的其他地方需要更多按钮和标题。 确保它们的外观和行为一致是一个好主意,这就是为什么我们应该将它们变成可重用的 React 组件的原因。
转动我们的 Button和 Header分成不同的组件揭示了一个新的挑战。 我们需要某种方式在它们和主节点之间进行通信 Counter零件。
这就是组件道具发挥作用的地方。 对于我们的 Header组件,我们添加一个 text支柱。 对于我们的 Button,我们都需要一个 label道具和一个 onClick打回来。 我们的代码现在看起来像这样:
import { useState } from 'react' const Header = ({ text }) =>{text}
const Button = ({ label, onClick }) => ( ) const Counter = () => { const [count, setCount] = useState(0) const increaseCount = () => { setCount(count + 1) } const decreaseCount = () => { if (count > 0) { setCount(count - 1) } } return () } export default Counter
这看起来很棒! 但是想象一下下面的场景:如果我们只需要在我们的家乡路线上显示计数并且有一个单独的路线怎么办? /controls我们在哪里显示计数和控制按钮? 我们应该怎么做呢?
鉴于我们正在构建一个单页应用程序,现在我们需要处理第二个状态——我们所在的路线。 例如,让我们看看如何使用 React Router 来完成。
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { useState } from 'react' const Header = ({ text }) =>{text}
const Button = ({ label, onClick }) => ( ) const Home = ({ count }) => { return} const Controls = ({ count, decreaseCount, increaseCount }) => { return ( <> > ) } const App = () => { const [count, setCount] = useState(0) const increaseCount = () => { setCount(count + 1) } const decreaseCount = () => { if (count > 0) { setCount(count - 1) } } return ( ) } export default App
好的! 我们现在有了单独的路线,一切都按预期工作。 但是,您可能会注意到一个问题。 我们将计数状态保持在 App并使用道具将其传递给组件树。 但似乎我们一遍又一遍地传递相同的道具,直到我们到达需要使用它的组件。 当然,随着我们的应用程序的增长,它只会变得更糟。 这被称为支柱钻孔。
让我们修复它!
如果有办法让我们的组件访问 count状态而不必通过道具接收? React Context API 和 useReducerHook 就是这样做的:
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { createContext, useContext, useReducer } from 'react' const initialState = 0 const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 >= 0 ? state - 1 : 0 default: return state } } const CountContext = createContext(null) const useCount = () => { const value = useContext(CountContext) if (value === null) throw new Error('CountProvider missing') return value } const CountProvider = ({ children }) => ({children} ) const Header = ({ text }) =>{text}
const Button = ({ label, onClick }) => ( ) const Home = () => { const [state] = useCount() return} const Controls = () => { const [state, dispatch] = useCount() return ( <>
惊人的! 我们已经解决了支柱钻孔的问题。 我们通过创建描述性化简器使我们的代码更具声明性,从而获得了额外的积分。
我们对我们的实施感到满意,而且对于许多用例来说,这确实是我们所需要的。 但是,如果我们可以保持计数,这样每次刷新页面时它不会重置为 0,那不是很好吗? 并记录应用程序状态? 崩溃报告呢?
了解我们的应用程序在崩溃时所处的确切状态以及在我们使用它时如何利用令人惊叹的开发工具将非常有帮助。 好吧,我们可以使用 Redux 做到这一点!
不要错过 The Replay 来自 LogRocket 的精选时事通讯
了解 LogRocket 的 Galileo 如何消除噪音以主动解决应用程序中的问题
使用 React 的 useEffect 优化应用程序的性能
之间切换 在多个 Node 版本
了解如何 使用 AnimXYZ 为您的 React 应用程序制作动画
探索 Tauri ,一个用于构建二进制文件的新框架
比较 NestJS 与 Express.js
我们可以完成以上所有工作以及更多 通过使用Redux 来管理我们应用程序的状态, 工作。 该工具背后有一个强大的社区和一个 的丰富生态系统。 可以轻松利用
设置我们的计数器 让我们使用Redux Toolkit 。
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { configureStore, createSlice } from '@reduxjs/toolkit' import { useSelector, useDispatch, Provider } from 'react-redux' const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: state => { state.value += 1 }, decrement: state => { if (state.value > 0) { state.value -= 1 } }, }, }) const store = configureStore({ reducer: { counter: counterSlice.reducer }, }) const { increment, decrement } = counterSlice.actions const Header = ({ text }) =>{text}
const Button = ({ label, onClick }) => ({label} ) const Home = () => { const count = useSelector(state => state.counter.value) return} const Controls = () => { const count = useSelector(state => state.counter.value) const dispatch = useDispatch() return ( <> dispatch(decrement())} label="-" /> dispatch(increment())} label="+" /> > ) } const App = () => { return ( ) } export default App
这看起来真的很整洁! 我们的状态现在存储在全局 Redux 存储中并使用纯函数进行管理(Redux Toolkit 在后台使用 Immer 来保证不变性)。 我们已经可以利用很棒的 Redux DevTools 了 。
但是诸如处理副作用、使状态持久化或实现日志记录和/或崩溃报告之类的事情呢? 这就是我们前面提到的 Redux 生态系统发挥作用的地方。
有多种处理副作用的选项,包括 redux-thunk 和 redux-saga 。 像 redux-persist 这样的库非常适合将来自 redux 存储的数据保存在本地或会话存储中以使其持久化。
总之,Redux 很棒! 它在 React 世界中被广泛使用,这是有充分理由的。
但是,如果我们更喜欢去中心化的状态管理方法呢? 也许我们担心性能,或者在 React 树的不同分支中有频繁的数据更新,所以我们希望在保持一切同步的同时避免不必要的重新渲染。
或者,也许我们需要一种好的方法来从我们的状态中获取数据并在客户端上高效且稳健地进行计算。 如果我们想在不牺牲应用程序范围的状态观察能力的情况下实现所有这些呢? 输入反冲。
建议我们可以通过一个简单的计数器应用程序来达到 React Context 或 Redux 的极限,这有点牵强。 如需更好的原子状态管理用例,请查看 Dave McCabe 在 Recoil 上的精彩视频。
尽管如此,从原子的角度考虑状态确实有助于扩展我们对状态管理可能是什么样子的词汇。 此外,Recoil API 玩起来很有趣,所以让我们用它重新实现我们的计数器。
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { atom, useRecoilState, RecoilRoot } from 'recoil' const countState = atom({ key: 'count', default: 0, }) const Header = ({ text }) =>{text}
const Button = ({ label, onClick }) => ({label} ) const Home = () => { const [count] = useRecoilState(countState) return} const Controls = () => { const [count, setCount] = useRecoilState(countState) const increaseCount = () => { setCount(count + 1) } const decreaseCount = () => { if (count > 0) { setCount(count - 1) } } return ( <> > ) } const App = () => { return ( ) } export default App
Using Recoil feels very much like using React itself. A peek back at our initial examples reveals how similar the two are. Recoil also has its very own set of dev tools. An important consideration to keep in mind is that this library is still experimental and subject to change. Use it with caution.
Okay, we can have a Recoil counter. But state management preferences depend on our priorities. What if the app is built by a team and it is really important that the developer, the designer, the project manager, and everyone else speak the same language when it comes to user interfaces?
此外,如果这种语言可以在我们的应用程序中用高度声明性的代码直接表达怎么办? 如果我们能保证我们永远不会达到不可能的状态,从而消除一整类错误呢? 你猜怎么着? 我们可以。
以上所有都可以借助状态图和状态机来实现。 状态图有助于可视化我们应用程序的所有可能状态并定义可能的状态。 它们很容易在整个团队中理解、分享和讨论。
这是我们的计数器状态图:
尽管这是一个微不足道的实现,但我们已经可以看到使用状态机的一个很酷的优势。 最初,不可能递减计数器,因为它的初始值为 0。这个逻辑在我们的状态机中声明并且在图表上可见,在我们探索的其他方法中,一般来说,找到正确的方法更难它的地方。
这是我们在实践中的状态机:
>import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { useMachine } from '@xstate/react' import { createMachine, assign } from 'xstate' export const counterMachine = createMachine({ initial: 'active', context: { count: 0 }, states: { active: { on: { INCREMENT: { actions: assign({ count: ctx => ctx.count + 1 }), }, DECREMENT: { cond: ctx => ctx.count > 0, actions: assign({ count: ctx => ctx.count - 1, }), }, }, }, }, }) const Header = ({ text }) =>{text}
const Button = ({ label, onClick }) => ({label} ) const Home = () => { const [state] = useMachine(counterMachine) return} const Controls = () => { const [state, send] = useMachine(counterMachine) return ( <> send('DECREMENT')} label="-" /> send('INCREMENT')} label="+" /> > ) } const App = () => { return ( ) } export default App
Wow, this is really great! However, we are only barely scratching the surface of state machines here. To find out more about them, check out the docs for XState.
Alright, last scenario! What happens if our simple frontend counter app has a backend? What if we need to communicate with a server in order to get or modify the count? What if, in addition, we want to handle data-fetching-related challenges like asynchronicity, loading states, caching, and re-fetching?
我们已经在 Recoil 部分介绍了原子模型,Jotai 采用了类似的方法。 J otai 甚至受到 Recoil atomic 模型的启发 ,所以这对我们来说应该是在公园里散步。 让我们重新实现我们的计数器应用程序,但这次使用 Jotai。
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { atom, useAtom } from 'jotai' // Create your atoms and derivatives const countState = atom(0) const Header = ({ text }) => {text} const Button = ({ label, onClick }) => {label} const Home = () => { const [count] = useAtom(countState) return } const Controls = () => { const [count, setCount] = useAtom(countState) const increaseCount = () => { setCount(count + 1) } const decreaseCount = () => { if (count > 0) { setCount(count - 1) } } return ( <> ) } const App = () => { return ( Home Controls ) } export default App
我们可以看到 Jotai 与 Recoil 非常相似。 使用 Jotai 也感觉像使用 React 的 useState.
使用 Jotai,可以通过组合原子来创建状态,并根据原子依赖性优化渲染。 这消除了对记忆技术的要求,并克服了 React 上下文的额外重新渲染问题。
MobX 深受面向对象编程和反应式编程原理的影响。 它允许您将特定的数据片段识别为“可观察的”,然后将它们包装起来并跟踪对该数据所做的任何更改,更新观察数据的任何其他代码。
使用 MobX 为我们的计数器应用程序重写状态管理相当容易,所以让我们这样做:
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { observable, action } from 'mobx'; const appState = observable({ count: 0, incCounter: action("decrease", function () { appState.count += 1; }) decCounter:action("increase", function() { appState.count -= 1; }) }) const Header = ({ text }) => {text} const Button = ({ label, onClick }) => {label} const Home = () => { const count = appState.count return } const Controls = () => { const count = appState.count const increaseCount = appState.incCounter const decreaseCount = () => { if (count > 0) { appState.decCounter } } return ( <> ) } const App = () => { return ( Home Controls ) } export default App
Zusand 是一个功能强大且紧凑的状态管理库 。 它的 API 是围绕钩子构建的,使其易于理解和使用。 Zusand 解决了常见问题,例如 僵尸子问题 、 React 并发 和 的上下文丢失。 混合渲染器之间
让我们使用 Zusand 设置我们的计数器应用程序:
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import create from "zustand"; const useStore= create((set) => ({ count: 0; increment: ()=> set((state) => ({ count: state.count + 1})), decrement: ()=> set((state) => ({ count: state.count - 1})) })) const Header = ({ text }) => {text} const Button = ({ label, onClick }) => {label} const Home = () => { const count = useStore((state) => state.count) return } const Controls = () => { const count = useStore((state) => state.count) const increaseCount = useStore((state) => state.increment) } const decreaseCount = () => { if (count > 0) { useStore(useStore(state) => state.decrement) } } return ( <> ) } const App = () => { return ( Home Controls ) } export default App
Zusand 易于使用和设置; 您需要做的就是创建一个商店(您的商店是一个钩子!),如上面的示例所示。 存储可以包含任何内容,包括函数、对象和原语。 我们现在可以在我们的应用程序中使用我们的钩子了。
Zusand 和 Redux 都基于不可变状态模型,因此,如果您了解 Redux,您应该能够了解 Zusand。
我要强调的最后一个 React 状态管理工具是 React Query 。 它专门设计用于简化数据获取并解决上述(以及更多)问题。 让我们看看它的实际效果。
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { ReactQueryDevtools } from 'react-query/devtools' import axios from 'axios' import { useQuery, useMutation, QueryClient, QueryClientProvider, } from 'react-query' const useCount = () => { return useQuery('count', async () => { const { data } = await axios.get('https://our-counter-api.com/count') return data }) } const useIncreaseCount = () => { return useMutation(() => axios.post('https://our-counter-api.com/increase', { onSuccess: () => { queryClient.invalidateQueries('count') }, }), ) } const useDecreaseCount = () => { return useMutation( () => axios.post('https://our-counter-api.com/descrease'), { onSuccess: () => { queryClient.invalidateQueries('count') }, }, ) } const Header = ({ text }) =>{text}
const Button = ({ label, onClick }) => ({label} ) const Home = () => { const { status, data, error } = useCount() return status === 'loading' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : () } const Controls = () => { const { status, data, error } = useCount() const increaseCount = useIncreaseCount() const decreaseCount = useDecreaseCount() return status === 'loading' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <> decreaseCount.mutate()} label="-" /> increaseCount.mutate()} label="+" /> > ) } const queryClient = new QueryClient() const App = () => { return ( ) } export default App
以上是一个相当幼稚的实现,有很大的改进空间。 需要注意的重要一点是我们可以轻松地进行服务器调用,缓存它们,并在需要时使缓存无效。 此外,使用 React Query,管理组件中的加载和错误状态的任务变得更加简单。
这是一个很棒的工具,可以与任何后端一起使用。 如果您想知道如何使用 GraphQL 进行设置,请查看我的文章 。
上面讨论的所有状态管理库都试图解决相同的问题,每个库都提供了一种独特的方法来处理整个应用程序中的共享数据。
寻找最佳状态管理库取决于您正在从事的项目和您自己的个人偏好。 对于 React 的情况,一些库可能是过度杀伤力 useState非常适合这份工作。
Redux 无疑是长期以来社区的最爱,它可以在许多较旧的 React 代码库中找到。 因此,对 Redux 有透彻的了解通常是非常有益的。
一般来说,学习 Redux 和 Recoil 是一个很好的途径。 Recoil 以非常低的学习曲线有效地处理了状态管理问题,而对 Redux 的透彻理解将大大减少维护旧 React 代码库所需的时间。
React 中的状态管理是一个广泛的话题。 本文中讨论的方法、模式和库的列表既不全面也不明确。 目的是说明以特定方式解决特定问题背后的思维过程。
最后,React 中的状态管理归结为了解不同的选项,了解它们的好处和权衡,并最终选择最适合我们用例的解决方案。
快乐编码!
调试 React 应用程序可能很困难,尤其是当用户遇到难以重现的问题时。 如果您对监控和跟踪 Redux 状态、自动显示 JavaScript 错误以及跟踪缓慢的网络请求和组件加载时间感兴趣,请 尝试 LogRocket 。
LogRocket 就像一个用于 Web 和移动应用程序的 DVR,几乎可以记录您的 React 应用程序上发生的所有事情。 无需猜测问题发生的原因,您可以汇总并报告问题发生时应用程序所处的状态。 LogRocket 还监控您的应用程序的性能,并使用客户端 CPU 负载、客户端内存使用情况等指标进行报告。
LogRocket Redux 中间件包为您的用户会话增加了一层额外的可见性。 LogRocket 记录来自 Redux 存储的所有操作和状态。