代码放在
demo/react/awesome/src/components/MyDemo文件夹中
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。然而在我们日常开发使用过程中,总会遇到一些棘手的问题,一些看似明明很正确的写法,总是得不到想要实现的效果,最终这些问题萦绕在大脑中,挥之不去,耿耿于怀。于是索性沉下心来,汇总了 React Hook 开发中涉及到的一些问题,看看走过路过的你是否对以下问题都了如指掌了呢?
# 问题 1、如何监听 object 或者 array 类型中 useState 的改变?
核心知识点 React 组件的更新机制对 state 只进行浅对比,也就是更新某个复杂类型数据时只要它的引用地址没变,那就不会重新渲染组件。更新复杂 state 的时候必须传给它一个全新的对象,而不是复制了它引用地址再修改的对象。所以 state 中如果是 object 或者 array 类型,如果直接改其值,是无法监听到变化。
错误示例 1.1 虽然点击两个按钮改变了 state 值,但是 useEffect 无法监听到 state 发生变化,html 也无法重新渲染
import React, { useEffect, useState } from "react";
const MyDemo1 = () => {
const [counter, setCounter] = useState({ a: 1, b: 2 });
const [number, setNumber] = useState([1, 2, 3]);
useEffect(() => {
console.log("监听到变化");
}, [counter, number]);
const changeCounter = () => {
counter.b = 40; //直接改变对象的值
setCounter(counter);
};
const changeNumber = () => {
number[2] = 10; //直接改变数组的值
number.push(4);
setNumber(number);
};
return (
<>
<div>{"counter:" + counter.a + "---" + counter.b}</div>
<button onClick={changeCounter}>change counter</button>
<div>num:</div>
{number.map((item, index) => {
return <p key={index}>{item}</p>;
})}
<button onClick={changeNumber}>change number</button>
</>
);
};
export default MyDemo1;
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
结果示例

正确示例 1.2:使用展开运算符返回一个新对象或者新数组
import React, { useState, useEffect } from "react";
const MyDemo2 = () => {
const [counter, setCounter] = useState({ a: 1, b: 2 });
const [number, setNumber] = useState([1, 2, 3]);
useEffect(() => {
console.log("监听到变化");
}, [counter, number]);
const changeCounter = () => {
setCounter({ ...counter, b: 40 });
};
const changeNumber = () => {
const newNumber = [...number];
newNumber[2] = 20;
newNumber.push(4);
setNumber(newNumber);
};
return (
<>
<div>{"counter:" + counter.a + "---" + counter.b}</div>
<button onClick={changeCounter}>change counter</button>
<div>num:</div>
{number.map((item, index) => {
return <p key={index}>{item}</p>;
})}
<button onClick={changeNumber}>change number</button>
</>
);
};
export default MyDemo2;
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
结果示例

# 问题 2、如何在异步改变 state 后拿到最新的 state 值?
核心知识点 React 合成事件中改变状态是异步的,出于减少 render 次数,react 会收集所有状态变更,然后比对优化,最后做一次变更。
# 2.1 实时获取 state 值
比如点击按钮后改变 counter 的值,然后 counter 作为执行其他操作的参数,需要立刻更新:
示例 2.1
import React, { useState } from "react";
const MyDemo3 = () => {
const [counter, setCounter] = useState(0);
function asyncData() {
console.log("异步请求数据:", counter); //这里会使用num做一些处理,比如num作为参数请求数据
}
function changeCounter() {
setCounter(10); //改变data后执行 asyncData 函数
console.log("counter", counter); //num为0
asyncData();
}
return (
<>
<div>{counter}</div>
<button onClick={changeCounter}>改变num</button>
</>
);
};
export default MyDemo3;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

