使用 React Hooks 来创建全局状态

标签: 教程 翻译 React 发布于:2021-02-22 15:04:25 编辑于:2022-11-15 13:06:19 浏览量:1833

本文由 JustSong 翻译,翻译耗时:~ 80 min

原文链接:https://labs.thisdot.co/blog/creating-a-global-state-with-react-hooks

前言

多年前当我刚开始在 React 生态开发的时候, 我接触到了应用状态管理的 “Flux 模式” 的思想, 其包含了一些诸如 Redux,Flow 以及 Mobx 的工具。 我用 Redux 做开发已经有段时间了,非常喜欢它, 甚至把它用在一些用 node 写的状态机应用中,完全和 React 或者前端无关。

Flux 模式的核心原则有:

  1. 应用中的数据源要集中,而不能分散到各个组件中去。
  2. 应用状态应仅当用户在 UI 上进行的了某些操作(action)后发生改变。
  3. 这些操作不应该直接去更新状态,而应被派遣(dispatch)到一个中心的交换所, 其中包含了所有的状态更新相关的逻辑。

基本上,应当有且仅有一个地方所有的组件可以从此处获取信息以及对告知某个操作已经发生。 Redux 通过所谓的 reducer 函数和来实现这一模式。当操作(action)被派遣(dispatch)时, 该 reducer 函数被执行,其接受两个参数:当前状态和具体的动作,并据此返回新的状态。

Redux & React

我很喜欢这个模式,即便要让其和 React 配合使用时会有一些麻烦。 React 组件的渲染函数仅当其父组件所传递给其的 props 发生变化时才会被触发。 组件自己是不能直接设置监听应用状态改变的监听器的。 因此当全局状态改变时,这些改变不能自动反射到应用的 UI 上,这一点很致命。

一种快速而草率的解决方法是在根组件保存应用状态,并将 prop(包含用于更新的回调函数)层层传递下去。 这种做的问题是,一旦应用程序变得复杂起来,层层传递 prop 将变得笨重麻烦, 且对测试而言是一个明显的阻碍。此即所谓的 props drilling

Redux 通过创建所谓的 连接组件(connected conponents) 来解决该问题. 你需要用一个 connect 函数来将任何需要访问全局状态以及对其发送状态更新请求(dispatch action)的组件包装起来。

这种方式创建了一个更高阶的组件,其包裹了你自己写的组件, 因此这个更高阶的组件可以通过传递 props 的方式来传递给子组件全局状态和相应的调遣函数。 这种方法的结果是很多组件都类似这样子:

const MyButton = (props) => {
  return (
    <button onClick={props.toggleButton}>
      { props.active ? "On" : "Off" }
    </button>
  )
}

const mapStateToProps = (state) => ({
  buttonIsActive: state.buttonIsActive
})

const mapDispatchToProps = (dispatch) => {
  toggleButton: () => dispatch({ type: "click_button" })
}

export default connect(mapStateToProps, mapDispatchToProps)(MyButton)

React Hooks

在 2019 年上半年发布的 React Hooks 改变了很多关于开发模式的观念, 对于组件而言获取和更改全局状态变得简单和清除很多。 例如对于一个按钮而言,如果你只需要一个自包含(self-contained)的开关状态, 你可以通过 useState Hook 来很轻松地进行实现:

const [active, setActive] = React.useState(true)

对于自包含的状态,useState 已经很棒了,但是对于别的情况,我们就需要用到一些别的 Hook 函数。

首先,我们先介绍一下 useReducer。 如果你熟悉 useState,你就知道其接受一个值作为状态默认值, 返回一个包含两个值的数组, 即当前的状态值以及相应的 setter 函数。 useReduer 与之类似,不过其接受的参数是一个 Redux 风格的 reducer 函数, 返回的是全局状态以及相应的 dipatch 函数。

以下是一个简单的单动作 reducer 函数以及一个初始值的例子:

// contexts/User/reducer.js

export const reducer = (state, action) => {
  switch (action.type) {
    case "toggle_button":
      return {
        ...state,
        active: !state.active
      }

    default:
      return state
  }
}

export const initialState = {
  active: false
}

我们可以在任何 React 组件中创建一个由 reducer 函数驱动的状态,但是这个状态仅对该组件可见:

const [state, dispatch] = React.useReducer(reducer, initialState)

为了使状态全局可用,我们需要配合 useContext 使用。 Context 提供了一种无需顺着组件树层层下传就能传递 props 的方法。

在接下来的例子中,我们首先导入之前所提到的 reducer 函数以及相应的初始状态, 之后将创建并导出一个组件,其:

  1. 用 reducer 函数来创建和维护一个全局状态和对应的派遣,之后
  2. 返回一个更高阶的由 React.createContext 函数调用所产生的 Provider 组件。 并把状态和 dispatch 函数封装到一个数组中,并作为一个名称为 value 的 prop 传递给该高阶组件。
// contexts/User/index.jsx

import React from "react"
import { reducer, initialState } from "./reducer"

export const UserContext = React.createContext({
  state: initialState,
  dispatch: () => null
})

export const UserProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initialState)

  return (
    <UserContext.Provider value={[ state, dispatch ]}>
        { children }
    </UserContext.Provider>
  )
}

不要担心,这里就是最难理解的部分,但其是一种通用模式,先记住就好,不影响我们使用 :D

下一步是把我们的整个应用(或者至少是我们想要访问该全局状态的所有组件的共享父组件) 包裹着该 Provider 组件中。

// components/App.jsx

import { UserProvider } from "../contexts/UserProvider"

// Some other components you've written for your app...
import Header from "./Header"
import Main from "./Main"

export default () => {
  return (
    <UserProvider>
      <Header />
      <Main />
    </UserProvider>
  )
}

最后,对于和任何想要访问和修改该全局状态的组件,其只需要导入该 context, 并作为参数传给 useContext 就好:

// components/MyButton.jsx

import React from "react"
import { UserContext } from "../contexts/User"

export default () => {
  const [ state, dispatch ] = React.useContext(UserContext)

  return (
    <button onClick={() => dispatch({ type: "toggle_button" })}>
      { state.active ? "On" : "Off" }  
    </button>
  )
}

调用的结果是一个数组,其中包含了全局状态和相应的 dispatch 函数。 对的,这就是我们之前传递给 Provider 组件的名称为 value 的 prop。

就这些,没了!

未经允许,禁止转载,本文源站链接:https://iamazing.cn/