React Hook常用场景

State Hook

更新

更新分为以下两种方式,即直接更新和函数式更新,其应用场景区分点在于:

  • 直接更新不依赖于旧state值

  • 函数式更新依赖于旧state值

1
2
3
4
5
// 直接更新
setState(newCount);

// 函数式更新
setState(prevCount => prevCount - 1);

实现合并

于class不同,useState不会自动合并并更新对象,而是直接替换它

1
2
3
4
setState(preVState=>{
//也可以使用Object.assign
return {...preVState,...updateValues}
})

Effect Hook

基础用法

可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate和 componentWillUnmount这三个函数的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Effect(){
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`You clicked ${count} times`);
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}

清除操作

为了防止内存泄漏,清除函数会在组件卸载前执行;如果组件多次渲染(通常如此),则在执行下一次effect之前,上一个effect就已经被清除,即先执行上一个effect中return的函数,再执行本effect中非return的函数

1
2
3
4
5
6
7
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});

执行时期

不同于componentDidMount或componentDidUpdate,使用useEffect的调度不会阻塞浏览器更新屏幕,这让你应用看起来响应更快。

性能优化

  • 依赖特定值通知React是否调用effect
1
2
3
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
  • 模拟componentDidMount,传递一个空数组,只运行一次的effect
1
2
3
useEffect(() => {
.....
}, []);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1、安装插件
npm i eslint-plugin-react-hooks --save-dev

// 2、eslint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

useContext

用于处理多级传递数据的方式,三步走

  • 使用 React Context API,在组件外部建立一个 Context
1
2
3
import React from 'react';
const ThemeContext = React.createContext(0);
export default ThemeContext;
  • 使用 Context.Provider提供了一个 Context 对象,这个对象可以被子组件共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState } from 'react';
import ThemeContext from './ThemeContext';
import ContextComponent1 from './ContextComponent1';

function ContextPage () {
const [count, setCount] = useState(1);
return (
<div className="App">
<ThemeContext.Provider value={count}>
<ContextComponent1 />
</ThemeContext.Provider>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

export default ContextPage;
  • useContext()钩子函数用来引入 Context 对象,并且获取到它的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 子组件,在子组件中使用孙组件
import React from 'react';
import ContextComponent2 from './ContextComponent2';
function ContextComponent () {
return (
<ContextComponent2 />
);
}
export default ContextComponent;


// 孙组件,在孙组件中使用 Context 对象值
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ContextComponent () {
const value = useContext(ThemeContext);
return (
<div>useContext:{value}</div>
);
}
export default ContextComponent;

useReducer

基础用法

比 useState 更适用的场景:例如 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
31
import React, { useReducer } from 'react';
interface stateType {
count: number
}
interface actionType {
type: string
}
const initialState = { count: 0 };
const reducer = (state:stateType, action:actionType) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
};
const UseReducer = () => {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<div className="App">
<div>useReducer Count:{state.count}</div>
<button onClick={() => { dispatch({ type: 'decrement' }); }}>useReducer 减少</button>
<button onClick={() => { dispatch({ type: 'increment' }); }}>useReducer 增加</button>
</div>
);
};

export default UseReducer;

惰性初始化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
31
32
33
34
35
36
interface stateType {
count: number
}
interface actionType {
type: string,
paylod?: number
}
const initCount =0
const init = (initCount:number)=>{
return {count:initCount}
}
const reducer = (state:stateType, action:actionType)=>{
switch(action.type){
case 'increment':
return {count: state.count + 1}
case 'decrement':
return {count: state.count - 1}
case 'reset':
return init(action.paylod || 0)
default:
throw new Error();
}
}
const UseReducer = () => {
const [state, dispatch] = useReducer(reducer,initCount,init)

return (
<div className="App">
<div>useReducer Count:{state.count}</div>
<button onClick={()=>{dispatch({type:'decrement'})}}>useReducer 减少</button>
<button onClick={()=>{dispatch({type:'increment'})}}>useReducer 增加</button>
<button onClick={()=>{dispatch({type:'reset',paylod:10 })}}>useReducer 增加</button>
</div>
);
}
export default UseReducer;

Memo

仅仅解决父组件没有传参给子组件的情况以及父组件传简单类型的参数给子组件的情况(例如 string、number、boolean等);如果有传复杂属性应该使用 useCallback(回调事件)或者 useMemo(复杂属性)

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

// 子组件
const ChildComp = () => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
const [count, setCount] = useState(0);