如上图所示,在点击按钮之后,Html 中明明已经改变了 counter 的值,但是在 asyncData 函数中 counter 仍是 0。 结合上述核心知识点,在代码中可以看出,asyncData 的调用和 setstate 在同一个宏任务中,这时 react 还没有 render,所以直接使用 state 获取的是上一次闭包里的值 0。
这时会有小伙伴说,我们可以使用 useEffect 监听 counter,待 counter 变化后在执行 asyncData。
但是若多个按钮点击后导致 counter 发生变化,但是只有第一个按钮改变 counter 的时候才去执行 asyncData 函数,这种情况就不行了,比如:
示例 2.2
import React, { useState, useEffect } from "react";
const MyDemo4 = () => {
const [counter, setCounter] = useState(0);
function asyncData() {
console.log("异步请求数据:" + counter); //这里会使用num做一些处理,比如请求数据
}
function changeCounter() {
setCounter(10);
console.log("counter", counter); //num为0
}
function pureChangeCounter() {
setCounter(20);
}
useEffect(() => {
asyncData(), [counter];
});
return (
<>
<button
onClick={() => {
changeCounter();
}}
>
点击我执行异步函数
</button>
<div>{counter}</div>
<button
onClick={() => {
pureChangeCounter();
}}
>
点击我只是单纯改变data
</button>
</>
);
};
export default MyDemo4;
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
上述代码中,点击两个按钮都会改动 counter 的值,useEffect 监听 counter 变化后执行 asyncData 函数,但是如果只想让点击第一个按钮的时候才执行 asyncData 函数,上述使用 useEffect 监听就无法满足了。
改进 可以使用 useRef,useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。
import React, { useState, useRef } from "react";
const MyDemo5 = () => {
const [counter, setCounter] = useState(0);
const cunterRef = useRef(0);
function asyncData() {
console.log("异步请求数据:" + cunterRef.current); //这里会使用 num 做一些处理,比如请求数据
}
function changeCounter() {
setCounter(10);
cunterRef.current = 10;
asyncData();
}
function pureChangeCounter() {
cunterRef.current = 20;
setCounter(20);
}
return (
<>
<button
onClick={() => {
changeCounter();
}}
>
点击我执行异步函数
</button>
<div>{counter}</div>
<button
onClick={() => {
pureChangeCounter();
}}
>
点击我只是单纯改变 data
</button>
</>
);
};
export default MyDemo5;
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
结果示例

如上图所示,点击两个按钮都可以改变 state 值,但是只有点击第一个按钮才会触发 asyncData 函数,并且第二个按钮改变的 state 值不会影响第一个按钮的逻辑。
# 2.2 usestate 函数式更新
如下面的代码 handleClickFn 中通过函数式改变 count,handleClick 是常规方法改变 count,多次点击按钮后,均是在 3s 之后改变 count,可以看出常规的方法只变化了一次,而函数式 usestate 则改变了多次,因为它可以获取之前的 state 值,也就是代码中的 prevCount 每次都是最新的值。
示例 2.3
import React, { useState } from "react";
function MyDemo6() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
setCount(count + 1);
}, 3000);
}
function handleClickFn() {
setTimeout(() => {
setCount((prevCount) => {
return prevCount + 1;
});
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>点击常规改变count</button>
<br />
<button onClick={handleClickFn}>点击函数式改变count</button>
</>
);
}
export default MyDemo6;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
结果示例

如上图所示,多次点击第一个按钮,页面在 3s 之后只变化 1 次,而多次点击第二个按钮,页面在 3s 之后就会变化多次。因为 setTimeout 是宏任务,多次点击按钮,在 3s 之后同时执行了多次改变数据,根据上述核心知识点“React 合成事件中改变状态是异步的,出于减少 render 次数,react 会收集所有状态变更,然后比对优化,最后做一次变更。”所知,页面只会改变 1 次数据,如果想改变多次,就要使用函数式改变数据的方式。
以上类似地在短时间内多次触发 setCount 函数,如下面代码所示:
import React, { useState } from "react";
function MyDemo7() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
function handleClickFn() {
setCount((prevCount) => {
return prevCount + 1;
});
setCount((prevCount) => {
return prevCount + 1;
});
setCount((prevCount) => {
return prevCount + 1;
});
}
return (
<>
Count: {count}
<br />
<button onClick={handleClick}>点击常规改变count</button>
<br />
<button onClick={handleClickFn}>点击函数式改变count</button>
</>
);
}
export default MyDemo7;
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
结果示例

