We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API 文档地址:https://zh-hans.reactjs.org/docs/context.html#reactcreatecontext
从 API 名字就可以看出, createContext 能够创建一个 React 的 上下文(context),然后订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。
基本的使用方法:
const MyContext = React.createContext(defaultValue)
其中 defaultValue 是传入的默认值。
如果要使用创建的上下文,需要通过 Context.Provider 最外层包装组件,并且需要显示的通过 <MyContext.Provider value={{xx:xx}}> 的方式传入 value,指定 context 要对外暴露的信息。
子组件在匹配过程中只会匹配最新的 Provider,也就是说如果有下面三个组件:ContextA.Provider->A->ContexB.Provider->B->C
如果 ContextA 和 ContextB 提供了相同的方法,则 C 组件只会选择 ContextB 提供的方法。
为什么要默认值?
如果匹配不到最新的 Provider 则会使用默认值,默认值一般只有在对组件进行单元测试(组件并未嵌入到父组件中)的时候,比较有用。
useContext 文档地址:https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext
通过 React.createContext 创建出来的上下文,在子组件中可以通过 useContext 这个 Hook 获取 Provider 提供的内容
const {funcName} = useContext(MyContext);
从上面代码可以发现,useContext 需要将 MyContext 这个 Context 实例传入,不是字符串,就是实例本身。
这种用法会存在一个比较尴尬的地方,父子组件不在一个目录中,如何共享 MyContext 这个 Context 实例呢?
一般这种情况下,我会通过 Context Manager 统一管理上下文的实例,然后通过 export 将实例导出,在子组件中在将实例 import 进来。
举个实际的例子:子组件中修改父组件的 state
一般的做法是将父组件的方法比如 setXXX 通过 props 的方式传给子组件,而一旦子组件多层级的话,就要层层透传。
使用 Context 的方式则可以免去这种层层透传
创建一个上下文管理的组件,用来统一导出 Context 实例
import React from 'react'; export const MyContext = React.createContext(null);
下面代码中,父组件引入了实例,并且通过 MyContext.Provider 将父组件包装,并且通过 Provider.value 将方法提供出去。
下面的实例提供了三个 state 操作方法:
以及一个副作用方法:
子组件 Child 接受的 props 只有三个 state 的值 step/number/count。
import React, { useState } from 'react'; import Child from './Child'; import { MyContext } from './context-manager'; const fetchData = () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }) }); } export default (props = {}) => { const [step, setStep] = useState(0); const [count, setCount] = useState(0); const [number, setNumber] = useState(0); return ( <MyContext.Provider value={{ setStep, setCount, setNumber, fetchData }}> <Child step={step} number={number} count={count} /> </MyContext.Provider> ); }
下面是子组件,相同的,也需要从 context-manager 中引入 MyContext 这个实例,然后才能通过 const { setStep, setNumber, setCount, fetchData } = useContext(MyContext); 解析出上下文中的方法,在子组件中则可以直接使用这些方法,修改父组件的 state。
import React, { useContext, useEffect, memo } from 'react'; import { MyContext } from './context-manager'; export default memo((props = {}) => { const { setStep, setNumber, setCount, fetchData } = useContext(MyContext); useEffect(() => { fetchData().then((res) => { console.log(`FETCH DATA: ${res}`); }) }, []); return ( <div> <p>step is : {props.step}</p> <p>number is : {props.number}</p> <p>count is : {props.count}</p> <hr /> <div> <button onClick={() => { setStep(props.step + 1) }}>step ++</button> <button onClick={() => { setNumber(props.number + 1) }}>number ++</button> <button onClick={() => { setCount(props.step + props.number) }}>number + step</button> </div> </div> ); });
4、效果:
可以发现,在子组件中点击按钮,直接调用 Context 透传过来的方法,可以修改父组件的 state,子组件则会重新渲染。
这种方式显式的避免了多级 props 的层层透传问题,虽然 Demo 只有一级 子组件,即使存在多级子组件也可以直接修改
上面子组件 Child 中,使用了 memo 包装子组件,这样做的原因是:
如果我们的 Context 会经常变化,那对子组件来讲会多次触发 re-render,通过 React.memo() 可以很好的管理子组件的性能问题
上面的示例虽然实现了多级组件方法共享,但是暴露出一个问题:所有的方法都放在了 Context.Provider.value 属性中传递,必然造成整个 Context Provider 提供的方法越来越多,也会臃肿。
<MyContext.Provider value={{ setStep, setCount, setNumber, fetchData }} /> 里面的方法可能越来越多
而向 setStep、setCount、setNumber 这三个方法,是可以通过 useReducer 包装,并且通过 dispatch 触发的,因此修改一下父组件:
下面的父组件与之前不同地方只是去掉了 setXXX 这些设置 state 的方法,并且在 Provider value 中,只传入了 value={{dispatch}}
import React, { useReducer } from 'react'; import Child from './Child'; import { MyContext } from './context-manager'; const initState = { count: 0, step: 0, number: 0 }; const reducer = (state, action) => { switch (action.type) { case 'stepInc': return Object.assign({}, state, { step: state.step + 1 }); case 'numberInc': return Object.assign({}, state, { number: state.number + 1 }); case 'count': return Object.assign({}, state, { count: state.step + state.number }); default: return state; } } export default (props = {}) => { const [state, dispatch] = useReducer(reducer, initState); const { step, number, count } = state; return ( <MyContext.Provider value={{ dispatch }}> <Child step={step} number={number} count={count} /> </MyContext.Provider> ); }
因此此时子组件只需要拿到 dispatch 即可修改父组件的 state:
子组件唯一的不同就是点击事件执行的是 dispatch
import React, { useContext, memo } from 'react'; import { MyContext } from './context-manager'; export default memo((props = {}) => { const { dispatch } = useContext(MyContext); return ( <div> <p>step is : {props.step}</p> <p>number is : {props.number}</p> <p>count is : {props.count}</p> <hr /> <div> <button onClick={() => { dispatch({ type: 'stepInc' }) }}>step ++</button> <button onClick={() => { dispatch({ type: 'numberInc' }) }}>number ++</button> <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button> </div> </div> ); });
效果如下:
上面的所有示例中,子组件获取父组件的 state 还是通过 props ,多级子组件又会存在层层嵌套
如果将整个 state 通过 Context 传入就无需层层组件的 props 传递(如果不需要整个state,可以只将某几个 state 给 Provider)
父组件的变化只是将 state 也给了 Provider,然后去掉了 Child 组件的 props 透传
import React, { useReducer } from 'react'; import Child from './Child'; import { MyContext } from './context-manager'; const initState = { count: 0, step: 0, number: 0 }; const reducer = (state, action) => { switch (action.type) { case 'stepInc': return Object.assign({}, state, { step: state.step + 1 }); case 'numberInc': return Object.assign({}, state, { number: state.number + 1 }); case 'count': return Object.assign({}, state, { count: state.step + state.number }); default: return state; } } export default (props = {}) => { const [state, dispatch] = useReducer(reducer, initState); return ( <MyContext.Provider value={{ state, dispatch }}> <button onClick={() => { dispatch({ type: 'stepInc' }) }}>parent step ++</button> <Child /> </MyContext.Provider> ); }
下面代码中可以看出,原来的 props.number 都变成了 state.number
注意:在 return 内部我写了一个 console
import React, { useContext, memo } from 'react'; import { MyContext } from './context-manager'; export default memo((props = {}) => { const { state, dispatch } = useContext(MyContext); return ( <div> {console.log('[Child] RE-RENDER')} <p>step is : {state.step}</p> <p>number is : {state.number}</p> <p>count is : {state.count}</p> <hr /> <div> <button onClick={() => { dispatch({ type: 'stepInc' }) }}>step ++</button> <button onClick={() => { dispatch({ type: 'numberInc' }) }}>number ++</button> <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button> </div> </div> ); });
注意看上面的动图,在点击子组件的 【number + step】 按钮的时候,虽然 count 的值没有发生任何变化,但是一直触发 re-render,即使子组件是通过 React.memo 包装过的。
出现这个问题原因是 React.memo 只会对 props 进行浅比较,而通过 Context 我们直接将 state 注入到了组件内部,因此 state 的变化必然会触发 re-render,整个 state 变化是绕过了 memo。
既然 React.memo() 无法拦截注入到 Context 的 state 的变化,那就需要我们在组件内部进行更细粒度的性能优化,这个时候可以使用 useMemo()
下面是对子组件的改造,去掉了 React.memo,在 return 内部通过 useMemo() 包装,并且声明了所有依赖项:(包括:step/number/count/dispatch)
import React, { useContext, useMemo } from 'react'; import { MyContext } from './context-manager'; export default (props = {}) => { const { state, dispatch } = useContext(MyContext); return useMemo(() => { console.log('[Child] RE-RENDER'); return ( <div> <p>step is : {state.step}</p> <p>number is : {state.number}</p> <p>count is : {state.count}</p> <hr /> <div> <button onClick={() => { dispatch({ type: 'stepInc' }) }}>step ++</button> <button onClick={() => { dispatch({ type: 'numberInc' }) }}>number ++</button> <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button> </div> </div> ) }, [state.count, state.number, state.step, dispatch]); }
从上面效果可以发现,当 number+step=count 不变的时候,是不会触发 return 中 DOM 的重新渲染的
使用 React.createContext 和 useContext: https://github.com/postbird/react-hook-practice/tree/master/src/mods/Demo12
使用 reducer 优化 Context 复杂程度: https://github.com/postbird/react-hook-practice/tree/master/src/mods/Demo13
通过 Context 透传 state: https://github.com/postbird/react-hook-practice/tree/master/src/mods/Demo14
通过 useMemo 解决 state 透传导致的频繁 re-render 性能问题: https://github.com/postbird/react-hook-practice/tree/master/src/mods/Demo15
The text was updated successfully, but these errors were encountered:
No branches or pull requests
一、React.createContext
API 文档地址:https://zh-hans.reactjs.org/docs/context.html#reactcreatecontext
从 API 名字就可以看出, createContext 能够创建一个 React 的 上下文(context),然后订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。
基本的使用方法:
其中 defaultValue 是传入的默认值。
如果要使用创建的上下文,需要通过 Context.Provider 最外层包装组件,并且需要显示的通过 <MyContext.Provider value={{xx:xx}}> 的方式传入 value,指定 context 要对外暴露的信息。
子组件在匹配过程中只会匹配最新的 Provider,也就是说如果有下面三个组件:ContextA.Provider->A->ContexB.Provider->B->C
如果 ContextA 和 ContextB 提供了相同的方法,则 C 组件只会选择 ContextB 提供的方法。
为什么要默认值?
如果匹配不到最新的 Provider 则会使用默认值,默认值一般只有在对组件进行单元测试(组件并未嵌入到父组件中)的时候,比较有用。
二、使用 useContext 获取上下文
useContext 文档地址:https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext
通过 React.createContext 创建出来的上下文,在子组件中可以通过 useContext 这个 Hook 获取 Provider 提供的内容
从上面代码可以发现,useContext 需要将 MyContext 这个 Context 实例传入,不是字符串,就是实例本身。
这种用法会存在一个比较尴尬的地方,父子组件不在一个目录中,如何共享 MyContext 这个 Context 实例呢?
一般这种情况下,我会通过 Context Manager 统一管理上下文的实例,然后通过 export 将实例导出,在子组件中在将实例 import 进来。
三、createContext 和 useContext 结合使用实现方法共享
举个实际的例子:子组件中修改父组件的 state
一般的做法是将父组件的方法比如 setXXX 通过 props 的方式传给子组件,而一旦子组件多层级的话,就要层层透传。
使用 Context 的方式则可以免去这种层层透传
1、context-manager.js
创建一个上下文管理的组件,用来统一导出 Context 实例
2、父组件 Provider 提供上下文 value
下面代码中,父组件引入了实例,并且通过 MyContext.Provider 将父组件包装,并且通过 Provider.value 将方法提供出去。
下面的实例提供了三个 state 操作方法:
以及一个副作用方法:
子组件 Child 接受的 props 只有三个 state 的值 step/number/count。
3、子组件 useContext 解析上下文
下面是子组件,相同的,也需要从 context-manager 中引入 MyContext 这个实例,然后才能通过 const { setStep, setNumber, setCount, fetchData } = useContext(MyContext); 解析出上下文中的方法,在子组件中则可以直接使用这些方法,修改父组件的 state。
4、效果:

