react 代码优化

  太长不看 -> 视图不显示的内容多用useRef,多用useMemo和useCallback,如果逻辑比较内聚,可以封装逻辑到 class,然后自己判断需要更新的时机,手动调用方法去重新render组件。

  在做自己的小项目的时候,突然想到,react优化本质上是阻止react不必要的刷新,那么除了什么list加key避免无效diff,使用pureComponent和React.memo,等官方说烂了的方法之外,我们在日常开发中要如何写一个让react少render的组件呢。

  比如以下例子,我们有一个form

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
32
33
34
35
36
37
38
39
40
41
42
43
import React, { useContext } from 'react';
import { RepoContext, type RepoInfo } from '@/hooks/useRepoInfoContext';

const TokenForm = (): React.JSX.Element | null => {
const { repoInfo, saveRepoInfo } = useContext(RepoContext);
const {tempRepoInfo, setTempRepoInfo} = useState(repoInfo)
const handleSaveRepoInfo = (): void => {
saveRepoInfo(tempRepoInfo);
};
return (
<div>
<div>
<input
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
setTempRepoInfo({...tempRepoInfo,token:e.detail.value})
}}
defaultValue={repoInfo.token}
/>
</div>
<div>
<input
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
setTempRepoInfo({...tempRepoInfo,domain:e.detail.value})
}}
defaultValue={repoInfo.domain}
/>
</div>
<div>
<input
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
setTempRepoInfo({...tempRepoInfo,projectId:e.detail.value})
}}
defaultValue={repoInfo.projectId}
/>
</div>
<Button onClick={handleSaveRepoInfo}>
提交
</Button>
</div>
);
};

export default TokenForm;

  代码很简单,但是问题也很大。因为这里我使用了一个tempRepoInfo去存储临时数据,但是我使用了useState,这就会导致我每一次输入都会重新渲染一次组件,但是其实我们每一次输入后不需要render组件,输入框也会显示我们输入的数据,我们应该避免每次render,所以这里我们不使用state来控制tempRepoInfo,代码就变成了如下所示

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { useContext } from 'react';
import { RepoContext, type RepoInfo } from '@/hooks/useRepoInfoContext';
const tempRepoInfo: RepoInfo = {
token: '',
domain: '',
projectId: '',
};
const TokenForm = (): React.JSX.Element | null => {
const { repoInfo, saveRepoInfo } = useContext(RepoContext);
const handleSaveRepoInfo = (): void => {
saveRepoInfo(tempRepoInfo);
};
return (
<div>
<div>
<input
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
tempRepoInfo.token = e.target.value;
}}
defaultValue={repoInfo.token}
/>
</div>
<div>
<input
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
tempRepoInfo.domain = e.target.value;
}}
defaultValue={repoInfo.domain}
/>
</div>
<div>
<input
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
tempRepoInfo.domain = e.target.value;
}}
defaultValue={repoInfo.projectId}
/>
</div>
<Button onClick={handleSaveRepoInfo}>
提交
</Button>
</div>
);
};

export default TokenForm;

  试了下代码没问题,但是这样的话变量在外面,所以建议使用react的useRef去存储这个变量。那么基于这一点,如果我们需要封装一个useRequest的话以下代码会不会有问题呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useState } from 'react';

type UseRequestReturn<T extends (...args: any) => Promise<any>> = {
run: (...param: Parameters<T>) => Promise<undefined>;
loading: boolean;
data: PromiseReturn<T> | null;
};

type PromiseReturn<T> = T extends (...args: any) => Promise<infer R> ? R : T;
export default <T extends (...args: any) => Promise<any>>(
fn: T extends (...args: any) => Promise<infer R> ? T : T,
): UseRequestReturn<T> => {
const [data, setData] = useState<null | PromiseReturn<T>>(null);
const [loading, setLoading] = useState(false);
const run = async (...param: Parameters<T>): Promise<undefined> => {
setLoading(true);
const result = await fn(...param);
setData(result);
setLoading(false);
};

return { run, loading, data };
};

  看上去好像是没问题的,但事实上我们用了两个state,这个时候肯定有疑惑了,react自动批量更新,有个锤子问题。嘛,批量更新是17就能用了,但是17的批量更新是有问题的,它只会在react事件中进行批量更新,在promise、setTimeout、native event中是不会有批量更新的,只有在react 18中实现了所有异步事件的批量更新。

  所以我们应该如何去修改呢,可以去参考一下ahook的代码,里面有一个useUpdate方法,直接返回了一个setState,然后手动执行setState就相当于强制更新页面。所以思路就是将request相关的内容封装为一个class,然后仅在我们认为需要刷新到地方的地方手动update就行了。代码如下