从上图可以看出,点击第一个按钮,页面数值只会变化 1 次,而点击第二个按钮就会变化 3 次。
# 问题 3、首次执行时 useEffect 的时机
核心知识点 函数首次执行时 useEffect 的副作用的依赖参数为 false 还会执行吗?useEffect 第二个参数表示发生变化后执行第一个函数,即使设置初始值为 false,DOM 初次渲染完以后也会执行 useEffect。
如下代码,即使 flag 为 false,首次执行函数也会执行 useEffect
示例 3.1
import React, { useState, useEffect } from "react";
const MyDemo8 = () => {
const [flag, setFlag] = useState(false);
useEffect(() => {
console.log("执行了effect", flag); //即使一开始是false,渲染完dom后也会执行该副作用
}, [flag]);
const changeFlag = () => {
setFlag(true);
};
return (
<div className="container">
{console.log("渲染了组件")}
<div>页面flag{flag}</div>
<button onClick={changeFlag}>点击我切换flag</button>
</div>
);
};
export default MyDemo8;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
结果示例

如上图所示,即使刚开始我们设置了 flag 为 false,useEffect 也会在 render 之后执行一次,所以如果我们并不想让其一开始就执行的话,需要在 useEffect 中增加条件判断:
import React, { useState, useEffect } from "react";
const MyDemo9 = () => {
const [flag, setFlag] = useState(false);
useEffect(() => {
flag && console.log("执行了effect", flag); //即使一开始是false,渲染完dom后也会执行该副作用
}, [flag]);
const changeFlag = () => {
setFlag(true);
};
return (
<div className="container">
{console.log("渲染了组件")}
<div>页面flag{flag}</div>
<button onClick={changeFlag}>点击我切换flag</button>
</div>
);
};
export default MyDemo9;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 问题 4、组件中 useEffect 的执行顺序?
先不要看答案,想一想下面的代码 console.log 的顺序是什么?
示例 4.1
import React, { useState, useEffect } from "react";
export default function MyDemo10() {
console.log("init render");
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log("change");
setCounter(counter + 2);
}, 1000);
console.log("effect:", timer);
}, []);
console.log("before render");
return (
<div className="container">
{console.log("render...")}
<div className="el">{counter}</div>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
结果示例

- 首先按照顺序执行
init render---before render---render... - 渲染完 DOM 之后,执行 useEffect 中
effect,500ms 之后,change,然后改变 counter - 因为改变了 counter,所以重新渲染函数,
init render---before render---render...
示例 4.2 useEffect 中加入 return 之后呢?
import React, { useState, useEffect } from "react";
export default function MyDemo11() {
console.log("init render");
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log("change");
setCounter(counter + 2);
}, 10000);
console.log("effect:", timer);
return () => {
console.log("clear:", timer);
clearTimeout(timer);
};
}, [counter]);
console.log("before render");
return (
<div className="container">
{console.log("render...")}
<div className="el">{counter}</div>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
结果示例

- 首先按照顺序执行
init render---before render---render... - 渲染完 DOM 之后,执行 useEffect 中
effect,500ms 之后,change,然后改变 counter - 因为改变了 counter,所以重新渲染函数,
init render---before render---render... - 渲染完 DOM 之后,执行 useEffect 中 return,
clear---执行完 return 后在次执行effect,500ms 之后,change,然后改变 counter - 循环往复
示例 4.3 父子组件中的 useEffect, useLayoutEffect 执行顺序
import React, { useState, useEffect, useLayoutEffect } from "react";
const Cards = () => {
useEffect(() => {
console.log("子孙组件useEffect");
});
useLayoutEffect(() => {
console.log("子孙组件useLayoutEffect");
});
return <>{console.log("子孙组件render")}我是子孙组件</>;
};
const Child = (props) => {
useEffect(() => {
console.log("组件useEffect");
});
useLayoutEffect(() => {
console.log("组件useLayoutEffect");
});
console.log("组件");
return (
<div>
{console.log("组件render")}我是子组件{props.name}
<Cards />
</div>
);
};
const MyDemo12 = () => {
const [myName, SetMyName] = useState("初始值");
return (
<div>
<Child name={myName} />
<button onClick={() => SetMyName("新名字")}>点击</button>
</div>
);
};
export default MyDemo12;
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
结果示例

点击按钮,改变子组件的 props,可以看到 useLayoutEffect 总是比 useEffect 先执行,如下图所示,先执行完父子组件的 useLayoutEffect,才会执行父子组件的 useEffect:

# 问题 5、useCallback、useMemo 和 React.memo 的区别联系
useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调,第二个参数为要依赖的数据
共同作用 仅当依赖数据发生变化, 才会重新计算结果,也就是起到缓存的作用。
两者区别
- 1、useMemo 计算结果是 return 回来的值, 主要用于缓存计算结果的值,应用场景如:需要计算的状态,类似于 vue 中的 computed
- 2、useCallback 计算结果是 函数, 主要用于缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化,整个组件都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能和减少资源浪费。
形如:
const onShow = useMemo(() => {
return () => {
setShow((isShow) => !isShow);
};
}, []);
const onShow = useCallback(() => {
setShow((isShow) => !isShow);
}, []);
2
3
4
5
6
7
8
示例 5.1 父组件中 input 输入内容的时候会重新执行 Parent 函数式组件,就会重新渲染 Button 子组件
import React, { useState } from "react";
//子组件
const Button = () => {
console.log("我被重新渲染了");
return (
<div>
<button>点击按钮</button>
</div>
);
};
//父组件
const MyDemo13 = () => {
const [inputValue, setInputValue] = useState("");
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Button />
</div>
);
};
export default MyDemo13;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

如上图所示,在父组件的 input 输入框中输入文字,按钮子组件居然也重新渲染了。
示例 5.2 改进方式:使用 React.memo 函数包裹子组件
React.memo 仅检查 props 变更。如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
注意的是 默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
function ChildComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
返回 true 则不在渲染子组件,
否则返回 false 渲染子组件
*/
}
export default React.memo(ChildComponent, areEqual);
2
3
4
5
6
7
8
9
10
示例改为:
import React, { useState } from "react";
//子组件
const Button = React.memo(function Button() {
console.log("我被重新渲染了");
return (
<div>
<button>点击按钮</button>
</div>
);
});
//父组件
const MyDemo14 = () => {
const [inputValue, setInputValue] = useState("");
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Button />
</div>
);
};
export default MyDemo14;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

由于 Button 组件被 React.memo 包裹,比较了 Button 的 props 没有发生变化,所以改变父组件的时候,不会重新渲染子组件。
我们再改进一下,如果要求点击子组件 Button 的按钮,触发父组件中 onShow 方法,从而改变 state:isShow,隐藏子组件:
示例 5.3
import React, { useState } from "react";
const Button = React.memo(function Button(props) {
console.log("子组件被重新渲染了");
return (
<div>
<button onClick={props.onShow}>点击隐藏</button>
</div>
);
});
const MyDemo15 = () => {
const [inputValue, setInputValue] = useState("");
const [isShow, setIsShow] = useState(true);
const onShow = () => {
setIsShow((isShow) => !isShow);
};
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
{isShow && <Button onShow={onShow} />}
</div>
);
};
export default MyDemo15;
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

我们发现父组件中 input 输入内容会导致父组件函数重新渲染,因为这关系到了 React 是如何浅层比较的,在子组件中 onShow 是引用类型,所以他们是始终都不相等的,也就是[]===[]这样比较时始终返回 false,在基本数据类型比较时 React.memo 才会起作用。从而导致子组件 Button 也会重新渲染。那么我们该如何优化呢?
方法一:使用 React.memo 的第二个比较函数包裹子组件
先确认执行顺序,像剥洋葱一样,我们看下面的示例: 示例 5.4
import React, { useState, useEffect, useRef, useCallback } from "react";
function areEqual(prevProps, nextProps) {
console.log(prevProps.arrList);
console.log(nextProps.arrList);
if (JSON.stringify(prevProps.arrList) === JSON.stringify(nextProps.arrList)) {
console.log("true");
return true;
} else {
console.log("false");
return false;
}
}
const Button = React.memo(function Button(props) {
const prevCountRef = useRef();
console.log("渲染子组件");
useEffect(() => {
console.log("子组件执行一次useEffect");
}, []);
useEffect(() => {
if (props.arrList.length > 3) {
console.log("子组件useEffect");
}
}, [props.arrList]);
return (
<div>
<ul>
{props.arrList.map((item, index) => {
return <li key={index}>{item}</li>;
})}
</ul>
</div>
);
}, areEqual);
const MyDemo16 = () => {
const [inputValue, setInputValue] = useState("");
const [arrList, setarrList] = useState([]);
console.log("渲染父组件");
useEffect(() => {
console.log("父组件执行一次useEffect");
}, []);
useEffect(() => {
if (inputValue.length > 0) {
console.log("触发父组件useEffect");
const newList = [...arrList];
newList.push(inputValue);
setarrList(newList);
}
}, [inputValue]);
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Button arrList={arrList} />
</div>
);
};
export default MyDemo16;
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60

