Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
tsx
, 我平时更加喜欢typescript
+ react
, 体验感更好。createContext
方法, 它接收一个参数, 我们举一个简单的例子, 通过这个简单的例子来一点点掌握 context 的用法。count
这个 props。 但是我们只是实现了访问, 那我们如果要进行更新呢?count
表示数量。setCount
实现更新数量的函数。undefined
进行占位, 那我们的 App 组件也要进行相对应的修改。CounterContext.Provider
, 我希望App
组件只是将所有组件做一个组合, 而不是做状态管理, 想象一下你的App
组件有 n 个 Context(UIContext、AuthContext、ThemeContext 等等), 如果在这里进行状态管理那是多么痛苦的事情, 而App
组件也失去了组合的意义。CounterProvider
进行提供 counter, 看起来很棒。useContext
和一个CounterProvider
读取 counter 的值, 代码变得更加精简。Counter
组件中可以直接使用这个 hook 来读取 CounterContext 的值。createContext
中声明了有两个类型CounterContextType
和undefined
, 虽然我们在ContextProvider
的时候已经注入了 count
和 setCount
。 但是 ts 并不能保证我们一定会有值, 所以我们怎么办呢?useCounter
中做一层类型保护, 通过类型保护来缩小我们的类型, 从而知道是什么类型了。context === undefined
来实现缩小类型,其实它还有另外一个更加重要的作用: 检测当前使用useCounter hook
的组件是否被正确的使用。useCounter hook
的组件在没有包裹在 CounterProvider
组件树中 , 那么读取到的值其实就是createContext
时候的初始值(在这里的例子中也就是undefined
)。 undefined
再去解构赋值是无法成功的, 所以这个时候就可以通过这一判断来防止这个问题。CounterDisplay
: 负责展示。CounterAction
: 负责更新。count
就好了, 我不需要读取多余的 setCount
更新函数, 这对我来说是没有意义的, 而对于更新操作来说, 我可以通过setCount(v => v + 1)
的方式来读取之前的值, 不需要再访问count
。setCount
往往不会改变, 而更新方法setCount
控制的状态count
却会改变。 如果你创建一个同时提供状态值和更新方法时, 那么所有订阅了该上下文的组件都会进行 rerender, 即使它们只是访问操作(可能还没有执行更新)。CounterAction
不会因为状态值count
的变化而进行没有必要的渲染。createCtx
创建 Context。CounterAction
组件。CounterAction
组件就变成了:createCtx
来实现创建一个createReducerCtx
。CounterAction
是可以避免 rerender 的, 我们通过避免读取count
这个 Context value 来达到目的, 而更新函数其实一直都是不变的, 这样子即使 Context value 改变也与我无关.const CounterContext = React.createContext<number>(0);const App = ({ children }) => {return (<CounterContext.Provider value={0}><Counter /></CounterContext.Provider>);};const Counter = () => {const count = useContext(CounterContext);return <h1> {count} </h1>;};
export type CounterContextType = {count: number;setCount: Dispatch<SetStateAction<number>>;};const CounterContext = React.createContext<CounterContextType | undefined>(undefined);
const App = ({ children }) => {const [count, setCount] = useState(0);return (<CounterContext.Provider value={{ count, setCount }}><Counter /></CounterContext.Provider>);};const Counter = () => {const { count, setCount } = useContext(CounterContext);return (<div><button onClick={() => setCount(count + 1)}>+1</button><button onClick={() => setCount(count - 1)}>-1</button></div>);};
const App = ({ children }) => {const [ui, setUI] = useState();const [auth, setAuth] = useState();const [theme, setTheme] = useState();return (<UIContext.Provider value={{ ui, setUI }}><AuthContext.Provider value={{ auth, setAuth }}><ThemeProvider.Provider value={{ theme, setTheme }}>{children}</ThemeProvider.Provider></AuthContext.Provider></UIContext.Provider>);};
export type Children = { children?: React.ReactNode };export const CounterProvider = ({ children }: Children) => {const [count, setCount] = useState(0);return <CounterContext.Provider value={{ count, setCount }}>{children}</CounterContext.Provider>;};
const App = () => {return (<CounterProvider><Counter /></CounterProvider>);};
export const useCounter = () => {const context = useContext(CounterProvider);return context;};
const { count, setCount } = useCounter();// Property 'count' does not exist on type 'CounterContextType | undefined'// Property 'setCount' does not exist on type 'CounterContextType | undefined'
export const useCounter = () => {const context = useContext(CounterContext);if (context === undefined) {throw new Error('useCounter must in CounterContext.Provider');}return context;};
export type Children = { children?: React.ReactNode };export type CounterContextType = {count: number;setCount: Dispatch<SetStateAction<number>>;};const CounterContext = React.createContext<CounterContextType['count'] | undefined>(undefined);const CounterDispatchContext = React.createContext<CounterContextType['setCount'] | undefined>(undefined,);export const CountProvider = ({ children }: Children) => {const [count, setCount] = useState(0);return (<CounterDispatchContext.Provider value={setCount}><CounterContext.Provider value={count}>{children}</CounterContext.Provider>;</CounterDispatchContext.Provider>);};export default CounterContext;
export const useCountDispatch = () => {const context = useContext(CounterDispatchContext);if (context === undefined) {throw new Error('useCountDispatch must be in CounterDispatchContext.Provider');}return context;};export const useCount = () => {const context = useContext(CounterContext);if (context === undefined) {throw new Error('useCount must be in CounterContext.Provider');}return context;};
// CounterDisplay.tsxconst CounterDisplay = () => {const count = useCount();return <h1>{count}</h1>;};// CounterAction.tsxconst CounterAction = () => {const setCount = useCountDispatch();const increment = () => setCount(c => c + 1);const decrement = () => setCount(c => c - 1);// 只会执行一遍useEffect(() => {console.log('CounterAction render');});return (<div><button onClick={increment}>+1</button><button onClick={decrement}>-1</button></div>);};
function createCtx<T>(initialValue: T) {const storeContext = createContext<T | undefined>(undefined);const dispatchContext = createContext<Dispatch<SetStateAction<T>> | undefined>(undefined);const useStore = () => {const context = useContext(storeContext);if (context === undefined) {throw new Error('useStore');}return context;};const useDispatch = () => {const context = useContext(dispatchContext);if (context === undefined) {throw new Error('useDispatch');}return context;};const ContextProvider = ({ children }: PropsWithChildren<{}>) => {const [state, dispatch] = useState(initialValue);return (<dispatchContext.Provider value={dispatch}><storeContext.Provider value={state}>{children}</storeContext.Provider></dispatchContext.Provider>);};return [ContextProvider, useStore, useDispatch] as const;}export default createCtx;
import createCtx from './createCtx';const [CountProvider, useCount, useCountDispatch] = createCtx(0);export { CountProvider, useCount, useCountDispatch };
// theme.tsconst [ThemeProvider, useTheme, useThemeDispatch] = createCtx({ theme: 'dark' });// ui.tsconst [UIProvider, useUI, useUIDispatch] = createCtx({ layout: '' });// app.tsxconst App = ({ children }) => {return (<ThemeProvider><UIProvider>{children}</UIProvider></ThemeProvider>);};
const setCount = useCountDispatch();const increment = () => setCount(c => c + 1);const decrement = () => setCount(c => c - 1);
const useIncrement = () => {const setCount = useCountDispatch();return () => setCount(c => c + 1);};const useDecrement = () => {const setCount = useCountDispatch();return () => setCount(c => c - 1);};
const CounterAction = () => {const increment = useIncrement();const decrement = useDecrement();return (<div><button onClick={increment}>+1</button><button onClick={decrement}>-1</button></div>);};
const useAsyncIncrement = () => {// 复用上面的hookconst increment = useIncrement();return () =>new Promise(resolve =>setTimeout(() => {increment();resolve(true);}, 1000),);};
const CounterAction = () => {// ...const asyncIncrement = useAsyncIncrement();return (<div>{/* ... */}<button onClick={asyncIncrement}> async + 1 </button></div>);};
dispatch({ type: 'increment' });setCount(v => v + 1);
function createReducerCtx<StateType, ActionType>(reducer: Reducer<StateType, ActionType>,initialValue: StateType) {const stateContext = createContext<StateType | undefined>(undefined);const dispatchContext = createContext<Dispatch<ActionType> | undefined>(undefined);const useStore = () => {// ...};const useDispatch = () => {// ...};const ContextProvider = ({ children }: PropsWithChildren<{}>) => {const [store, dispatch] = useReducer(reducer, initialValue);return (// ...);};return [ContextProvider, useStore, useDispatch] as const;}
type CounterActionTypes = { type: 'increment' } | { type: 'decrement' };function reducer(state = 0, action: CounterActionTypes) {switch (action.type) {case 'increment':return state + 1;case 'decrement':return state - 1;default:return state;}}const [ReducerCounterProvider, useReducerCount, useReducerCountDispatch] = createReducerCtx(reducer,0,);export const useIncrement = () => {const setCount = useReducerCountDispatch();return () => setCount({ type: 'increment' });};export const useDecrement = () => {const setCount = useReducerCountDispatch();return () => setCount({ type: 'decrement' });};export const useAsyncIncrement = () => {const increment = useIncrement();return () =>new Promise(resolve =>setTimeout(() => {increment();resolve(true);}, 1000),);};