【前端架构】使用React进行应用程序状态管理

Chinese, Simplified

Software Engineer, React Training, Testing JavaScript Training

React是管理应用程序状态所需的全部内容

 

管理状态可以说是任何应用程序中最难的部分。这就是为什么有这么多的状态管理库可用,而且每天都有更多的库出现(甚至有些库是建立在其他库之上的。。。npm上有数百个“更简单的Redux”的摘要)。尽管状态管理是一个很难解决的问题,但我认为,使之如此困难的一个原因是我们经常过度设计解决问题的方法。

有一个状态管理解决方案,我个人一直在使用React,随着React钩子的发布(以及对React上下文的大量改进),这种状态管理方法已经大大简化。

我们经常把React组件当作乐高积木来构建我们的应用程序,我想当人们听到这些时,他们会认为这不包括状态方面。我个人解决状态管理问题的方法背后的“秘密”是考虑应用程序的状态如何映射到应用程序的树结构。

redux如此成功的原因之一是react redux解决了支柱钻井问题。事实上,通过简单地将组件传递到某种神奇的connect函数中,就可以在树的不同部分共享数据,这一点非常棒。它对reducer/action creators/etc.的使用也很棒,但我相信redux的普遍存在是因为它解决了开发者的道具钻削痛点。

这就是我只在一个项目中使用redux的原因:我经常看到开发人员把他们所有的状态都放到redux中。不仅是全局应用程序状态,还包括本地状态。这会导致很多问题,尤其是当您维护任何状态交互时,它涉及到与reducer、action creator/type和dispatch调用的交互,这最终导致必须打开许多文件并在头脑中跟踪代码,以确定发生了什么以及它对代码库其余部分的影响。

很明显,对于真正全局的状态来说,这是很好的,但是对于简单状态(比如模态是开放的还是表单输入值状态),这是一个大问题。更糟糕的是,它的规模并不是很好。应用程序越大,这个问题就越难解决。当然,您可以连接不同的reducer来管理应用程序的不同部分,但是间接遍历所有这些action creator和reducer并不是最佳的。

将所有应用程序状态都放在一个对象中也会导致其他问题,即使您没有使用Redux。当一个反应<提供程序上下文>获取一个新值,使用该值的所有组件都将更新并必须呈现,即使它是只关心部分数据的函数组件。这可能会导致潜在的性能问题。(React reduxv6也尝试使用这种方法,直到他们意识到它不能正确地与hooks一起工作,这迫使他们在v7中使用不同的方法来解决这些问题。)但我的观点是,如果您的状态在逻辑上更为分离,并且位于React树中更靠近它的位置,那么就不会出现这个问题。

这是真正的关键,如果您使用React构建应用程序,那么您的应用程序中已经安装了状态管理库。你甚至不需要npm安装(或纱添加)它。它不需要为用户额外增加字节,它与npm上的所有React包集成,而且React团队已经对它进行了很好的记录。它自己反应。

React是一个状态管理库

当您构建React应用程序时,您将组装一组组件,以组成一个组件树,从<App/>开始,到<input/>、<div/>和<button/>结束。您不需要在一个中心位置管理应用程序呈现的所有低级复合组件。相反,你让每个单独的组件来管理它,它最终成为构建UI的一种非常有效的方法。你也可以用你的状态来做这件事,而且很可能你今天也会这样做:


 

 

function Counter() {

  const [count, setCount] = React.useState(0)

  const increment = () => setCount(c => c + 1)

  return <button onClick={increment}>{count}</button>

}

function App() {

  return <Counter />

}

请注意,我在这里所说的一切也适用于类组件。钩子只是让事情变得简单一点(特别是我们马上要讨论的上下文)。

class Counter extends React.Component {

  state = {count: 0}

  increment = () => this.setState(({count}) => ({count: count + 1}))