可以发现,在子组件中点击按钮,直接调用 Context 透传过来的方法,可以修改父组件的 state,子组件则会重新渲染。
5、为什么使用 React.memo()
上面子组件 Child 中,使用了 memo 包装子组件,这样做的原因是:
如果我们的 Context 会经常变化,那对子组件来讲会多次触发 re-render,通过 React.memo() 可以很好的管理子组件的性能问题
四、使用 useReducer 减少 Context 的复杂程度
上面的示例虽然实现了多级组件方法共享,但是暴露出一个问题:所有的方法都放在了 Context.Provider.value 属性中传递,必然造成整个 Context Provider 提供的方法越来越多,也会臃肿。
<MyContext.Provider value={{ setStep, setCount, setNumber, fetchData }} /> 里面的方法可能越来越多
而向 setStep、setCount、setNumber 这三个方法,是可以通过 useReducer 包装,并且通过 dispatch 触发的,因此修改一下父组件:
下面的父组件与之前不同地方只是去掉了 setXXX 这些设置 state 的方法,并且在 Provider value 中,只传入了 value={{dispatch}}
因此此时子组件只需要拿到 dispatch 即可修改父组件的 state:
子组件唯一的不同就是点击事件执行的是 dispatch
效果如下:

五、将 state 也通过 Context 传递给子组件
上面的所有示例中,子组件获取父组件的 state 还是通过 props ,多级子组件又会存在层层嵌套
如果将整个 state 通过 Context 传入就无需层层组件的 props 传递(如果不需要整个state,可以只将某几个 state 给 Provider)
1、父组件
父组件的变化只是将 state 也给了 Provider,然后去掉了 Child 组件的 props 透传
2、子组件直接使用父组件的 state
下面代码中可以看出,原来的 props.number 都变成了 state.number
注意:在 return 内部我写了一个 console
3、效果
4、直接使用父组件 state 带来的性能问题
注意看上面的动图,在点击子组件的 【number + step】 按钮的时候,虽然 count 的值没有发生任何变化,但是一直触发 re-render,即使子组件是通过 React.memo 包装过的。
出现这个问题原因是 React.memo 只会对 props 进行浅比较,而通过 Context 我们直接将 state 注入到了组件内部,因此 state 的变化必然会触发 re-render,整个 state 变化是绕过了 memo。
六、使用 useMemo() 解决 state Context 透传的性能问题
既然 React.memo() 无法拦截注入到 Context 的 state 的变化,那就需要我们在组件内部进行更细粒度的性能优化,这个时候可以使用 useMemo()
1、使用 useMemo 优化子组件渲染
下面是对子组件的改造,去掉了 React.memo,在 return 内部通过 useMemo() 包装,并且声明了所有依赖项:(包括:step/number/count/dispatch)
2、优化后效果:
从上面效果可以发现,当 number+step=count 不变的时候,是不会触发 return 中 DOM 的重新渲染的
七、代码
使用 React.createContext 和 useContext:
https://github.com/postbird/react-hook-practice/tree/master/src/mods/Demo12
使用 reducer 优化 Context 复杂程度:
https://github.com/postbird/react-hook-practice/tree/master/src/mods/Demo13
通过 Context 透传 state:
https://github.com/postbird/react-hook-practice/tree/master/src/mods/Demo14
通过 useMemo 解决 state 透传导致的频繁 re-render 性能问题:
https://github.com/postbird/react-hook-practice/tree/master/src/mods/Demo15
The text was updated successfully, but these errors were encountered: