如何高效地使用React Context by Kent C. Dodds
This is a translation of the original post How to use React Context effectively by Kent C. Dodds.
本篇翻译自Kent C. Dodds 的原作 如何高效地使用React Context Kent C. Dodds
在React应用状态管理一文中,我提到过如何混合使用本地状态和React Context可以更好地管理React应用里的状态。在那篇文章中,我也展示了一些例子。今天我想就那些例子强调几点,也想说说如何高效地创建React Context Consumers来避免一些问题,这同时也能帮助你的应用或者库提高开发者体验和Context对象的可维护性。
在那之前,请务必阅读React应用状态管理. 谨记不要在一需要处理状态共享的问题时,就立马使用context来解决。 希望本篇文章能让你更好地了解如何有效地使用context。也请记住,context并不一定要存在于整个应用中的全局空间,context也可以在你的组件树中。在你的应用里,你可以(也许更应该)有多个逻辑分明的context。
首先,让我们来创建一个src/count-context.js
,并且如下创建context:
import * as React from 'react'
const CountContext = React.createContext()
一开始我并没有给CountContext
定义任何初始值。如果我想有一个初始值,我可以调用React.createContext({count: 0})
。不过,我故意不想有个默认值。defaultValue只在如下场景有用:
function CountDisplay() {
const {count} = React.useContext(CountContext)
return <div>{count}</div>
}
ReactDOM.render(<CountDisplay />, document.getElementById('⚛️'))
正因为CountContext
没有默认值,在高亮的代码里,一旦解构useContext
返回的值就会立马报错,因为默认值是undefined
,而undefined
不能被解构。
运行时错误让人讨厌,所以你一拍脑袋就赶紧加了个默认值。 不过呢,如果一个context里不是一个有用的值,它能有什么用?如果这个context只是一直使用默认值,那它不能发挥多大用途。 当我们想创建并使用context
时,百分之99的时候,我们希望让context消费者(那些调用useContext
的组件)能够渲染在一个能提供有用值的provider里。
有些时候默认值有用,不过绝大多数的时候不需要或没有太多帮助
React的文档里建议,给context提供一个默认值的用途是”可以在不包裹组件的情况下,进行组件的独立测试“。虽然可以如此做,但我并不觉得这样测试就比直接给组件包裹一层context来得好。记住,当每次你为了测试组件添加改动东西,而这些东西又并不出现在你的实际应用里,那么你就不能很肯定改测试是有效的。 当然context默认值自有用途,只是这上述用例并不是其中之一。
如果你使用ts,在使用React.useContext
时,不提供默认值就会很烦。 我等下给你们看看怎么避免这个问题,接着看吧。
自定义Provider组件
如果想让这个context模块有用,我们需要使用一个Provider,并用一个组件提供一个值,这个组件大概这样用:
function App() {
return (
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
)
}
ReactDOM.render(<App />, document.getElementById('⚛️'))
如上,我们来造这个具体的组件
import * as React from 'react'
const CountContext = React.createContext()
function countReducer(state, action) {
switch (action.type) {
case 'increment': {
return {count: state.count + 1}
}
case 'decrement': {
return {count: state.count - 1}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}) {
const [state, dispatch] = React.useReducer(countReducer, {count: 0})
// NOTE: you *might* need to memoize this value
// Learn more in http://kcd.im/optimize-context
const value = {state, dispatch}
return <CountContext.Provider value={value}>{children}</CountContext.Provider>
}
export {CountProvider}
这是个过度工程化的例子,目的是想展示一个更贴近现实的场景。 不过这并不意味着你每次都要搞得这么复杂 如果你的场景合适,
useState
也是可以的。而且,有些providers如上例子一样很简短,但有些将调用多个钩子的providers会非常复杂。
自定义Consumer Hook
绝大数我见到的context都是这么用的:
import * as React from 'react'
import {SomethingContext} from 'some-context-package'
function YourComponent() {
const something = React.useContext(SomethingContext)
}
我觉得这样子没能很好地为开发者提供一个良好的体验
相反,我觉得应该这样(译者注:我也觉得,感觉比较自然):
import * as React from 'react'
import {useSomething} from 'some-context-package'
function YourComponent() {
const something = useSomething()
}
这样定义的好处有几点,我等下在实现上说明:
import * as React from 'react'
const CountContext = React.createContext()
function countReducer(state, action) {
switch (action.type) {
case 'increment': {
return {count: state.count + 1}
}
case 'decrement': {
return {count: state.count - 1}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}) {
const [state, dispatch] = React.useReducer(countReducer, {count: 0})
// NOTE: you *might* need to memoize this value
// Learn more in http://kcd.im/optimize-context
const value = {state, dispatch}
return <CountContext.Provider value={value}>{children}</CountContext.Provider>
}
function useCount() {
const context = React.useContext(CountContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
export {CountProvider, useCount}
首先useCount
这个自定义hook使用了 React.useContext
来获取最近的CountProvider
提供的值。不过,如果没有值,那就丢出一个有用的错误信息,提示这个渲染在CountProvider
里的功能是组件里的hook并没有被调用。这很明显是个错误,提供一个错误信息很有帮助。安全地处理错误。
自定义Consumer组件
如果你可以使用hook,那你可以跳过这一节。不过如果你需要支持React<16.8.0,或者你觉得Context应该被一个类组件使用,那可以参考下面这个调用渲染prop的API(render-prop based API)的例子。
function CountConsumer({children}) {
return (
<CountContext.Consumer>
{context => {
if (context === undefined) {
throw new Error('CountConsumer must be used within a CountProvider')
}
return children(context)
}}
</CountContext.Consumer>
)
}
类组件可以这么用:
class CounterThing extends React.Component {
render() {
return (
<CountConsumer>
{({state, dispatch}) => (
<div>
<div>{state.count}</div>
<button onClick={() => dispatch({type: 'decrement'})}>
Decrement
</button>
<button onClick={() => dispatch({type: 'increment'})}>
Increment
</button>
</div>
)}
</CountConsumer>
)
}
}
这是我在hooks出来前的用法,如果你可以使用hooks了,那我推荐你就不用对上述内容太上心。Hooks更美好。
TypeScript
我答应过会说如何避免因没提供defaultValue
而导致的TS报错。如果照我建议的做,这个问题自然而然就不存在。如下:
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'}
type Dispatch = (action: Action) => void
type State = {count: number}
type CountProviderProps = {children: React.ReactNode}
const CountStateContext = React.createContext<
{state: State; dispatch: Dispatch} | undefined
>(undefined)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {count: state.count + 1}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
const [state, dispatch] = React.useReducer(countReducer, {count: 0})
// NOTE: you *might* need to memoize this value
// Learn more in http://kcd.im/optimize-context
const value = {state, dispatch}
return (
<CountStateContext.Provider value={value}>
{children}
</CountStateContext.Provider>
)
}
function useCount() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
export {CountProvider, useCount}
这样一来,大家都可以使用useCount
也无需在做undefined
效验,因为我们已经处理好了。
那如果dispatch type
手抖打错怎么办?
现在redux的狂热粉应该会吼你一脸,“我的action creators去哪了?!” 如果你想添加action creators我也不会拦着你,不过我从来就不喜欢action creators。我总感觉他们是一层多余的抽象,而且如果你使用TS并好好给你的action添加type的话,你应该不需要action creators。你还能有自动补全和代码行里的type报错。
我很喜欢这样传递dispatch
,还有个优势就是这样的dispatch
在创建他的组件里很稳定,并不需要额外把他们放进useEffect的依赖数组里(因为放不放都没差,不会变)。
如果你没有给你的JavaScript添加typing(也许你该考虑下),那我们丢出缺失的action types就可以早点发现问题。继续看下面的内容, 下面这节也能帮到你:
那异步actions咋办呢?
好问题,如果你需要做异步请求,并且在这个请求里,你需要dispatch很多东西,该怎么办呢? 诚然, 你可以直接在调用的组件里处理,但每个组件你都要手动把这些步骤组合起来,那一定很烦。
我建议就在context模块里做个helper函数,这个函数接受一个dispatch
和所有的数据,然后让这个helper专门来处理异步请求的逻辑,参考我的进阶React模式workshop::
async function updateUser(dispatch, user, updates) {
dispatch({type: 'start update', updates})
try {
const updatedUser = await userClient.updateUser(user, updates)
dispatch({type: 'finish update', updatedUser})
} catch (error) {
dispatch({type: 'fail update', error})
}
}
export {UserProvider, useUser, updateUser}
然后这样用:
import {useUser, updateUser} from './user-context'
function UserSettings() {
const [{user, status, error}, userDispatch] = useUser()
function handleSubmit(event) {
event.preventDefault()
updateUser(userDispatch, user, formState)
}
// more code...
}
我很满意这个模式,如果你想让我教你的公司如何使用这个模式 让我晓得 (或者 添加自己到愿望表 以便知悉我下次举办的workshop)!
结语
这就是最后版本的代码
import * as React from 'react'
const CountContext = React.createContext()
function countReducer(state, action) {
switch (action.type) {
case 'increment': {
return {count: state.count + 1}
}
case 'decrement': {
return {count: state.count - 1}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}) {
const [state, dispatch] = React.useReducer(countReducer, {count: 0})
// NOTE: you *might* need to memoize this value
// Learn more in http://kcd.im/optimize-context
const value = {state, dispatch}
return <CountContext.Provider value={value}>{children}</CountContext.Provider>
}
function useCount() {
const context = React.useContext(CountContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
export {CountProvider, useCount}
注意我没有导出CountContext
,这是故意的。我只想有唯一一种提供context值和唯一一种消费它的方式。这样确保了使用context值的人正确地使用它,也确保了我能为消费者们提供实用的工具。
我希望这点对你有帮助,记住:
- 不要试图用context来解决所有状态共享的问题
- context也不需要非得在整个应用里的全局空间,它可以使用在局部的组件树里
- 在你的应用里,可以(也许更应该)有多个逻辑分明的context
Good luck!