  render() {

    return <button onClick={this.increment}>{this.state.count}</button>

  }

“好吧,Kent,在一个组件中管理一个状态元素是很容易的,但是当我需要跨组件共享状态时,您会怎么做?例如,如果我想这样做呢:“

function CountDisplay() {

  // where does `count` come from?

  return <div>The current counter count is {count}</div>

}

function App() {

  return (

    <div>

      <CountDisplay />

      <Counter />

    </div>

  )

 

“计数是在<Counter/>中管理的,现在我需要一个状态管理库从<CountDisplay/>访问该计数值并在<Counter/>中更新它!”

这个问题的答案和反应本身一样古老(旧?)在我记事的时候,我就在文档里写了很久:提升状态

“提升国家”合法地回答了React中的国家管理问题,这是一个坚如磐石的答案。以下是如何将其应用于这种情况:

function Counter({count, onIncrementClick}) {

  return <button onClick={onIncrementClick}>{count}</button>

}

function CountDisplay({count}) {

  return <div>The current counter count is {count}</div>

}

function App() {

  const [count, setCount] = React.useState(0)

  const increment = () => setCount(c => c + 1)

  return (

    <div>

      <CountDisplay count={count} />

      <Counter count={count} onIncrementClick={increment} />

    </div>

  )

}

我们刚刚改变了谁对我们的国家负责,这真的很简单。我们可以一直提升状态,直到我们的应用程序的顶端。

“当然肯特,好吧,但是道具钻的问题呢?”

好问题。您的第一道防线就是改变构建组件的方式。利用组件组成。也许不是:

function App() {

  const [someState, setSomeState] = React.useState('some state')

  return (

    <>

      <Header someState={someState} onStateChange={setSomeState} />

      <LeftNav someState={someState} onStateChange={setSomeState} />

      <MainContent someState={someState} onStateChange={setSomeState} />

    </>

  )

}

你可以这样做:

function App() {

  const [someState, setSomeState] = React.useState('some state')

  return (

    <>

      <Header

        logo={<Logo someState={someState} />}

        settings={<Settings onStateChange={setSomeState} />}

      />

      <LeftNav>

        <SomeLink someState={someState} />

        <SomeOtherLink someState={someState} />

        <Etc someState={someState} />

      </LeftNav>

      <MainContent>

        <SomeSensibleComponent someState={someState} />

        <AndSoOn someState={someState} />

      </MainContent>

    </>

  )

}

如果这不是很清楚(因为它是超级做作),迈克尔杰克逊有一个伟大的视频,你可以看,以帮助澄清我的意思。

不过,最终,即使是组合也不能为您做到这一点,所以您的下一步是跳转到React的Context API中。这实际上是一个“解决方案”,但很长一段时间以来,这个解决方案是“非官方的”。正如我所说,很多人求助于react redux,因为它使用我所指的机制解决了这个问题,而不必担心react文档中的警告。但是,既然context是React API的一个官方支持的部分,那么我们可以直接使用它而没有任何问题:

// src/count/count-context.js

import * as React from 'react'

const CountContext = React.createContext()

function useCount() {

  const context = React.useContext(CountContext)

  if (!context) {

    throw new Error(`useCount must be used within a CountProvider`)

  }

  return context

}

function CountProvider(props) {

  const [count, setCount] = React.useState(0)

  const value = React.useMemo(() => [count, setCount], [count])

  return <CountContext.Provider value={value} {...props} />

}

export {CountProvider, useCount}

// src/count/page.js

import * as React from 'react'

import {CountProvider, useCount} from './count-context'

function Counter() {

  const [count, setCount] = useCount()

  const increment = () => setCount(c => c + 1)

  return <button onClick={increment}>{count}</button>

}

function CountDisplay() {

  const [count] = useCount()

  return <div>The current counter count is {count}</div>

}

function CountPage() {

  return (

    <div>

      <CountProvider>

        <CountDisplay />

        <Counter />

      </CountProvider>

    </div>

  )

}

注意:这个特定的代码示例非常做作,我不建议您使用上下文来解决这个特定的场景。请阅读支柱钻井,以获得更好的理解为什么支柱钻井不一定是一个问题,往往是可取的。不要太快接触上下文!

这种方法的酷之处在于,我们可以将更新状态的常用方法的所有逻辑放在useCount钩子中:

function useCount() {

  const context = React.useContext(CountContext)

  if (!context) {

    throw new Error(`useCount must be used within a CountProvider`)

  }

  const [count, setCount] = context

  const increment = () => setCount(c => c + 1)

  return {

    count,

    setCount,

    increment,

  }

}

你也可以很容易地用这个来说明:

function countReducer(state, action) {

  switch (action.type) {

    case 'INCREMENT': {

      return {count: state.count + 1}

    }

    default: {

      throw new Error(`Unsupported action type: ${action.type}`)

    }

  }

}

function CountProvider(props) {

  const [state, dispatch] = React.useReducer(countReducer, {count: 0})

  const value = React.useMemo(() => [state, dispatch], [state])

  return <CountContext.Provider value={value} {...props} />

}

function useCount() {

  const context = React.useContext(CountContext)

  if (!context) {

    throw new Error(`useCount must be used within a CountProvider`)

  }

  const [state, dispatch] = context

  const increment = () => dispatch({type: 'INCREMENT'})

  return {

    state,

    dispatch,

    increment,

  }

}

这为您提供了极大的灵活性,并将复杂性降低了一个数量级。在这样做的时候,要记住以下几点:

  • 并非应用程序中的所有内容都需要处于单个状态对象中。保持逻辑上的分离(用户设置不必与通知处于同一上下文中)。使用此方法将有多个提供程序。
  • 不是所有的上下文都需要全局访问!让状态政府尽可能靠近需要的地方。

关于第二点的更多信息。你的应用程序树可能如下所示:

function App() {

  return (

    <ThemeProvider>

      <AuthenticationProvider>

        <Router>

          <Home path="/" />

          <About path="/about" />

          <UserPage path="/:userId" />

          <UserSettings path="/settings" />

          <Notifications path="/notifications" />

        </Router>

      </AuthenticationProvider>

    </ThemeProvider>

  )

}

function Notifications() {

  return (

    <NotificationsProvider>

      <NotificationsTab />

      <NotificationsTypeList />

      <NotificationsList />

    </NotificationsProvider>

  )

}

function UserPage({username}) {

  return (

    <UserProvider username={username}>

      <UserInfo />

      <UserNav />

      <UserActivity />

    </UserProvider>

  )

}

function UserSettings() {

  // this would be the associated hook for the AuthenticationProvider

  const {user} = useAuthenticatedUser()

}

 

请注意,每个页面都可以有自己的提供程序,其中包含其下组件所需的数据。代码拆分对这种东西也“管用”。如何将数据导入每个提供程序取决于这些提供程序使用的钩子以及如何在应用程序中检索数据,但您知道从何处开始查找(在提供程序中)如何工作。

关于为什么这个托管是有益的,请查看我的“State colosition will make your React app faster”和“colocation”博客文章。有关上下文的更多信息,请阅读如何有效地使用React context

服务器缓存与UI状态

最后我想补充一点。状态有多种类型,但每种类型的状态都可以分为两种类型:

  • 服务器缓存—实际存储在服务器上的状态,我们将其存储在客户机中以便快速访问(如用户数据)。
  • UI状态—仅在UI中用于控制应用程序交互部分的状态(如模态isOpen状态)。

当我们把两者结合在一起时,我们犯了一个错误。服务器缓存与UI状态有着本质上不同的问题,因此需要进行不同的管理。如果你接受这样一个事实:你所拥有的根本不是状态,而是一个状态缓存,那么你就可以开始正确地思考它,从而正确地管理它。

当然,您可以使用自己的useState或useReducer在这里和那里使用正确的useContext来管理它。但请允许我帮你直截了当地说,缓存是一个非常困难的问题(有人说它是计算机科学中最难的问题之一),在这个问题上站在巨人的肩膀上是明智的。

这就是为什么我对这种状态使用并推荐react query。我知道我知道,我告诉过你不需要状态管理库,但我并不认为react query是状态管理库。我认为这是个藏匿处。这真是个好主意。看看!坦纳·林斯利是个聪明的小甜饼。

性能怎么样?

当你遵循上面的建议时,性能就很少是个问题了。尤其是当你遵循有关托管的建议时。但是,在某些用例中,性能可能会有问题。当您遇到与状态相关的性能问题时,首先要检查的是有多少组件由于状态更改而被重新呈现,并确定这些组件是否真的需要由于状态更改而重新呈现。如果是这样,那么perf问题不在管理状态的机制中,而是在渲染速度上,在这种情况下,需要加快渲染速度。

但是,如果您注意到有许多组件在没有DOM更新或需要的副作用的情况下进行渲染,那么这些组件将不必要地进行渲染。在React中,这种情况一直都会发生,而且它本身通常不是问题(您应该首先集中精力快速进行不必要的重新渲染),但是如果这真的是瓶颈,那么以下是一些在React上下文中使用state解决性能问题的方法:

  • 将你的状态划分为不同的逻辑部分,而不是在一个大的存储区中,这样对状态的任何部分进行一次更新都不会触发对应用程序中每个组件的更新。
  • 优化上下文提供程序
  • 把 jotai带进来

这又是一个库的建议。的确,有些用例React的内置状态管理抽象不太适合。在所有可用的抽象中,jotai对于这些用例是最有前途的。如果您想知道这些用例是什么,那么jotai很好地解决的问题类型实际上在 Recoil: State Management for Today's React - Dave McCabe aka @mcc_abe at @ReactEurope 2020一书中得到了很好的描述。Recoil和jotai非常相似(并且解决了相同类型的问题)。但根据我和他们的(有限)经验,我更喜欢jotai。

无论如何,大多数应用程序都不需要像recoil或jotai这样的原子状态管理工具。

结论

同样,这是你可以用类组件来做的事情(你不必使用钩子)。钩子使这变得容易得多,但是您可以用React 15来实现这一理念。尽可能保持状态的本地性,并且只有在支柱钻井成为问题时才使用上下文。这样做会使您更容易维护状态交互。

 

原文:https://kentcdodds.com/blog/application-state-management-with-react/

本文:http://jiagoushi.pro/node/1282

讨论:请加入知识星球【首席架构师圈】或者小号【jiagoushi_pro】或者QQ群【11107777】

SEO Title
Application State Management with React