有人说:构建
React应用就像用积木搭房子,每个组件就是一块积木。
这么说的人可能忽视了state(状态)—— 不同于组件是以树的形式组合,我们经常会在不同层级的组件间公用同一个state。
面对这种情况,有些同学选择引入状态管理库(比如Redux)。
事实上,React内置的contextAPI可以解决大部分状态传递问题。
本文接下来要讲的,就是如何更有效的使用context。
错误前置
在我们的Demo中,有个context——CountContext。
当其render时如果上层结构中不存在context provider为他提供context value,则在解构context value时会报错。
const CountContext = React.createContext();
function CountDisplay() {
// 解构语法报错
const {count} = React.useContext(CountContext);
return <div>{count}</div>;
}
这是因为CountContext没有默认值,所以为undefined。将undefined当作对象解构时报错。
在有些场景下默认值是有意义的。但是大多数情况,context consumer需要context provider为他提供有用的context value。
这意味着使用context的业务组件需要判断undefined,否则可能出现运行时错误。
为了解决这个问题,我们可以用自定义hook将错误前置。
// src/count-context.js
const CountContext = React.createContext();
function useCount() {
const context = React.useContext(CountContext);
if (context === undefined) {
throw new Error('必须在CountProvider内使用useCount');
}
return context;
}
同时提供CountProvider:
// src/count-context.js
function CountProvider({children}) {
const stateHook = React.useState();
return (
<CountContext.Provider value={stateHook}>
{children}
</CountStateContext.Provider>
)
}
在需要CountContext的业务组件中,我们不再需要直接使用CountContext,而是引入useCount与CountProvider:
// src/app.jsx
import {useCount, CountProvider} from './count-context.js'
function CountDisplay() {
const [count] = useCount();
return <div>{count}</div>;
}
function App() {
return (
<CountProvider>
<CountDisplay />
</CountProvider>
)
}
如果在未包裹CountProvider的情况下单独使用useCount,会直接抛出错误,而不需要等到使用context时再报错。
这种将错误前置的方式能够帮我们更好的规避运行时错误。
state与dispatch分离
在CountProvider中,stateHook作为context value传递给context consuer。
function CountProvider({children}) {
const stateHook = React.useState();
// ...
}
其中state与改变state的方法(dispatch)同时存在于context value中。
有些时候,展示state的组件与触发state变化的组件不是同一个组件,比如:
function App() {
return (
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
)
}
其中CountDisplay用于展示state。
Counter用于触发state变化。
由于state与dispatch同时存在于context value,state变化后CountDisplay与Counter都会重新render。
这对于只负责触发组件state变化的Counter来说是不必要的。
为此,我们可以将state与dispatch分离。
修改CountProvider:
// src/count-context.js
const CountStateContext = React.createContext();
const CountDispatchContext = React.createContext();
function CountProvider({children}) {
const [state, dispatch] = React.useState();
return (
<CountStateContext.Provider value={state}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
同时,useCount也拆分为useCountState与useCountDispatch:
// src/count-context.js
function useCountState() {
const context = React.useContext(CountStateContext);
if (context === undefined) {
throw new Error('必须在CountProvider内使用useCountState');
}
return context;
}
function useCountDispatch() {
const context = React.useContext(CountDispatchContext);
if (context === undefined) {
throw new Error('必须在CountProvider内使用useCountDispatch');
}
return context;
}
在CountDisplay中使用useCountState。
在Counter中使用useCountDispatch。
这样,即使state变化,只有CountDisplay会render,Counter不会render。
更灵活的拓展
事实上,将context分离为动态(state),静态(dispatch)两部分,这套context的实践可以拥有更灵活的拓展。
当需要更多state时,可以将CountProvider的useState替换为useReducer:
// src/count-context.js
function CountProvider({children}) {
const [state, dispatch] = React.useReducer(countReducer);
return (
<CountStateContext.Provider value={state}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
当需要更多方法时,你也可以拓展CountDispatchContext。
总结
通过这套context的“更”佳实践,审视下我们现有的业务,是不是很多时候并不需要额外的状态管理库呢?