- 渲染父组件-->渲染子组件-->子组件执行 useEffect-->父组件执行 useEffect
- 父组件中输入内容后,inputValue 发生变化,所以会触发:
- 父组件重新渲染-->执行 areEqual 函数[返回 true](因为此时还没有触发父组件的 useEffect,所以没有改变 props)所以不渲染子组件-->父组件执行 useEffect,改变 arrList
- 重新渲染父组件-->执行 areEqual 函数[返回 false](因为此时已经触发父组件的 useEffect,所以改变 props 的 arrList)-->渲染子组件

确定好执行顺序之后,我们再来看优化的示例:
示例 5.5 如果传入的 props 是函数时,即使父组件中传入的 props.onVisible 一样,但是子组件得到的也是不一样的
import React, { useState } from "react";
function areEqual(prevProps, nextProps) {
if (prevProps == nextProps) {
console.log("true");
return true;
} else {
console.log("false");
return false;
}
}
const Button = React.memo(function Button(props) {
console.log("渲染子组件");
return (
<div>
<button onClick={props.onShow}>点击按钮</button>
</div>
);
}, areEqual);
const MyDemo17 = () => {
const [inputValue, setInputValue] = useState("");
const onShow = () => {
console.log("isShow");
return "hello";
};
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Button onShow={onShow} />
</div>
);
};
export default MyDemo17;
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

从上面可以看出,父组件输入内容后,父组件 state 发生变化,重新渲染父组件,则传递给子组件的函数也是重新渲染的,areEqual 函数打印的是 false。
当然我们可以修改 areEqual 函数,对比两次 props 传入的值是否一样,但是 props 数据较多的时候就有些不方便了。
function areEqual(prevProps, nextProps) {
if (JSON.stringify(prevProps) == JSON.stringify(nextProps)) {
console.log("true");
return true;
} else {
console.log("false");
return false;
}
}
2
3
4
5
6
7
8
9
这是 useCallback 或者 useMemo 就登场了:
示例 5.6 父组件使用 useCallback 或者 useMemo 控制父组件中函数的缓存
父组件:
const onShow = useCallback(() => {
console.log("isShow");
return "hello";
}, []);
2
3
4
或者
const onShow = useMemo(() => {
return () => {
console.log("isShow");
return "hello";
};
}, []);
2
3
4
5
6
示例 5.7 使用 useCallback 进行缓存函数示例
import React, { useState, useMemo, useCallback } from "react";
const Button = React.memo(function Button(props) {
console.log("子组件被重新渲染了");
return (
<div>
<button onClick={props.onShow}>点击隐藏</button>
</div>
);
});
const MyDemo18 = () => {
const [inputValue, setInputValue] = useState("");
const [isShow, setIsShow] = useState(true);
const onShow = useCallback(() => {
setIsShow((isShow) => !isShow);
}, [isShow]);
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
{isShow && <Button onShow={onShow} />}
</div>
);
};
export default MyDemo18;
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

