React状态提升、Context使用及带来的问题分析与解决

React 状态提升、Context 使用及带来的问题分析与解决

在我们平时开发中,避免不了一种场景就是组件状态共享,A 和 B 两个组件有公共的状态,常见的处理方法:

状态提升

将公共的 state 提升至它们的公共祖先,由祖先提供 state 和修改 state 的逻辑,这样有可能导致重新渲染一颗巨大的组件树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const A = (props) => {
console.log("child1 更新了");
return <div>A组件 {props.value.name}</div>;
};

const B = (props) => {
console.log("child2 更新了");
return <div>B组件 {props.value.age}</div>;
};

function App() {
const [value, setValue] = useState({ name: "chenjiang", age: 26 });
return (
<>
<A value={value} />
<B value={value} />

{/* 不依赖value的子组件 */}
<C xxxProps />
<D xxxProps />
<button
onClick={() => {
setValue({ ...value, name: "chenjiang666" });
}}
>
修改name
</button>
</>
);
}

当我们更新 value 时候,父组件会重新渲染,由于 A 和 B 两个组件都依赖了也会随着重新渲染,但是父组件还有一些不依赖的 value 的子组件也进行不必要的渲染。

如何减少不必要的渲染

不依赖 value 的子组件:C、D 这两个组件

我们对 C、D 组件用 React.memo 包裹,保证它的 props 没有发生变化的时候不会重新渲染,但是由于父组件更新了导致函数重新执行,那么 C、D 组件放在父组件的 props 引用发生变化,也会导致重新渲染,所以说并不是组件被 React.memo 包裹了就万事俱备了,还需要对放在父组件的 props 用 useMemo 或者 useCallback 包裹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const C = React.memo(function test() {
return <div>123213</div>;
});

function App() {
const [value, setValue] = useState({ name: "chenjiang", age: 26 });

const cState = useMemo(() => {}, []);
const handleC = useCallback(() => {}, []);
const handleD = useCallback(() => {}, []);

return (
<>
<A value={value} />
<B value={value} />

<C handleC={handleC} cState={cState} />
<D handleD={handleD} />

<button
onClick={() => {
setValue({ ...value, name: "chenjiang666" });
}}
>
修改name
</button>
</>
);
}

注:一般来说 React.memo 都是要和 useMemo 或者 useCallback 配合来使用的。

使用 Context

将公共状态放到 Provider 组件的 value 里面,然后包裹需要此状态的组件,可能引起不必要的渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const AppContext = createContext();

function App() {
const [value, setValue] = useState({ theme: "dark", name: "chenjiang", age: 26 });
return (
<>
<AppContext.Provider value={value}>
<Theme />
<Person />
</AppContext.Provider>

<button
onClick={() => {
setValue({ ...value, theme: value.theme === "dark" ? "light" : "dark" });
}}
>
修改主题色
</button>
</>
);
}

当前 value 更新,Provider 就会重新渲染,其包裹的组件也随着重新渲染,但是有组件所依赖的状态并没有改变,导致了不必要的渲染。

例如:Theme 组件依赖 value 的 theme 属性,Person 组件依赖 value 的 age、name 属性,有一次更新了 value.theme 导致 value 引用发生了改变,就导致 Provider 及其子组件都更新了,Person 组件其实并不依赖 theme 属性但是也更新了,这就造成了不必要的渲染。

如何减少不必要的渲染

为什么存在不必要的渲染,其实本质就是用了自己本不需要的 props,需要做的就是拆分,各取所需。

拆分 Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const AppContext = createContext();
const InfoContext = createContext();

function App() {
const [theme, setTheme] = useState({ theme: "dark" });
const [info, setInfo] = useState({ name: "chenjiang", age: 26 });

const cState = useMemo(() => {}, []);
const handleC = useCallback(() => {}, []);

const handleD = useCallback(() => {}, []);
return (
<>
<AppContext.Provider value={theme}>
<A />
</AppContext.Provider>

<InfoContext.Provider value={info}>
<B />
</InfoContext.Provider>

<button
onClick={() => {
setTheme({ theme: theme.theme === "dark" ? "light" : "dark" });
}}
>
修改主题色
</button>
</>
);
}

提取独有的状态 React.memo

memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。

把当前组件所需要的状态,单独从 Context 提取出来,以 props 形式传递,然后对组件 React.memo 包裹,但是如果组件多了话,每个都需要单独拎出来进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Person = React.memo(function P(props) {
return <div>{props.theme}</div>;
});

function App() {
const [value, setValue] = useState({ theme: "dark", name: "chenjiang", age: 26 });
return (
<>
<AppContext.Provider value={value}>
{/* 所需要的状态单独提取出来 */}
<Theme theme={value.theme} />

<Person />
</AppContext.Provider>

<button
onClick={() => {
setValue({ ...value, theme: value.theme === "dark" ? "light" : "dark" });
}}
>
修改主题色
</button>
</>
);
}

提取独有的状态 useMemo

useMemo 是一个 Hook,可以缓存计算的结果,只有依赖发生变化了,才会重新计算。我们可以利用这个特性,对组件进行缓存,只有它所需要的状态发生变化,才去更新组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function App() {
const [value, setValue] = useState({ theme: "dark", name: "chenjiang", age: 26 });
return (
<>
<AppContext.Provider value={value}>
{/* 所需要的状态单独提取出来 */}
{useMemo(
() => (
<UserPage theme={value.theme} />
),
[value.theme]
)}

<Person />
</AppContext.Provider>

<button
onClick={() => {
setValue({ ...value, theme: value.theme === "dark" ? "light" : "dark" });
}}
>
修改主题色
</button>
</>
);
}