Posted on:
Last modified:
useState
是 React 开发中最常用的一个钩子。但当程序稍微复杂一些的时候,只依赖 useState 就
显得有些力不从心了,这时候需要一个全局状态管理工具。
在我刚接触 React 时,看到的教程一般都推荐 redux,在经历过无数次的尝试之后,我发现以我的 智商理解不了 redux 神奇的设计,也接受不了 redux 冗长的 boilerplate 代码。不过幸运的是, 有了 React 内置的 useReducer + useContext,完全可以不用 redux。
首先来开下 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 用来向所有后代元素广播状态,可以跳跃组件树层级,而不需要层层传递。
React.createContext
定义一个高层次 Context<MyContext.Provider />
来包裹需要接受这个 context 的所有组件<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>
}
除了使用使用 <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>
</>
}
看到这里你可能会问,useReducer 看起来不错,那怎么实现 redux-thunk 这种数据请求功能呢?
我的回答可能有些争议——不要实现 redux-thunk 的功能,用 swr 或者 ReactQuery 来请求数据。
UI 状态和数据状态是两个东西,然而人们总是把这两个东西混淆在一起。Redux 等工具实际上是 一个 UI 状态工具,但是人们总用它(通过 thunk) 来请求数据,这是错误的。
在 Redux 等工具的 readme 中一般都是提供的 Counter 或者 TODO list 这个 demo, 这是非常误导 人的,因为这两个 demo 只是本地的 UI 状态管理。到了实际使用的过程中,大多数状态是数据状态, 需要使用 thunk 等 middleware 来获取后端数据并管理,非常丑陋。
UI 状态指的是前端的一些状态,比如说是否使用暗黑模式,某个状态栏是否显示等等,和后端的 数据库无关。数据状态指的是从后端数据库中加载的一些数据,比如说用户名,当前的文章,评论 等等。如果前端进行了更新,也需要写回到后端数据库中。
按照前面的划分,实际上 SWR 也算一个(隐式)状态管理工具,它相当于使用了 API 的路径作为了 key, 然后把整个状态存储到了一个类似 kv 字典的结构当中。
一旦把 UI 状态和(后端)数据状态这两个概念分清了,那么解决起来就一下子开朗了。我们可以 使用 useReducer 管理前端程序自身的状态,而获取数据都通过 SWR 来实现。
关于 swr,可以参考我的另一篇文章:SWR 才是真正的数据状态管理工具
© 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 教程站