return (
<div className="App">
<div>hello world {count}</div>
<div onClick={() => { setCount(count => count + 1); }}>点击增加</div>
<MemoChildComp/>
</div>
);
};

export default Parent;

useMemo

假设以下场景,父组件在调用子组件时传递 info 对象属性,点击父组件按钮时,发现控制台会打印出子组件被渲染的信息。

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
import React, { memo, useState } from 'react';

// 子组件
const ChildComp = (info:{info:{name: string, age: number}}) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = { name, age };

return (
<div className="App">
<div>hello world {count}</div>
<div onClick={() => { setCount(count => count + 1); }}>点击增加</div>
<MemoChildComp info={info}/>
</div>
);
};

export default Parent;

分析原因:

  • 点击父组件按钮,触发父组件重新渲染;
  • 父组件渲染,const info = { name, age } 一行会重新生成一个新对象,导致传递给子组件的 info 属性值变化,进而导致子组件重新渲染。

解决:

使用 useMemo 将对象属性包一层,useMemo 有两个参数:

  • 第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象;
  • 第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。
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
import React, { memo, useMemo, useState } from 'react';

// 子组件
const ChildComp = (info:{info:{name: string, age: number}}) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);

// 使用 useMemo 将对象属性包一层
const info = useMemo(() => ({ name, age }), [name, age]);

return (
<div className="App">
<div>hello world {count}</div>
<div onClick={() => { setCount(count => count + 1); }}>点击增加</div>
<MemoChildComp info={info}/>
</div>
);
};

export default Parent;

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
30
import React, { memo, useMemo, useState } from 'react';

// 子组件
const ChildComp = (props:any) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = useMemo(() => ({ name, age }), [name, age]);
const changeName = () => {
console.log('输出名称...');
};

return (
<div className="App">
<div>hello world {count}</div>
<div onClick={() => { setCount(count => count + 1); }}>点击增加</div>
<MemoChildComp info={info} changeName={changeName}/>
</div>
);
};

export default Parent;

分析下原因:

  • 点击父组件按钮,改变了父组件中 count 变量值(父组件的 state 值),进而导致父组件重新渲染;
  • 父组件重新渲染时,会重新创建 changeName 函数,即传给子组件的 changeName 属性发生了变化,导致子组件渲染;

解决: 修改父组件的 changeName 方法,用 useCallback 钩子函数包裹一层, useCallback 参数与 useMemo 类似

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
import React, { memo, useCallback, useMemo, useState } from 'react';

// 子组件
const ChildComp = (props:any) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = useMemo(() => ({ name, age }), [name, age]);
const changeName = useCallback(() => {
console.log('输出名称...');
}, []);

return (
<div className="App">
<div>hello world {count}</div>
<div onClick={() => { setCount(count => count + 1); }}>点击增加</div>
<MemoChildComp info={info} changeName={changeName}/>
</div>
);
};

export default Parent;

useRef

指向 dom 元素

如下所示,使用 useRef 创建的变量指向一个 input 元素,并在页面渲染后使 input 聚焦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useRef, useEffect } from 'react';
const Page1 = () => {
const myRef = useRef<HTMLInputElement>(null);
useEffect(() => {
myRef?.current?.focus();
});
return (
<div>
<span>UseRef:</span>
<input ref={myRef} type="text"/>
</div>
);
};

export default Page1;

存放变量

useRef 在 react hook 中的作用, 正如官网说的, 它像一个变量, 类似于 this , 它就像一个盒子, 你可以存放任何东西. createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用,如下例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useRef, useEffect, useState } from 'react';
const Page1 = () => {
const myRef2 = useRef(0);
const [count, setCount] = useState(0)
useEffect(()=>{
myRef2.current = count;
});
function handleClick(){
setTimeout(()=>{
console.log(count); // 3
console.log(myRef2.current); // 6
},3000)
}
return (
<div>
<div onClick={()=> setCount(count+1)}>点击count</div>
<div onClick={()=> handleClick()}>查看</div>
</div>
);
}

export default Page1;

useImperativeHandle

使用场景:通过 ref 获取到的是整个 dom 节点,通过 useImperativeHandle 可以控制只暴露一部分方法和属性,而不是整个 dom 节点。

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect,这里不再举例。

  • useLayoutEffect 和平常写的 Class 组件的 componentDidMount 和 componentDidUpdate 同时执行;
  • useEffect 会在本次更新完成后,也就是第 1 点的方法执行完成后,再开启一次任务调度,在下次任务调度中执行 useEffect;