React 中的重新渲染是什么?
当谈到 React 的性能时,我们需要关注两个主要阶段:
-
初始渲染——当组件第一次出现在屏幕上时发生
-
重新渲染——已经在屏幕上的组件后续的渲染
当 React 更新应用程序数据时,就会发生 re-render,如
用户与应用程序交互
、
通过异步请求
、
某些订阅模型
传入一些外部数据。
哪些 re-render 是必需的?
-
必需的 re-render
—— 作为更改源的组件,或直接使用新数据的组件。
-
-
例如,如果用户在 input 中输入,管理其状态的组件需要在每次按键时更新自己,就会重新渲染。
-
多余的 re-render
—— 由于错误或低效的应用程序架构,通过数据传递,组件被重新渲染。
-
-
例如,用户在 input 中输入,每次按键时都重新渲染整个页面,那么页面就是不必要地重新渲染了。
React 足够快,大多数情况下都能够在用户没有注意到的情况下重新渲染页面,但如果 re-render 发生得太频繁或在非常重的组件上,可能会导致用户体验出现“卡顿迟滞”,这就需要我们进行性能优化了。
React 组件何时会 re-render?
组件重新渲染大概有四个原因:
-
状态变化
-
父级组件重新渲染
-
Context 变化
-
hooks 变化
一个误区:当组件的 props 改变时,会发生重新渲染。其实这是由于父组件重新渲染传播给子组件而造成的,与 props 变化与否无直接关系,详见下文。
状态更改
当组件的状态发生变化时,它将重新渲染,常发生在回调或 useEffect 钩子中。
状态更改是所有 re-render 的“根”源。
import { useEffect, useState } from "react";
const App = () => {
const [state, setState] = useState(0);
const onClick = () => {
setState(state + 1);
console.log("重新渲染次数: ", state);
useEffect(() => {
console.log("re-mount");
}, []);
return (
<h2>打开控制台,点击按钮</h2>
<p>每次点击都会输出log,因为状态改变,页面重新渲染</p>
<p>re-render count: {state}</p>
<button onClick={onClick}>点击{state}次</button>
export default App;
父级组件重新渲染
如果父组件重新渲染,则当前组件也会重新渲染。也就是说,当一个组件重新渲染,其所有子组件都会重新渲染。
re-render 总是沿着树“向下”:子节点的重新渲染不会触发父节点的重新渲染。
import { useState } from "react";
const Child = () => {
console.log("子组件 re-render,字体颜色改变");
const r = Math.ceil(Math.random() * 255);
const g = Math.ceil(Math.random() * 255);
const b = Math.ceil(Math.random() * 255);
return <p style={{ color: 'rgb('+r+','+g+','+b+')' }}>child</p>;
const App = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
console.log("当前组件re-render 次数: ", state);
return (
<h2>打开控制台,点击按钮</h2>
<p>每次点击都会输出log,因为状态改变,页面重新渲染</p>
<Child />
<button onClick={onClick}>点击{state}</button>
export default App;
Context 变化
当
Context Provider
中的值
value
发生变化时,所有使用该 Context 的组件都将 re-render,即使它们不直接使用变化的那部分数据。
这些 re-render 不能通过缓存 memo 直接阻止,但也有一些办法可以阻止由上下文引起的 re-render。
import { useState, createContext, useContext, useMemo, ReactNode } from "react";
const Context = createContext<{ value: number }>({ value: 1 });
const Provider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
const value = useMemo(
() => ({
value: state,
[state]
return (
<Context.Provider value={value}>
<button onClick={onClick}>点击</button>
{children}
</Context.Provider>
const useValue = () => useContext(Context);
const Child1 = () => {
const { value } = useValue();
console.log("Child1 re-renders: ", value);
return <></>;
const Child2 = () => {
const { value } = useValue();
console.log("Child2 re-renders: ", value);
return <></>;
const App = () => {
return (
<Provider>
<h2>打开控制台,点击按钮</h2>
<p>两个child页面都会重新渲染</p>
<Child1 />
<Child2 />
</Provider>
export default App;
hooks 变化
hook 内部发生的一切都“属于”使用它的组件。一些规则:
-
hook 内部的状态更改将触发“host”组件不可避免的 re-render;
-
如果 hook 使用了 Context,而 Context 的值发生了变化,它将触发“host”组件不可避免的 re-render;
hook 可以链式调用。链中的每个 hook 仍然“属于”“host”组件,并且相同的规则适用于它们中的任何一个。
host:宿主
import { useState, createContext, useContext, useMemo, ReactNode } from "react";
const Context = createContext<{ value: number }>({ value: 1 });
const Provider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
const value = useMemo(
() => ({
value: state,
[state]
return (
<Context.Provider value={value}>
<button onClick={onClick}>点击</button>
{children}
</Context.Provider>
const useValue = () => useContext(Context);
const useSomething = () => {
const count = useValue();
return count.value;
const Child = () => {
const value = useSomething();
console.log("Child re-renders:", value);
return <></>;
const App = () => {
return (
<Provider>
<h2>打开控制台,点击按钮</h2>
<p>hooks链式作用,Child将re-render</p>
<Child />
</Provider>
export default App;
props 变化
未 memoized 组件,props 不改变,child 依旧重新渲染
re-render 未缓存的组件,组件的 props 是否改变并不重要。为了改变 props,它们需要由父组件更新。这意味着父组件必须重新渲染,这将触发子组件的重新渲染,而不管它的 props 是什么。
只有在使用缓存技巧时(React.memo, useMemo),那么 props 的改变就变得重要了。
import { useState, memo } from "react";
const Child = ({ data }: { data: string; state?: number }) => {
console.log("Child re-renders");
return <p>{data}</p>;
const ChildMemo = memo(Child);
const value = "test";
const App = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<h2>打开控制台,点击按钮</h2>
<p>prop未变化,Child依旧重渲染了</p>
<button onClick={onClick}>点击{state}</button>
<Child data={value} />
<p>props如果有变化的值,即便child未用到,也会导致child重渲染</p>
<ChildMemo data={value} state={state} />
export default App;
在渲染函数中创建组件
不推荐的写法
在另一个组件的渲染函数中创建组件是一种很不好的开发模式,对性能很不友好。在每次重新渲染时,React 都会重新挂载这个组件(即销毁它并从头重新创建它),这比正常的重新渲染要慢得多。可能会出现问题:
import { useState, useEffect } from "react";
const Component = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
const VerySlowComponent = () => {
console.log("Very slow component re-renders");
useEffect(() => {
console.log("Very slow component re-mounts");
}, []);
return <div>Very slow component</div>;
return (
<button onClick={onClick}>点击</button>
<VerySlowComponent />
const App = () => {
return (
<h2>打开控制台,点击按钮</h2>
<p>每次点击,重型组件都会重新挂载</p>
<Component />
export default App;
如何减少重新渲染?
state 下放
当重型组件管理状态时,此模式非常有用,并且 state 仅用于呈现树的一小部分独立部分。
一个典型的例子是,在呈现页面重要部分的复杂组件中,通过单击按钮打开/关闭Dialog。在这种情况下,控制Dialog外观的状态、Dialog本身和触发更新的按钮可以封装在更小的组件中。因此,较大的组件不会在这些状态更改时重新渲染。
import { useState } from "react";
const VerySlowComponent = () => {
console.log("Very slow component re-render");
return <div>Very slow component</div>;
const FullComponent = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<h3>一般写法,平铺在一起</h3>
<p>事件交互时,影响重型组件也跟着一起re-render</p>
<p>re-render count: {state}</p>
<button onClick={onClick}>点击</button>
<VerySlowComponent />
const ComponentWithButton = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<p>re-render count: {state}</p>
<button onClick={onClick}>点击</button>
const SplitComponent = () => {
return (
<h3>拆分后组件,包含两个子组件:交互组件 & 重型组件</h3>
<p>事件交互时,只有交互组件会re-render,重型组件不受影响</p>
<ComponentWithButton />
<VerySlowComponent />
export default function App() {
return (
<h2>打开控制台,点击按钮</h2>
<p>平铺代码</p>
<FullComponent />
<hr />
<p>拆分代码</p>
<SplitComponent />
}
逻辑更内聚,确保影响范围最小,性能会更好。细粒度控制,不会影响不相关组件。
children 作为 props
类似于上一种方式:它将状态更改封装在较小的组件中。这里的不同之处在于,state 是在封装渲染树中较慢部分的元素上使用的,因此不能那么容易地提取它。
一个典型的例子是附加到组件根元素的 onScroll 或 onMouseMove 回调函数。
在这种情况下,状态管理和使用该状态的组件可以被提取到更小的组件中,而速度较慢的组件可以作为子组件传递给它。从较小组件的角度来看,子组件只是 props,所以它们不会受到状态变化的影响,因此不会重新渲染。
import React, { useState, ReactNode } from "react";
const VerySlowComponent = () => {
console.log("Very slow component re-renders");
return <div>Very slow component</div>;
const FullComponent = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<div onClick={onClick} style={{ background: "#dfa" }}>
<p>Re-render count: {state}</p>
<h3>平铺写法,事件交互,会影响无关组件</h3>
<p>事件交互时,重型组件也跟着re-render</p>
注意交互区域包含重型组件,也就是重型组件也参与交互,但不依赖变化的值
<VerySlowComponent />
const ComponentWithClick = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<div onClick={onClick} style={{ background: "#afd" }}>
<p>Re-render count: {state}</p>
{children}
const SplitComponent = () => {
return (
<ComponentWithClick>
把重型组件从其父级组件中去除,用children代替其位置,通过children展示重型组件
<p>点击交互区域,重型组件不再re-render</p>
<VerySlowComponent />
</ComponentWithClick>
export default function App() {
return (
<h2>打开控制台,点击按钮</h2>
<FullComponent />
<hr />
<SplitComponent />
}
组件作为 props
与前面的模式基本相同,具有相同的行为:它将状态封装在一个较小的组件中,重的组件作为 props 传递给它。props 不受状态变化的影响,所以重的组件不会重新渲染。
当少数重型组件独立于状态时可以有用,但不能抽取成 children 来使用。
import { useState, ReactNode } from "react";
const VerySlowComponent = () => {
console.log("Very slow component re-renders");
return <div>Very slow component</div>;
const AnotherSlowComponent = () => {
console.log("Another slow component re-renders");
return <div>Another slow component</div>;
const FullComponent = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<div onClick={onClick} style={{ background: "#19b" }}>
<h3>糟糕的写法</h3>
<p>点击交互,重型组件re-render</p>
<p>Re-render count: {state}</p>
<VerySlowComponent />
<p>Something</p>
<p>Something</p>
<p>Something</p>
<AnotherSlowComponent />
const ComponentWithClick = ({
upup,
down,
upup: ReactNode;
down: ReactNode;
}) => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<div onClick={onClick} style={{ background: "#b19" }}>
<p>Re-render count: {state}</p>
{upup}
<p>Something</p>
<p>Something</p>
<p>Something</p>
{down}
const SplitComponent = () => {
const up = (
<h3>交互区域内部的多个不连续重型组件,被当作props分别传入</h3>
<p>交互时,重型组件不再随着re-render</p>
<VerySlowComponent />
const down = <AnotherSlowComponent />;
return (
<ComponentWithClick upup={up} down={down} />
export default function App() {
return (
<h2>打开控制台,点击按钮</h2>
<FullComponent />
<hr />
<SplitComponent />
}
React.memo
简单组件,无 props
使用 React.memo 包裹的组件,其下游组件不会被重新渲染,除非该组件的 props 发生了变化。
import React, { useState } from "react";
const Child = () => {
console.log("Child re-renders");
const r = Math.ceil(Math.random() * 255);
const g = Math.ceil(Math.random() * 255);
const b = Math.ceil(Math.random() * 255);
return <p style={{ color: 'rgb('+r+','+g+','+b+')' }}>child</p>;
const ChildMemo = React.memo(Child);
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<h2>打开控制台,点击按钮</h2>
<button onClick={onClick}>点击 {state}</button>
<p>未被React.memo()包裹,组件会重新渲染,字体颜色会改变</p>
<Child />
<p>被React.memo()包裹,组件不会重新渲染,字体颜色不会改变</p>
<ChildMemo />
}
含 props 组件
要让 React.memo 生效,所有不是简单数据类型的 props 都必须被记住(memorized)
import React, { useState, useMemo } from "react";
const Child = ({ data }: { data: { value: string } }) => {
console.log("Child re-renders", data.value);
return <>{data.value}</>;
const ChildMemo = React.memo(Child);
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
const memoValue = useMemo(() => ({ value: "second" }), []);
return (
<h2>打开控制台,点击按钮</h2>
<p>Second child doesn't re-render</p>
<button onClick={onClick}>点击</button>
<p>props为复杂数据类型,未被useMemo记忆,导致组件re-renders</p>
<ChildMemo data={{ value: "first" }} />
<p>props为复杂数据类型但被useMemo记忆,组件不会re-renders</p>
<ChildMemo data={memoValue} />
}
组件作为 children 或 props
当组件作为 props 或 children 传递时,"React.memo"必须应用于这些元素上,只是 memo 父组件不起作用
import React, { useState, useMemo, ReactNode } from "react";
const Child = ({ data }: { data: { value: string } }) => {
console.log("Child re-renders", data.value);
return <p>{data.value}</p>;
const ChildMemo = React.memo(Child);
const Parent = ({
left,
children,
children: ReactNode;
left?: ReactNode;
}) => {
console.log("parent re-render");
return (
<div style={{ display: "flex", justifyContent: "center" }}>
<aside style={{ background: "#abc", marginRight: "16px" }}>{left}</aside>
<main style={{ background: "#cba" }}>{children}</main>
const ParentMemo = React.memo(Parent);
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
const memoValue = useMemo(() => ({ value: "memoized data" }), []);
return (
<h2>打开控制台,点击按钮</h2>
<button onClick={onClick}>点击</button>
<p>memoized 父,未 memoized 子 将 re-render</p>
{}
<ParentMemo left={<Child data={{ value: "left child of ParentMemo" }} />}>
<Child data={{ value: "child of ParentMemo" }} />
</ParentMemo>
未Memoized父,Memoized 子 将不会
re-render;适用上一条原则,要React.memo生效,props需被记忆
{}
<Parent left={<ChildMemo data={memoValue} />}>
<ChildMemo data={memoValue} />
</Parent>
}
useMemo/useCallback
仅缓存 props
不推荐:props 上不必要的使用 useMemo/useCallback
单独缓存 props 并不会阻止子组件的重新渲染。如果父组件重新渲染,它将触发子组件的重新渲染,而不管它的 props 是什么。
import React, { useState, useMemo } from "react";
const Child = ({ data }: { data: { value: string } }) => {
console.log("Child re-renders", data.value);
return <p>Child:value一直是{data.value},未改变</p>;
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
const memoValue = useMemo(() => ({ value: "child" }), []);
return (
<h2>打开控制台,点击按钮</h2>
<p>非必要的使用useMemo/useCallback(二者同质)</p>
组件没有使用React.memo包裹,只是使用useMemo记忆value,依旧会重新渲染
<p>子组件props未改变,但依旧重新渲染</p>
<button onClick={onClick}>点击</button>
<Child data={memoValue} />
}
useMemo 处理子组件的复杂数据类型 props
import React, { useState, useMemo } from "react";
const Child = ({ data }: { data: { value: number } }) => {
console.log("Child re-render:", data.value);
return <p>{data.value}</p>;
const ChildMemo = React.memo(Child);
const values = [1, 2, 3];
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
console.log("当前组件re-render");
const items = useMemo(() => {
return values.map((val) => <Child data={{ value: val }} key={val} />);
}, []);
const vals = useMemo(() => {
return values.map((val) => ({
value: val,
}, []);
return (
<h2>打开控制台,点击按钮</h2>
使用useMemo记忆子组件列表,不用分别使用memo记忆子组件,useMemo记忆props
value值
<button onClick={onClick}>改变 {state},当前组件重新渲染</button>
{}
{items}
{}
{values.map((val, index) => (
<ChildMemo data={{ value: val }} key={val} />
}
当前组件 hook 依赖复杂数据类型
如果一个组件在 useEffect、useMemo、useCallback 等钩子中使用复杂数据类型作为依赖项,那么它应该被缓存。
import React, { useState, useMemo, useEffect } from "react";
export default function App() {
const val = { value: "not memoized" };
const memoValue = useMemo(() => ({ value: "memoized" }), []);
const [value] = useState({ value: "memoized? possibly" });
const [state, setState] = useState(0);
const onClick = () => {
setState(state + 1);
useEffect(() => {
console.log("我被memo,再点你也看不见我");
}, [memoValue]);
useEffect(() => {
console.log("我在state, 一样看不见我");
}, [value]);
useEffect(() => {
console.log("val 我比较可爱,一点我就出来啦");
}, [val]);
return (
<h2>打开控制台,点击按钮</h2>
<button onClick={onClick}>点击了{state}次</button>
}
很费算力的情景
useMemo 的一个用例是避免在每次重新渲染时进行昂贵的计算。
useMemo 有它的成本(消耗一点内存并使初始渲染稍微变慢),所以不应该在每次计算中都使用它。大多数情况下,挂载和更新组件是最昂贵的计算。
因此,useMemo 的典型用例是缓存 React 元素。通常是现有渲染树的一部分或生成渲染树的结果,如返回新元素的 map 函数。
与组件更新相比,“纯”javascript 操作(如排序或筛选数组)的成本通常可以忽略不计。
import React, { useState, useMemo } from "react";
const ExpensiveChild = ({ data }: { data: { value: number } }) => {
console.log("Expensive Child re-renders", data.value);
return <p>{data.value}</p>;
const values = [1, 2, 3];
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
const items = useMemo(() => {
return values.map((val) => (
<ExpensiveChild data={{ value: val }} key={val} />
}, []);
return (
<h2>打开控制台,点击按钮</h2>
<p>CPU密集型子组件不会重新渲染</p>
<button onClick={onClick}>点击 {state}</button>
{items}
}
优化列表性能
除了常规的重新渲染规则和模式外,key 属性还会影响 React 中列表的性能。
仅仅提供 key 属性不会提高列表的性能。为了防止列表元素的重新渲染,将列表元素用 React.memo 包裹,并遵循以下最佳实践:
key 中的 value 应该是一个字符串,它在列表中的每个元素的重新渲染之间是一致的。通常,使用元素的 id 或数组的索引。
如果列表是静态的,即元素没有添加/删除/插入/重新排序,那么使用数组的下标作为key也是可以的。
不过在动态列表中使用数组的索引会导致:
-
如果项目有状态或任何未受控元素(如表单输入),则会出现错误
-
如果项目被包裹在 React.memo 中,则会降低性能
静态列表
import React, { useEffect, useState } from "react";
const Child = ({ value }: { value: number }) => {
console.log("Child re-renders", value);
return <li>{value}</li>;
const ChildMemo = React.memo(Child);
const values = [1, 2, 3];
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
console.log("父级组件由于state变化,re-render");
useEffect(() => {
console.log("父级组件 re-mount");
}, []);
return (
<h2>打开控制台,点击按钮</h2>
<p>静态列表,分别使用value和index作为key,都可以的</p>
<p>子组件都不会重新渲染</p>
<button onClick={onClick}>点击 {state}</button>
{values.map((val, index) => (
<ChildMemo value={val} key={index} />
{values.map((val) => (
<ChildMemo value={val} key={val} />
}
动态列表
import React, { useState } from "react";
const Child = ({ value }: { value: string }) => {
console.log("Child re-renders", value);
return <div>{value}</div>;
const values = [3, 1, 2];
const ChildMemo = React.memo(Child);
export default function App() {
const [state, setState] = useState<"ascend" | "descend">("ascend");
const onClick = () => {
setState(state === "ascend" ? "descend" : "ascend");
const sortedValues =
state === "ascend" ? values.sort() : values.sort().reverse();
return (
<h2>打开控制台,点击按钮</h2>
<p>动态列表,分别使用id value和index作为key</p>
<p>index作为key的组件会重新渲染,value作为key的组件不会</p>
<button onClick={onClick}>切换排序 {state}</button>
{}
{}
{sortedValues.map((val, index) => (
<ChildMemo value={`Child of index: ${val}`} key={index} />
{/* 同一个组件,props(key/value)都没有改变,状态改变前后还是同一个组件,组件只是发生了移动,并未重新渲染 */}
{sortedValues.map((val, index) => (
<ChildMemo value={`Child of id: ${val}`} key={val} />
}
随机值作为列表中的key
不推荐的用法
随机生成的值不应该用作列表中 key 属性的值。它们将导致 React 在每次重新渲染时重新挂载元素,导致
-
性能很差的列表
-
如果列表有状态或任何未受控元素(如表单输入),则会出现 bug
import React, { useState, useEffect } from "react";
const Child = ({ value }: { value: number }) => {
console.log("Child re-renders", value);
useEffect(() => {
console.log("Child re-mounts");
}, []);
return <div>{value}</div>;
const ChildMemo = React.memo(Child);
const values = [1, 2, 3];
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<h2>打开控制台,点击按钮</h2>
<button onClick={onClick}>点击 {state}</button>
{}
{values.map((val) => (
<ChildMemo value={val} key={Math.random()} />
}
使用 Context 时如何防止重新渲染
缓存 Provider 的值
如果 Context Provider 不是放在应用的根位置,且组件有可能因为其祖先的更改而重新渲染,那么此组件的值应该被记住。
import React, {
useState,
createContext,
useContext,
useMemo,
ReactNode,
} from "react";
const Context = createContext<{ value: number }>({ value: 1 });
const Context2 = createContext<{ value: number }>({ value: 1 });
const Provider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
const data = useMemo(
() => ({
value: state,
[state]
return <Context.Provider value={data}>{children}</Context.Provider>;
const Provider2 = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
const data = {
value: state,
return <Context.Provider value={data}>{children}</Context.Provider>;
const useValue = () => useContext(Context);
const useValue2 = () => useContext(Context2);
const Child1 = () => {
const { value } = useValue();
const { value: value2 } = useValue2();
console.log("Child1 re-renders: ", value, value2);
return <></>;
const Child2 = () => {
const { value } = useValue();
const { value: value2 } = useValue2();
console.log("Child2 re-renders: ", value, value2);
return <></>;
const Child1Memo = React.memo(Child1);
const Child2Memo = React.memo(Child2);
export default function App() {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
return (
<Provider>
<Provider2>
<h2>打开控制台,点击按钮</h2>
切换Provider2中的data,一个使用useMemo,另一个没使用memo,查看控制台输出
<p>没有memoize value时,两个组件会被非必需的重新渲染</p>
<button onClick={onClick}>button {state}</button>
<Child1Memo />
<Child2Memo />
</Provider2>
</Provider>
}
分割数据和 API
如果在 Context 中有 data 和 api 的组合(getter 和 setter),它们可以被分割到同一个组件下的不同提供者。这样,仅使用 API 的组件就不会在数据更改时重新渲染。
import {
useState,
createContext,
useContext,
ReactNode,
Dispatch,
SetStateAction,
} from "react";
const ContextData = createContext<number>(1);
const ContextApi = createContext<Dispatch<SetStateAction<number>>>(
() => undefined
const Provider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
return (
<ContextData.Provider value={state}>
{}
<ContextApi.Provider value={setState}>{children}</ContextApi.Provider>
</ContextData.Provider>
const Child1 = () => {
const useApi = () => useContext(ContextApi);
const api = useApi();
console.log("使用API的子组件 re-renders");
const onClick = () => {
api(Math.random() * 10);
return <button onClick={onClick}>在context中设置随机数</button>;
const Child2 = () => {
const useData = () => useContext(ContextData);
const value = useData();
console.log(`使用Data (${value}) 的子组件 re-renders`);
return <p>{value}</p>;
export default function App() {
return (
<Provider>
<h2>打开控制台,点击按钮</h2>
<p>只使用data的子组件会re-render</p>
<p>触发方法所在组件不会更新</p>
<Child1 />
<Child2 />
</Provider>
}
将数据分割成块
如果 Context 管理几个独立的数据块,则可以将它们拆分为同一 Provider 下的更小的 Providers。这样,只有更改块的消费者 consumers 才会重新渲染。
import { useState, createContext, useContext, ReactNode } from "react";
const ContextData1 = createContext<number>(123);
const ContextData2 = createContext<string>("abc");
const Provider = ({ children }: { children: ReactNode }) => {
const [numState, setNumState] = useState(123);
const [strState, setStrState] = useState("abc");
return (
<ContextData1.Provider value={numState}>
<ContextData2.Provider value={strState}>
{}
<button onClick={() => setNumState(numState + 1)}>改变 number</button>
{}
<button onClick={() => setStrState(`${strState}d`)}>改变 string</button>
{children}
</ContextData2.Provider>
</ContextData1.Provider>
const ChildNum = () => {
const useNumData = () => useContext(ContextData1);
const num = useNumData();
console.log("依赖 num data 子组件 re-render");
return <p>{num}</p>;
const ChildStr = () => {
const useStrData = () => useContext(ContextData2);
const str = useStrData();
console.log("依赖 string data 子组件 re-render");
return <p>{str}</p>;
export default function App() {
return (
<Provider>
<h2>打开控制台,点击按钮</h2>
<p>拆分多个不同的providers提供独立的数据,子组件独立更新</p>
<ChildNum />
<ChildStr />
</Provider>
【Message 全局提示】基于 React 实现 Message 组件
使用 ReactDOM.createRoot、React.forwardRef、React.useImperativeHandle 实现 Message 组件。使用 Web Crypto API 生成符合密码学要求的安全的随机 ID。