从上图可以看出,父组件中 onShow 函数经过 useCallback 包裹后,子组件不再触发。
示例 5.8 类似的使用 useMemo 进行缓存
const onShow = useMemo(() => {
return () => {
setIsShow((isShow) => !isShow);
};
}, [isShow]);
2
3
4
5
注意 useMemo 用来返回缓存的变量;useCallBack 用来返回缓存的函数。
# 问题 6、如何获取上一时刻的 props 和 state?
核心知识点 使用 useRef 可以很好的保存变量,核心是在 render 之后执行 useEffect 保存当前的 state 和 props,然后下一次渲染子组件的时候,先执行自定义的 hook 函数,从而得到上一时刻的 state 和 props,待 render 之后再去执行 自定义的 hook 中的 useEffect 函数,从而更新保存的 useRef 中的变量,最终写成自定义 useHook 的形式:
示例 6.1 自定义的 Hook 函数,获取上一时刻的 props 和 state
import React, { useState, useEffect, useRef } from "react";
//获取上一时刻的props
const usePreProps = (props) => {
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = props;
});
return prevCountRef.current;
};
//获取上一时刻的state
const usePreData = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const ChildCom = (props) => {
const [num, setNum] = useState(0);
const mycounter = usePreProps(props.counter);
console.log("上一时刻的props", mycounter);
const preData = usePreData(num);
console.log("上一时刻的data", preData);
const changeNum = () => {
setNum(num + 1);
};
return (
<>
<div>子组件props:{props.counter}</div>
<div>子组件data:{num}</div>
<button onClick={changeNum}>点击改变state</button>
</>
);
};
const MyDemo19 = () => {
const [counter, SetCounter] = useState(0);
const changeCounter = () => {
SetCounter((preState) => {
return preState + 1;
});
};
return (
<>
<ChildCom counter={counter} />
<button onClick={changeCounter}>点击改变子组件props</button>
</>
);
};
export default MyDemo19;
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
47
48
49
50
51
52

点击两个按钮,可以得到上一时刻的 props 值和 state 值,核心就是使用了 useRef,并使用自定义 Hook 进行了封装;
示例 6.2 useEffect 中 return 返回的 props 是哪一次的?
import React, { useState, useEffect } from "react";
interface IChildDemoProps {
id: number;
}
function ChildDemo(props: IChildDemoProps) {
useEffect(() => {
console.log(props.id);
return () => {
console.log("clear", props.id);
};
});
return (
<div className="container">
<div className="el">{props.id}</div>
</div>
);
}
export default function MyBox() {
const [myId, setMyId] = useState(0);
return (
<>
<ChildDemo id={myId} />
<button
onClick={() => {
setMyId(myId + 1);
}}
>
点击me
</button>
</>
);
}
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

在 props 发生变化之后,每次执行 useEffect 前都会先执行 return 中的函数,所以 return 中的 props 都是上一次的 props
# 问题 7、如何使用“引用传递 Forwarding Refs”
引用传递(Ref forwading)是一种通过组件向子组件自动传递 引用 ref 的技术。比如某些 input 组件,需要控制其 focus,本来是可以使用 ref 来控制,但是因为该 input 已被包裹在组件中,这时就需要使用 Ref forward 来透过组件获得该 input 的引用。
import React, { forwardRef, useEffect, useRef } from "react";
const FocusInput = forwardRef(function FocusInput(props, ref) {
return <input type="text" ref={ref} defaultValue={props.inputDefault} />;
});
const MyDemo21 = () => {
const inputRef = useRef(null);
useEffect(() => {
if (!!inputRef.current) {
inputRef.current.focus();
}
});
return (
<div>
<p>forward ref</p>
<FocusInput ref={inputRef} inputDefault={"默认值"} />
</div>
);
};
export default MyDemo21;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

