自React 16.3 以来,React为我们提供了Context
。Context
本身与状态无关,但它是一个不依赖属性进行跨组件传值的机制。通过把Context与组件状态组合在一起,我们就可以构造全局属性了。
此外,React 16.8 还提供了useContext
钩子。通过使用useContext
和useState
(其实useReducder
)也行,我们就自定义用于全局状态的钩子了。
Context
并不完全是为了全局状态而设计的。Context
的一个众所周知的缺陷在于,所有Context
的消费者会在其状态更新时,进行重新渲染。而这其中可能会有不必要的重新渲染。所以,人们更推荐把全局状态进行拆分。
在这一章节,我们会结合具体的例子讲述Context
的常见用法。我们还好讨论结合TypScript使用Context
的技巧。这么做的目的是,你可以更加自信地用Context
处理全局状态。
在这一章,我们会讨论下面这些主题
- 探索
useState
和useContext
- 理解
Context
- 创建一个存储全局对象的
Context
Context
的最佳实践
探索useState
和useContext
通过组合useState
和useContext
,我们可以创建一个全局状态。让我们回顾一下如何在不使用 useContext
的情况下使用 useState
,useContext
对于静态值是如何起作用的,以及我们如何将 useState
和 useContext
结合起来使用。
使用useState
而不使用useContext
在深入useContext
之前,我们先回顾一下如何使用useState
。
下面是代码:
js
const App = () => {
const [count, setCount] = useState(0);
return <Parent count={count} setCount={setCount} />;
};
这是很基础的一个用法。
然后,我们再创建一个Parent
组件。Parent
组件把两个属性传递给Component1
和Component2
,如下:
js
const Parent = ({ count, setCount }) => {
<>
<Component1 count={count} setCount={setCount} />
<Component2 count={count} setCount={setCount} />
</>
}
这种从父组件向子组件传递属性的操作是一项重复性的任务,通常被称为 "属性钻取"。
下面是Component1
组件和Component2
组件的代码:
js
const Component1 = ({ count, setCount }) => (
<div>
{count}
<button onClick={() => setCount((c) => c + 1)}>
+1
</button>
</div>
);
const Component2 = ({ count, setCount }) => (
<div>
{count}
<button onClick={() => setCount((c) => c + 2)}>
+2
</button>
</div>
);
这两个组件,是纯组件。这意味着,它们展示的内容,仅仅依赖于属性。当然,Component1
和Component2
还是有细微不同的。
这个例子没啥问题。在这种情况下,父组件不一定需要了解 count
状态,并且在父组件中隐藏 count
状态的存在可能是有意义的。
结合一个静态值使用useContext
React允许在使用组件时,不使用属性。
下面这个例子,会展示如何使用一个静态值来使用Context。它对于不同的值,有不同的providers,而消费者组件会在最近的providers获取对应的值。
首先,我们可以用createContext
来定义一个颜色:
js
const ColorContext = createContext('black');
在这个例子中,我们有一个黑色的默认颜色。这个默认值可以用于任何providers。
接下来,我们要定义一个消费者组件。它会读取Context的颜色,并展示这个颜色:
js
const Component = () => {
const color = useContext(ColorContext);
return <div style={{ color }}>Hello {color}</div>;
}
Component
会读取Context的color
。但此时,我们并不知道color
为何,它是依赖于Context的。
之后,我们可以定义一个App
组件。值这个组件中,我们可以定义多个provider,为其设置不同的颜色,代码如下:
js
const App = () => (
<>
<Component />
<ColorContext.Provider value="red">
<Component />
</ColorContext.Provider>
<ColorContext.Provider value="green">
<Component />
</ColorContext.Provider>
<ColorContext.Provider value="blue">
<Component />
<ColorContext.Provider value="skyblue">
<Component />
</ColorContext.Provider>
</ColorContext.Provider>
</>
);
第一个Component
会展示 black
,因为它没有被任何provider包裹。第二个和第三个,分别展示red
和green
。第三个会展示 blue
。最后一个会展示skyblue
,因为包裹它最近的provider的值是skyblue
。
多个提供者(provider)以及重用消费者(consumer)组件是 React 上下文(Context)的一项重要功能。如果对于你的应用场景来说,这项功能并不重要,那么你可能就不需要使用 React 上下文。我们将在第四章《通过订阅共享模块状态》中讨论不使用上下文的订阅方法
结合useContext
使用useState
现在,我们看看useContext
和useState
可以如何组合。我们可以传递状态和Context的更新函数,这样就不用传递props了。
下面这个例子,展示了一个用useState
和useContext
实现的简单的 count
状态。我们定义了一个count
状态,及其更新函数setCount
函数。而Parent
组件并不传递组件,Component1
和Component2
组件通过 useContext
来获取状态。
首先,我们为 count 状态创建了一个Context。这个Context里的count
有一个默认的静态值,和一个空的setCount
回退函数。代码是这样的:
js
const CountStateContext = createContext({
count: 0,
setCount: () => {},
})
这个默认值可以帮助TS来推断类型。然而,在大多数情况下,我们更需要一个状态,而非一个静态值,因为默认值其实没啥用。之后,我们会讨论使用Context的最佳实践。
而App
组件有count状态及其setter函数,并把它们传递给了Context provider 组件,如下面的代码所示:
js
const App = () => {
const [count, setCount] = useState(0);
return (
<CountStateContext.Provier
value={ { count, setCount } }
>
<Parent />
</CountStateContext.Provier>
)
}
传递给 CountStateContext.Provier 的值是一个 成员 为 count
和 setCount
的对象。这个对象的结构和默认值的结构是一样的。
我们定义了一个Parent
组件。不像上一部分的例子,我们不用再传递属性了。代码如下所示:
js
const Parent = () => {
<>
<Component1 />
<Component2 />
</>
}
虽然Parent
组件被包裹在了 Context provider里面,Parent
组件并不知道count
状态的存在。而Parent
组件内的组件,依然可以通过Context使用count
。
最后,我们要定义Component1
组件 和 Componen2
组件。它们不再通过属性来获取count
和 setCount
,而是通过Contetxt来获取:
js
const Component1 = () => {
const { count, setCount } = useContext(CountStateContext);
return (
<div>
{count}
<button onClicak={() => setCount((c) => c + 1 )}>
+1
</button>
</div>
)
}
const Component2 = () => {
const { count, setCount } = useContext(CountStateContext);
return (
<div>
{count}
<button onClicak={() => setCount((c) => c + 2 )}>
+2
</button>
</div>
)
}
那么,这些组件通过Context得到了什么值呢?它们通过离自己最近的provider来获取值。我们可以生成多个providers,这样就会有多个各自独立的count
状态了。而生成多个独立的Context,是React Context一项重要的能力。
在这一部分,我们学习了如何使用React Context来创建一个全局状态。接下来,我们要深入理解React Context的运行。
理解Context
当一个上下文提供者(Context provider)拥有一个新的上下文值时,所有的上下文消费者(Context consumers)都会接收到这个新值并重新渲染。这意味着提供者中的值会被传播到所有的消费者那里。理解上下文的传播机制及其局限性对我们来说是很重要的。
Context的传播是如何运作的。
当一个Context的provider有了新的Context值时,这个Context的所有消费者会接收这个新值,并进行重新渲染。这意味着,Context的值的变化,会传导到其所有的消费者。
有时,一个子组件会因为两个原因重新渲染 -一个是因为其父组件重新渲染,另一个是因为Context的值发生了变化。
为了阻止不是因为Context值变化而引起的重新渲染,我们可以使用内容提升的技巧,或者使用memo。memo用于包裹一个组件,并过滤掉属性没有变化时的重新渲染。
让我们看一个使用memo的例子。
js
const ColorContext = createContext('black');
'black'
是一个默认值,会在组件树上没有Context provider时使用。
之后,我们定义ColorComponent
组件,同时使用renderCount
来统计组件被重新渲染的次数,如下所示:
js
const ColorComponent = () => {
const color = useContext(ColorContext);
const renderCount = useRef(1);
useEffect(() => {
renderCount.current +=1;
})
return (
<div style={{ color }}>
Hellor {color} (renders: {renderCount.current})
</div>
)
}
我们用useRef
来定义 renderCount
。 renderCount.current
代表的就是渲染次数。每当useEffect
被触发时,renderCount.current
就会递增。
接下来,便是MemoedColorComponent
组件
js
const MemoedColorComponent = memo(ColorComponent);
memo
函数会返回一个基于基准组件的,被记忆化的组件。被记忆化的组件会在属性不变时不再渲染。
之后,我们在生成一个不使用useContext
的DummyComponent
组件:
js
const DummyComponent = () => {
const renderCount = useRef(1);
useEffect(() => {
renderCount.current +=1;
})
return (
<div style={{ color }}>
Dummy (renders: {renderCount.current})
</div>
)
}
这个组件用于比较ColorComponent
组件的行为。
同理,我们再生成一个MemoedDummyComponent
组件:
js
const MemoedDummyComponent = memo(DummyComponent);
接下来,我们定义一个Parent
组件,用于展示刚刚定义的四个组件:
js
const Parent = () => {
<ul>
<li><DummyComponent></li>
<li><MemoedDummyComponent></li>
<li><ColorComponent></li>
<li><MemoedColorComponent></li>
</ul>
}
最后,我们写一个App
组件。在这个App
组件定义color
和 setColor
。之后,我们可以通过一个input 来设置颜色:
js
const App = () => {
const [color, setColor] = useState('red');
return (
<ColorContext.Provider value={color}>
<input
value={color}
onChange={ (e) => setColor(e.target.value) }
/>
<Parent />
</ColorContext.Provider>
)
}
这个组件会这样运行:
- 首先,渲染所有的组件。
- 如果你在input里输入了新的值,
App
组件会重新渲染,因为useState
被调用了。 - 3之后,
ColorContext.Provider
的到来一新值,与此同时,Parent
组件也重新渲染了。 DummyComponent
渲染了,但是MemoedDummyComponent
并没有ColorComponent
会因为两个原因重新渲染:其父组件重新渲染了,Context的值变动了MemoedColorComponent
重新渲染了,因为Context变动了
在Context的值为对象时的局限
如果Context值是原始类型,当然是简单的。但如果其值为引用类型,就有很多要注意的了。一个对象也许有多个值,而这个Context的消费者用不到这么多值。
在下面的例子中,我们以一个值为对象的Context展开。
首先,我们定义一个值为 count1
和 count2
的对象,再把它传递给 Context:
js
const CountContext = createContext({ count1: 0, count2: 0 });
为了使用这个CountContext
,我们可以定义一个展示count1
的Counter1
。我们可以用 renderCount
来展示渲染次数。我们还可以定义一个MemoedCounter1
组件。代码如下:
js
const Couner1 = () => {
const { count1 } = useContext(CountContext);
const renderCount = useRef(1);
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
Count1: {count1} (renders: {renderCount.current})
</div>
)
}
const MemoedCounter1 = memo(Counter1);
注意,Counter1
组件会使用Context的 count1
。
同理,我们可以定义一个展示 count2
的 Counter2
组件,并定义一个MemoedCounter2
组件。
js
const Couner2 = () => {
const { count2 } = useContext(CountContext);
const renderCount = useRef(1);
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
Count2: {count2} (renders: {renderCount.current})
</div>
)
}
const MemoedCounter2 = memo(Couner2);
而Parent
组件以两个被缓存的组件作为子组件:
js
const Parent = () => {
<>
<MemoedCounter1 />
<MemoedCounter2 />
</>
}
最后,我们要定义一个App
组件。在App
组件,我们定义了两个count及其setter函数,并把两个count注入到Context中:
js
const App = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<CountContext.Provider value={{ count1, count2 }}>
<button onClick={() => setCount1((c) => c + 1)}>
{count1}
</button>
<button onClick={() => setCount2((c) => c + 1)}>
{count2}
</button>
<Parent />
</CountContext.Provider>
);
};
注意,这两个按钮的位置很重要!
这两个count,count1
和 count2
是 各自的独立的。Counter1
组件只消费了count1
;Counter2
组件只消费了count2
.因此,理想上来说,Counter1
应该只在count1
变化时才重新渲染。反之,则意味着产生了不必要的重新渲染。在这个例子中,Counter1
会在count2
变化时重新渲染。
这种不必要的重新渲染,在使用React Context时还是要注意的。
在这一部分,我们学习了React Context的局限性,尤其是其值为引用类型时。接下来,我们要学习用React Context实现全局状态的典型实践。
用Context创建全局状态
基于React Context的行为,我们会讨论用Context 创建全局状态的两种方案:
- 创建小块状态
- 创建一个使用 useReducer 的状态并通过多个 Context 进行传播
让我们看看每个方案。
创建小块状态
第一个方案是把全局状态进行细分。所以,我们不再把全局状态放在一个对象里,而是为每一个状态创建一个Context。
下面这个例子,创建了两个count
状态,并为每一个状态创建了其对应的Context provider。
首先,我们创建了两个Context,Count1Context
和Count2Context
:
js
type CountContextType = {
number,
Dispatch<SetStateAction<number>>
}
const Count1Context = createContext<CountContextType>([
0,
() => {}
])
const Count2Context = createContext<CountContextType>([
0,
() => {}
])
Context的值是一个由 count
和其更新函数组成的元组。
之后,我们定义Counter1
:
js
const Counter1 = () => {
const [count1, setCount1] = useContext(Count1Context);
return (
<div>
Count1: {count1}
<button onClick={() => setCount1((c) => c + 1)}>
+1
</buuton>
</div>
)
}
注意,Counter1
只依赖Count1Context
,它没有使用其他上下文。
Counter2
的实现是类似的:
js
const Counter2 = () => {
const [count2, setCount2] = useContext(Count2Context);
return (
<div>
Count1: {count1}
<button onClick={() => setCount2((c) => c + 1)}>
+1
</buuton>
</div>
)
}
之后,实现Parent
组件:
js
const Parent = () => {
<div>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</div>
}
之后,我们定义Count1Context
的provider Count1Provider
:
js
const Count1Provider =({
children
}: {
children: ReactNode
}) => {
const [count1, setCount1] = useState(0);
return (
<Count1Contxt.Provider value ={[count1, setCount1]}>
{children}
</Count1Contxt.Provider>
)
}
同理,我们定义Count2Provider
:
js
const Count2Provider =({
children
}: {
children: ReactNode
}) => {
const [count2, setCount2] = useState(0);
return (
<Count2Contxt.Provider value ={[count2, setCount2]}>
{children}
</Count2Contxt.Provider>
)
}
Count1Provider
和 Count2Provider
类似,其不同在于传递的值。
最后,Parent
组件,被这两个Provider所包裹:
js
const App = () => {
<Count1Provider>
<Count2Provider>
<Parent />
</Count2Provider>
</Count1Provider>
}
之后,这个App
组件就会被嵌套在这两个provider组件里面。如果有更多的provider,就会造成更深的嵌套。
这个应用并不会遇到上文说的额外重新渲染。这是因为每个Context只接收原始值。所以Counter1
和Counter2
之后在其对应的count变化时,进行重新渲染。为每一个状态创建provider是有必要的;否则,就要把多个状态整合在一个状态里,就会引起很多重新渲染。
如果你确定一个对象只会被修改一次,那为它单独创建一个Context也是可以的。比如这种:
js
const [user, setUser] = useState({
firstName: 'react',
lastName: 'hooks'
})
这种情况下,就没必要为每一个name各建立一个Context了。
接下来,我们看看另一个方法。
用useReducer建立状态,并使用多个Context传播状态
第二种解决方案是创建一个单一的状态,并使用多个 Context 来分发状态的不同部分。在这种情况下,分发更新状态的函数应该使用一个单独的 Context。
以下示例基于 useReducer,它有三个 Context:两个用于状态的不同部分,最后一个用于 dispatch 函数。
首先,我们为两个counts变量创建Context,再创建一个Context用于分发对应的操作:
js
type Action = { type: "INC1" } | { type: "INC2" };
const Count1Context = createContext<number>(0);
const Count2Context = createContext<number>(0);
const DispatchContext = createContext<Dispatch<Action>>(
() => {}
)
在这个例子中,我们有两个count变量,为其各自创建了一个Context。但是,它们的都操作行为都由同一个Context进行分发。
为了更好的分发操作,我们需要定义一个reducer
。
接下来,我们要定义一个Counter1
组件。这个组件使用了两个Contexts,一个Context用来存值,另一个用来分发函数:
js
const Counter1 = () => {
const count1 = useContext(Count1Context);
const dispatch = useContext(DispatchContext);
return (
<div>
Count1: {count1}
<button onClick={() => dispatch({ type: "INC1"})}>
+1
</button>
</div>
)
}
同理,我们可以定义一个Counter2
组件:
js
const Counter2 = () => {
const count2 = useContext(Count2Context);
const dispatch = useContext(DispatchContext);
return (
<div>
Count2: {count2}
<button onClick={() => dispatch({ type: "INC2"})}>
+1
</button>
</div>
)
}
Counter1
组件 和 Counter2
组都使用了 DispatchContext
。
而Parent
组件如下:
js
const Parent = () => {
<div>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</div>
}
现在,我们为其定义一个唯一的Provider
组件。这个Provider
组件会使用useReducer
。这个reducer会处理两种操作INC1
和 INC2
。这个Provider
组件还使用了之前定义的三个provider:
js
const Provider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(
(
prev: { count1: number, coount2: number },
action: Action
) => {
if (action.type === "INC1") {
return { ...prev, count1: prev.count1 +1 }
}
if (action.type === "INC2") {
return { ...prev, count2: prev.count2 +1 }
}
throw new Error("no matching action")
},
{
count1: 0,
count2: 0
}
)
return (
<DisptachContext.Provider value={disptach}>
<Count1Context.Provider value={state.count1}>
<Count2Context.Provider value={state.count2}>
{children}
</Count2Context.Provider>
</Count1Context.Provider>
</DisptachContext.Provider value={disptach}>
)
}
最后,再把Parent
组件放在Provider
组件里面:
js
const App = () => {
<Provider>
<Parent />
</Provider>
}
这个应用并不会遭遇额外的重新渲染;count1
的变动之后触发Counter1
组件的重新渲染,Counter2
组件并不会因此重新渲染.
把状态拆分成更细的Context的好处是,更新时可以同时更细多个状态。比如说,你可以在reducer里面添加这个操作:
js
if (action.type === "INC_BOTH") {
return {
...prev,
count1: prev.count1 + 1,
count2: prev.count2 +1,
};
}
正如我们在第一种解决方案中讨论的那样,创建一个用于对象(如用户对象)的 Context 在这种解决方案中也是可以接受的。
在本节中,我们学习了两种使用 Context 实现全局状态的解决方案。这些是典型的解决方案,但可能会有很多变体。关键点是使用多个 Context 来避免不必要的重新渲染。在下一节中,我们将学习一些基于多个 Context 的全局状态的最佳实践。
使用Context的最佳实践
在下面的部分,我们要讨论下面三个模式,以处理全局状态问题:
- 自定义钩子和provider组件
- 自定义钩子的工厂模式
- 使用
reducerRight
以避免provider嵌套。
让我们挨个看。
自定义钩子和provider组件
在之前的例子中,我们直接使用useContext
来获取Context的值。现在,我们可以直接显示地定义相关的钩子和provider组件。这样,我们就可以隐藏Context,并限制其使用。
下面的例子里,我们要创建自定义钩子和provider组件。我们要创建一个默认的Context 值 null,再检查 这个 值 在自定义 钩子 中是否为 null。
首先,我们要创建一个Context:
js
type CountContextType = [
number,
Dispatch<SetStateAction<number>>
]
const Count1Context = createContext<
CountContextType | null
>(null);
然后,我们定义Count1Provider
,在Count1Provider
中,我们用useState
创建一个状态,再把它传递给 Count1Context.Provider
:
js
export const Count1Provider = ({
chilren
}: {
children: ReactNode
}) => (
<Count1Context.Provider value={useState(0)}>
{children}
</Count1Context.Provider>
)
注意,useState(0)
这个写法。它是合法的,它是const [count, setCount] = useState(0)
的简写。
之后,我们可以定义 useCount1
钩子,来返回Count1Context
的值。在这里,我们要检查Context的值是否为null。如果为null,我们要抛出一个 Error:
js
export const useCount1 = () => {
const value = useContext(Count1Context);
if (value === null) throw new Error("Provider missing");
return value;
}
同样的代码逻辑,用到count2上:
js
const Count2Context = createContext<
CountContextType | null
>(null);
export const Count2Provider = ({
chilren
}: {
children: ReactNode
}) => (
<Count2Context.Provider value={useState(0)}>
{children}
</Count2Context.Provider>
)
export const useCount2 = () => {
const value = useContext(Count2Context);
if (value === null) throw new Error("Provider missing");
return value;
}
接下来,我们就可以定义Counter1
组件了:
js
const Counter1 = () => {
const [count1, setCount1] = useCount1();
return (
<div>
Count1: {count1}
<button onClick={() => setCount1((c) => c + 1 )}
+1
</button>
</div>
)
}
同理,这是Counter2
组件:
js
const Counter2 = () => {
const [count2, setCount2] = useCount2();
return (
<div>
Count1: {count2}
<button onClick={() => setCount2((c) => c + 1 )}
+1
</button>
</div>
)
}
注意,Counter2
组件 和 Counter1
组件非常相似。其主要不同在于,Counter2
组件使用了 useCount2
钩子,而Counter1
使用了useCount1
钩子。
我们定义了一个Parent
组件,这个组件调用了Counter1
组件 和 Counter2
组件:
js
const Parent = () => {
<div>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</div>
}
最后,就是定义App
组件,在App
组件里,用两个provider来包裹着Parent
组件:
js
const App = () => {
<Count1Provider>
<Count2Provider>
<Parent />
</Count2Provider>
</Count1Provider>
}
通常,我们会为每个 Context 创建一个单独的文件,例如 contexts/count1.jsx
。在这个文件中,我们只导出自定义钩子(如 useCount1
)和提供者组件(如 Count1Provider
),而不导出 Count1Context
本身。这种做法有助于保持代码的组织性和可维护性。
用自定义钩子实现工厂模式
为一个状态创建钩子和provider组件,其实是一个重复性的工作;好在,我们可以为这个任务创建一个函数。
下面这个例子展示了一个createStateContext
函数的具体实现。
createStateContext
函数接受一个自定义钩子useValue
。useValue
会接收一个初始值,并返回一个状态。如果你使用useState
,它会返回一个由 state 和 setState 组件的元组。而这个 createStateContext
函数 会返回一个 由 provider组件和 自定义钩子组件的元组。
此外,它还有一个特性:provider组件 可以 接收一个 可选的,传递给 useValue
的 initialValue
属性。这允许你在运行时设置状态的初始值,而不是在创建时定义一个固定的初始值。代码如下:
js
const createStateContext = (
useValue: (init) => State,
) => {
const StateContext = createContext(null);
const StateProvider = ({
initialValue,
children
}) => {
<StateContext.Provider value={useValue(initialValue)}>
{children}
</StateContext.Provider>
};
const useContextState = () => {
const value = useContext(StateContext);
if (value === null) throw new Error("Provider missing");
return value;
}
return [StateProvider, useContextState] as const;
}
然后,我们看看如何使用createStateContext
函数。我们可以定义一个自定义钩子useNumberState
; 它会接收一个可选的 init
参数:
js
const useNumberState = (init) => useState(init || 0);
通过传递useNumberState
给 createStateCount
,我们可以创建很多Context。我们创建了两个Context。我们用useNumberState
可以 创建自定义的 useCount1
和 useCount2
钩子。代码如下:
js
const [Count1Provider, useCount1] =
createStateCount(useNumberState);
const [Count2Provider, useCount2] =
createStateCount(useNumberState);
有了createStateCount
后,我们就可以避免重复工作了。
之后,我们可以再次定义Counter1
组件 和 Counter2
组件。然后,它们使用useCount1
和 useCount2
钩子 和之前是 一样的:
js
const Counter1 = () => {
const [count1, setCount1] = useCount1();
return (
<div>
Count1: {count1}
<button onClick={() => setCount1((c) => c + 1 )}
+1
</button>
</div>
)
}
const Counter2 = () => {
const [count2, setCount2] = useCount2();
return (
<div>
Count1: {count2}
<button onClick={() => setCount2((c) => c + 1 )}
+1
</button>
</div>
)
}
然后,我们要写 Parent
和 App
组件。在使用 Count1Provider
和 Count2Provider
时,和之前一样:
js
const Parent = () => {
<div>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</div>
}
const App = () => {
<Count1Provider>
<Count2Provider>
<Parent />
</Count2Provider>
</Count1Provider>
}
注意,这样的代码可以减少代码量。createStateContext
的要点是,在避免了重复代码的前提下,还保证提供了同样的功能。
除了用useState
实现useNumberState
外,我们可用useReducer
实现useNumberState
:
js
const useMyState = () => useReducer({}, (prev, action) => {
if (action.type === 'SET_FOO') {
return { ...prev, foo: action.foo };
}
// ...
};
我们甚至可以创建一个更复杂的钩子:
js
const useMyState = (initialState = { count1: 0, count2: 0} ) => {
const [state, setState] = useState(initialState);
useEffect(() => {
console.log('updated', state);
});
const inc1 = useCallback(() => {
setState((prev) => {
...prev,
count1: prev.count1 + 1
})
}, []);
const inc2 = useCallback(() => {
setState((prev) => {
...prev,
count2: prev.count2 + 1
})
}, []);
return [state, { inc1, inc2 }]
};
传入 useMyState
给 createStateContext
, 依然是可行的。传其他自定义钩子也行。
值得注意的是,这个模式在使用TS的情况下,运行地非常好。TS可以进行类型检查,进而提升开发体验。
js
const createStateContext = <Value, State>(
useValue: (init?: Value) => State
) => {
const StateContext = createContext<State | null>(null);
const StateProvider = ({
initialValue,
children,
}: {
initialValue?: Value;
children?: ReactNode;
}) => (
<StateContext.Provider value={useValue(initialValue)}>
{children}
</StateContext.Provider>
);
const useContextState = () => {
const value = useContext(StateContext);
if (value === null){
throw new Error("Provider missing");
}
return value;
};
return [StateProvider, useContextState] as const;
};
const useNumberState = (init?: number) => useState(init || 0);
使用reduceRight
来避免重复嵌套
有了createStateContext
函数,创建状态变得很容易。假设我们创建了五个状态:
js
const App = () => (
<Count1Provider initialValue={10}>
<Count2Provider initialValue={20}>
<Count3Provider initialValue={30}>
<Count4Provider initialValue={40}>
<Count5Provider initialValue={50}>
<Parent />
</Count5Provider>
</Count4Provider>
</Count3Provider>
</Count2Provider>
</Count1Provider>
);
这段代码本身是没问题。但是这样的开发体验太糟糕了。为了避免这种风格,我们可以使用reduceRight
:
js
const App = () => {
const providers = [
[Count1Provider, { initialValue: 10 }],
[Count2Provider, { initialValue: 20 }],
[Count3Provider, { initialValue: 30 }],
[Count4Provider, { initialValue: 40 }],
[Count5Provider, { initialValue: 50 }],
]
return providers.reduceRight(
(children, [Component, props]) =>
createElement(Component, props, children),
<Parent />,
);
}
当然,这个技巧也有变体。我们也可以使用 HOC 高阶组件。但其实核心都是一样,使用 reduceRight
来构建provider 树。
这个技巧不仅对全局状态有用,对任何组件都有用。
在这个部分,我吗学习了用Context 构建全局状态的最佳实践。并不是说,你必须要遵守所有的最佳实践。只要你理解了Context的工作原理与局限,任何模式都可以有效的使用。
概要
在本章中,我们学习了如何使用 React Context 创建全局状态。Context 的传播机制可以避免在组件树中传递属性。如果正确理解 Context 的行为,使用 Context 实现全局状态就变得简单明了。基本上,我们应该为每个状态片段创建一个独立的 Context,以避免不必要的重渲染。一些最佳实践将有助于使用 Context 实现全局状态,特别是 createStateContext
的具体实现,这将帮助组织应用代码。
在下一章中,我们将学习另一种使用订阅模式实现全局状态的方法。