$ ls ~yifei/notes/

useReducer + useContext = (Better) Redux

Posted on:

Last modified:

useState 是 React 开发中最常用的一个钩子。但当程序稍微复杂一些的时候,只依赖 useState 就 显得有些力不从心了,这时候需要一个全局状态管理工具。

在我刚接触 React 时,看到的教程一般都推荐 redux,在经历过无数次的尝试之后,我发现以我的 智商理解不了 redux 神奇的设计,也接受不了 redux 冗长的 boilerplate 代码。不过幸运的是, 有了 React 内置的 useReducer + useContext,完全可以不用 redux。

useReducer

首先来开下 useReducer 的 API.

const [state, dispatch] = useReducer(reducerFn, initialState [, init]);

useReducer 通常放在一组状态的根元素层级,如一个页面。dispatch 函数触发事件,reducer 函数用来处理事件,更新 state.

当单独使用 reducer 的时候,其实就相当于一个高级的 useState,dispatch 写起来要比 setState 更清晰一些。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {...state, count: state.count + 1};
    case 'decrement':
      return {...state, count: state.count - 1};
    default:
      throw new Error(`action type ${action.type} not found`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useReducer 的 reducer 函数和 redux 不同,不需要 state=initialState 参数。 默认参数在调用 useReducer 的时候已经给出了。

Context API

Context 用来向所有后代元素广播状态,可以跳跃组件树层级,而不需要层层传递。

  1. 首先需要通过 React.createContext 定义一个高层次 Context
  2. 然后在最外层使用 <MyContext.Provider /> 来包裹需要接受这个 context 的所有组件
  3. 在需要使用状态的元素中调用 <MyContext.Consumer />, 访问 Context 中的值

例子:

const AppContext = createContext()

function App({children}) {
  return <AppContext.Provider value={42}>
    {children}
  </AppContext.Provider>
}

function Page() {
  return <AppContext.Consumer>
    <span>{value}</span>
  </AppContext.Consumer>
}

useContext

除了使用使用 <Consumer/> 以外,还可以使用 useContext 钩子来读取 Context 中的值。这样就 不用额外再嵌套一层 Consumer 了。

function Page() {
  // 注意这里的参数是 Context
  const value = useContext(AppContext);
  return <span>{value}</span>;
}

组合起来

用 Context.Provider 把 state 和 dispatch 这两个变量广播给所有元素,这样每个组件都可以使用 useContext(Context) 访问到 state 和 dispatch 和两个函数,从而可以使用或者更新全局状态。 也就实现了 redux 的核心功能。

首先定义 store.js

import React, { useContext, useReducer } from 'react';

// 初始状态
const initialState = {
  user: { name: "", },
  sidebar: { showToolbox: false, }
};

// 用来组合不同的 reducer
function combineReducers(reducers) {
  return function(state, action) {
    const newState = {};
    for (let key in reducers)
      newState[key] = reducers[key](state[key], action);
    return newState;
  }
}

// 处理用户状态的 reducer
function userReducer(state, action) {
  switch (action.type) {
    case "user.updateName":
      return { ...state, name: action.data.name }
    default:
      return state;
  }
}

// 处理 sidebar 的 reducer
function sidebarReducer(state, action) {
  switch (action.type) {
    case "sidebar.toggleToolbox":
      return { ...state, showToolbox: !state.showToolbox }
    default:
      return state;
  }
}

// 组合起来
const reducer = combineReducers({
  user: userReducer,
  ui: uiReducer,
})

const Context = createContext(initialState)

export function Store({ children }) {
  // useReducer 实际上只在这里调用了一次
  const [state, dispatch] = useReducer(reducer, initialState);
  // 把 state 和 dispatch 传递给所有元素
  return <Context.Provider value={[state, dispatch]}> {children} </Context.Provider>
}

// 自定义钩子 useStore 调用 useContext,读取 state 和 dispatch
export function useStore() { return useContext(Context); }

在 app.js 或者 index.js 中用 Context.Provider 包裹所有元素。

import {Store, useStore} from "./store.js"

export default function App({children}) {
    return <Store>{children}</Store>
}

在组件中就可以使用 state 和 dispatch 和其他组件通信了。

// page.js
import {useStore} from './store'

export default function Page() {
    const [state, dispatch] = useStore()
    return <>
        <p>Hello, {state.user.name}</p>
        <SomeComponent />
    </>
}

// some-component.js
export default function SomeComponent() {
    const [state, dispatch] = useStore()
    return <>
        <button
            onClick={dispatch({type: "user.updateName", data: {name: "Sheldon"})}
        >Login</button>
    </>
}

怎么请求数据?redux-thunk 呢?

看到这里你可能会问,useReducer 看起来不错,那怎么实现 redux-thunk 这种数据请求功能呢?

我的回答可能有些争议——不要实现 redux-thunk 的功能,用 swr 或者 ReactQuery 来请求数据。

UI 状态和数据状态是两个东西,然而人们总是把这两个东西混淆在一起。Redux 等工具实际上是 一个 UI 状态工具,但是人们总用它(通过 thunk) 来请求数据,这是错误的。

在 Redux 等工具的 readme 中一般都是提供的 Counter 或者 TODO list 这个 demo, 这是非常误导 人的,因为这两个 demo 只是本地的 UI 状态管理。到了实际使用的过程中,大多数状态是数据状态, 需要使用 thunk 等 middleware 来获取后端数据并管理,非常丑陋。

UI 状态指的是前端的一些状态,比如说是否使用暗黑模式,某个状态栏是否显示等等,和后端的 数据库无关。数据状态指的是从后端数据库中加载的一些数据,比如说用户名,当前的文章,评论 等等。如果前端进行了更新,也需要写回到后端数据库中。

  • 对于 UI 状态管理来说,redux, mobx, 甚至包括 useReducer 等工具都是非常合适的,但是他们 真的不是数据状态管理工具。
  • 对于服务端数据的获取和缓存,需要使用单独的网络请求工具,而不是所谓的状态管理工具, useSWR 和 ReactQuery 是非常适合的工具。

按照前面的划分,实际上 SWR 也算一个(隐式)状态管理工具,它相当于使用了 API 的路径作为了 key, 然后把整个状态存储到了一个类似 kv 字典的结构当中。

一旦把 UI 状态和(后端)数据状态这两个概念分清了,那么解决起来就一下子开朗了。我们可以 使用 useReducer 管理前端程序自身的状态,而获取数据都通过 SWR 来实现。

关于 swr,可以参考我的另一篇文章:SWR 才是真正的数据状态管理工具

WeChat Qr Code

© 2016-2022 Yifei Kong. Powered by ynotes

All contents are under the CC-BY-NC-SA license, if not otherwise specified.

Opinions expressed here are solely my own and do not express the views or opinions of my employer.

友情链接: MySQL 教程站