上述示例中,子组件使用了 forwardRef 进行包裹,则父组件可以通过 ref 透传对子组件的控制。也就是首次渲染子组件的时候,就会将光标放置在子组件的 input 输入框中。
# 7.1 父组件调用子组件的方法
比如,子组件是一个input输入框,组件内部有个x号可以清除输入的内容,同时要求在父组件中也可以清除子组件的内容,可以使用forwardRef和 useImperativeHandle,使得通过子组件抛出函数,父组件可以调用抛出的函数。
import {forwardRef,useState,useImperativeHandle,useRef} from 'react'
//子组件内容
const ParentCallChildrenFunOrigin =forwardRef((props, refparams)=>{
useImperativeHandle(refparams, () => {
return {
clearInput
}
},[])
const [inputValue,setInputValue] = useState("")
const clearInput = ()=>{
setInputValue("")
}
return <div>
<input type="text" value={inputValue} onChange={(e)=>{setInputValue(e.target.value)}}/>
<button onClick={clearInput}>子组件删除</button>
</div>
})
//父组件内容
const ParentComponent = ()=>{
const childRef = useRef<any>(null)
const parentClear = ()=>{
childRef.current?.clearInput()
}
return <div>
<ParentCallChildrenFunOrigin ref={childRef}/>
<button onClick={parentClear}>父组件删除</button>
</div>
}
export default ParentComponent;
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
# 问题 8、state 发生变化后更新问题
由于每次渲染 react 函数式组件,会产生闭包,所以更改 state 之后,如果没有在 html 中使用,哪怕是异步延迟获取 state,获取到也是当时函数中的 state,所以 isBindCopy 一直是 false。 如果在 html 中监听函数,比如点击函数由于 html 模版每次渲染不属于闭包,点击后触发的事件相当于新渲染的事件,获得的数据就是最新改版后的数据,则 isBindCopy 是 true。
- 还有解决方法是使用 useRef
- 或者使用 mobx 的全局状态管理
import { useState, useEffect } from "react";
function App() {
let [isBindCopy, setIsBindCopy] = useState("false");
useEffect(() => {
// 判断是否授权
setIsBindCopy("true");
asyncFun();
}, []);
const asyncFun = () => {
setTimeout(() => {
console.log(`asyncFun`, isBindCopy); //asyncFun false
}, 3000);
};
const clickFun = () => {
console.log(`clickFun`, isBindCopy); //clickFun true
};
return (
<>
<div onClick={clickFun}>按钮</div>
</>
);
}
export default App;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 问题9: 数据监听问题汇总
useRef变化到底是否可以监听?
- 1、点击按钮1,改变了
strRef.current,即时页面html用到了strRef.current,也无法使用useEffect监听到strRef.current发生了变化,并且html也不发生变化,也就是strRef.current本身并没有触发监听事件。 - 2、点击了按钮2,在改变了
strRef.current的同时,改变了useState的数据,则不但页面html发生了变化,且更新了strRef.current,监听到strRef.current的变化 - 3、点击了按钮4,虽然改变了state和
strRef.current,但是state没有在html中使用,但是只要改变了state,就会监听到strRef.current的更新,html中的strRef.current也会发生变化; - 4、先点击按钮1,再点击了按钮3,同点击按钮2,只要state发生了变化,就会触发
strRef.current的监听。
import React, { useEffect, useState, useRef } from 'react';
const EffectChangeFun = ()=>{
const strRef = useRef('false');
const [currStatus,setCurrStatus] = useState("one")
const [noUsedStatus,setNoUsedStatus] = useState("one")
//
useEffect(()=>{
console.log("strRef.current",strRef.current)
},[strRef.current])
//点击按钮1 只改变strRef.current
const changeBtns = () => {
strRef.current = 'true';
};
const changeStateBtns = ()=>{
strRef.current = 'true';
setCurrStatus("second")
}
const changeThreeBtns = ()=>{
setCurrStatus("three")
}
const changeForthBtns = ()=>{
setNoUsedStatus("forth")
}
return (
<div>
<p>{strRef.current}</p>
<button onClick={changeBtns}>按钮1</button>
<div>{currStatus}</div>
<button onClick={changeStateBtns}>按钮2</button>
<button onClick={changeThreeBtns}>按钮3</button>
<button onClick={changeForthBtns}>按钮4</button>
</div>
);
}
export default EffectChangeFun;
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
代码功能:
- 点击按钮后 5秒后执行setTimeOut
- 执行顺序是 2-3-1
- useRef不具有数据的监听功能 不能放在html中 也不能作为useEffect的监听项
- 没有在html中使用到的useState数据,异步获取无法获取到最新的值
import React, { useEffect, useState, useRef } from 'react';
const OrderDemo = () => {
const [dataStr, setDataStr] = useState('false');
const strRef = useRef('false');
useEffect(() => {
setDataStr('true');
setTimeout(() => {
//useState设置的值,如果没有在html中使用,即使改变了值,在异步函数中也无法获取更新后的值
console.log('1', dataStr, strRef.current); //1 false true
}, 5000);
}, []);
//不要使用useRef的值用来作为监听项,useRef的变化不会被监听到
useEffect(() => {
console.log('2', dataStr, strRef.current); //2 false false
}, [strRef.current]);
const changeBtns = () => {
strRef.current = 'true';
setDataStr('true');
console.log('3', dataStr, strRef.current); //3 true true
};
return (
<div>
<p>不要使用useRef的值放在html中,不会监听变化而重新渲染页面</p>
<button onClick={changeBtns}>点击改变{strRef.current}</button>
</div>
);
};
export default OrderDemo;
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
# 总结
好了,洋洋洒洒通过了 20+个示例,介绍了 React Hook 在日常开发中要注意的一些问题和遇到过的坑,夯实了基础,相信遇见类似的问题不再一头雾水。然而整个 React Hook 知识体系庞大,后面我们仍需要总结学习,以上权当抛砖引玉,欢迎各位小伙伴留言讨论。
2022 年 6 月 23 日更新
# 每次渲染都是独立的闭包
- 每一次渲染都有它自己的 Props 和 State
- 每一次渲染都有它自己的事件处理函数
- 当点击更新状态的时候,函数组件都会重新被调用,那么每次渲染都是独立的,取到的值不会受后面操作的影响
function Counter2() {
let [number, setNumber] = useState(0);
function alertNumber() {
setTimeout(() => {
// alert 只能获取到点击按钮时的那个状态
alert(number);
}, 3000);
}
return (
<>
<p>{number}</p>
<button onClick={() => setNumber(number + 1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 惰性初始化 state
- initialState 参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略
- 如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
function Counter5(props) {
console.log("Counter5 render");
// 这个函数只在初始渲染时执行一次,后续更新状态重新渲染组件时,该函数就不会再被调用
function getInitState() {
return { number: props.number };
}
let [counter, setCounter] = useState(getInitState);
return (
<>
<p>{counter.number}</p>
<button onClick={() => setCounter({ number: counter.number + 1 })}>
+
</button>
<button onClick={() => setCounter(counter)}>setCounter</button>
</>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用多个 Effect 实现关注点分离
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的 每一个 effect。
function FriendStatusWithCounter(props) {
//功能一
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
//功能二
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 在 useEffect 中调用用函数
要把该函数在 useEffect 中申明,不能放到外部申明,然后再在 useEffect 中调用,async 的调用
react.docschina.org/docs/hooks-…
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}
2
3
4
5
6
7
8
9
10
要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:
import React, { useRef, useEffect, useState } from "react";
const App = () => {
useEffect(() => {
const getUser = async () => {
const user = await getUser();
};
getUser();
}, []);
return <div>hello</div>;
};
export default App;
2
3
4
5
6
7
8
9
10
11
只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。下面这个案例有一个 Bug:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch("http://myapi/product" + productId); // 使用了 productId prop
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 🔴 这样是无效的,因为 `fetchProduct` 使用了 `productId`
// ...
}
2
3
4
5
6
7
8
9
10
11
12
推荐的修复方案是把那个函数移动到你的 effect 内部。这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
async function fetchProduct() {
const response = await fetch("http://myapi/product" + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
# usememo 和使用 useHook 获取立即生效的 data
下面示例中,点击按钮,ChildBox 自组件虽然没有改变 props,但是也会改变
import React, { useState, useCallback, useMemo } from "react";
import { Button } from "antd-mobile";
interface BoxIprops {
title: string;
}
const ChildBox: React.FC<BoxIprops> = (props) => {
const { title } = props;
console.log(title);
return <div>{title}</div>;
};
const Box = () => {
const [num, setNum] = useState(0);
const changeNum = () => {
const newNum = num + 1;
setNum(newNum);
console.log(num);
};
return (
<>
<h1>{num}</h1>
<ChildBox title="标题" />
<Button color="primary" onClick={changeNum}>
点击
</Button>
;
</>
);
};
export default Box;
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
# useState 函数式形式和变量形式区别
import { useState, useEffect } from "react";
import "./App.css";
//变量的形式:1、多个set会合并;2、每次取到的值是当前的data
const Box1 = () => {
const [num, setNum] = useState(0);
const add = () => {
setNum(num + 1);
setNum(num + 1);
setNum(num + 1);
};
return (
<>
<button onClick={add}>点击</button>
<div>{num}</div>
</>
);
};
//函数的形式:1、多个set不会合并;2、每次取到的值是上一时刻的data
const Box2 = () => {
const [num, setNum] = useState(0);
const add = () => {
setNum((num) => num + 1);
setNum((num) => num + 1);
setNum((num) => num + 1);
};
return (
<>
<button onClick={add}>点击</button>
<div>{num}</div>
</>
);
};
function App() {
return (
<div className="App">
<Box1 />
<Box2 />
</div>
);
}
export default App;
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