这是一篇关于本人 React Hooks 学习的总结文章,因为有一定 Vue 3 的编写经验,所以学起来不是那么吃力,下面一些总结仅代表个人看法。

React Hooks 对应 API

  1. useState
  2. useReducer
  3. useContext
  4. useEffect、useLayoutEffect
  5. useMemo、useCallback
  6. useRef、forwardRef、useImperativeHandle
  7. 自定义hook
  8. stale-closure

1.useState

1.1useState基础用法

用于变量声明,形式大致如下:

jsx
1
2
3
4
5
6
7
8
9
import React from 'react'

// String、Number、Boolean
const [s, setS] = React.useState('')
const [n, setN] = React.useState(0)
const [b, setB] = React.useState(false)
// Object、Array
const [user, setUser] = React.useState({ name: 'xxx' })
const [array, setArray] = React.useState([1, 2, 3])

1.2useState不可局部更新

如果state是一个对象,能否部分setState?答案是不行的,,演示如下 : useState-DEMO1

具体代码如下 :

jsx
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, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
const [user, setUser] = useState({ name: 'Howard', age: 18 })
// 由此可得出结论,每次点击事件会造成重新渲染页面,但每次的user对象却是一个新的对象。
console.log(user)
const onClick = () => {
// error
// 这种方式会丢失 age 属性
setUser({
name: 'John'
})
// solution
// setUser({
// ...state,
// name:'John'
// })
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App/>, rootElement);

1.3useState地址需要变换

如果state是一个对象,setState(state)如果state的地址不变,react就会认为数据没有变换,因此页面不会重新渲染对应值,演示如下 : useState-DEMO2

具体代码如下 :

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, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
const [user, setUser] = useState({ name: 'Howard', age: 18 })
const onClick = () => {
// error
// 这种方式不会重新渲染
user.name = 'xxx'
user.age = 11
console.log(user)
setUser(user)
// solution
// setUser(()=>({ name:'xxx',age:11}))
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App/>, rootElement);

1.4useState可以接受函数

useState 是可以接受如下写法的,演示如下 : useState-DEMO3

具体代码如下 :

jsx
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, { useState } from "react";
import ReactDOM from "react-dom";

// 以初始值提出到全局作用域
const intial = {
name: "Howard",
age: 18
};

function App() {
// 以函数形式传入并return出初始值
const [user, setUser] = useState(() => intial);
const onClick = () => {
setUser({
...user,
hobby: "play video game"
});
};
return (
<div className="App">
<h1>name : {user.name}</h1>
<h2>age : {user.age}</h2>
<h2>hobby : {user.hobby || "click show result"}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App/>, rootElement);

1.5**set**State可以接受函数

如果想对同一个值操作两次,可以使用如下方法,演示如下 : setState-DEMO1

具体代码如下 :

jsx
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
import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
const [n, setN] = useState(0);
const onClick = () => {
// 该操作 n 不能加 2
setN(n + 1)
setN(n + 1)
// solution
// 通过函数形式传入即可
// setN(i => i + 1);
// setN(x => x + 1);
};
return (
<div className="App">
<h1>n: {n}</h1>

<button onClick={onClick}>+2</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App/>, rootElement);

2.useReducer

总的来说,useReducer 是 useState 的复杂版,演示如下 : useReducer-DEMO1

具体代码如下 :

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
47
48
49
50
import React from "react";
import ReactDOM from "react-dom";

// step1 : 创建初始值
const initial = {
name: 'lzy',
age: 18
};

// step 2 : 创建所有操作
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return { ...state, age: state.age + action.number };
case 'multi':
return { ...state, age: state.age * action.number };
default:
throw new Error('unknown type')
}
};

function App() {
// step3 : 将初始值 initial 和 操作 reducer 传给 useReducer
const [state, dispatch] = React.useReducer(reducer, initial);
const { age, name } = state;
const onClick = () => {
// step4 : 调用写({type:'action'})方法
// dispatch 作为事件分发 给reducer提供方法并在其内部操作
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
const onClick3 = () => {
dispatch({ type: "multi", number: 2 });
};
return (
<div className="App">
<h1>name: {name}</h1>
<h1>age: {age}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
<button onClick={onClick3}>*2</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App/>, rootElement);

3.useContext

useContext 就是 上下文,注意:useContext 不是响应式的在一个模块将 Context内 的值改变,另一个模块不会感知变化。具体演示如下 : useContext-DEMO1

比较类似 vue 的 provide/inject ,通过 provide 一个对象/组件,在任意后代组件内部 inject 即可拿到provide对象的实例。

具体代码如下 :

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, { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";

// step 1 : 使用 createContext(initial) 创建上下文
const Context = createContext(null);

const App = () => {
console.log("App 执行了");
const [n, setN] = useState(0);
return (
// step 2 : 使用<Context.Provider> 圈定作用域
<Context.Provider value={{ n, setN }}>
<div className="App">
<Parent/>
</div>
</Context.Provider>
);
}

// step 3 : 在作用域内的组件 通过 useContext(Context) 来使用上下文
const Parent = () => {
const { n } = useContext(Context);
return (
<div>
我是爸爸 n: {n} <Child/>
</div>
);
}

// step 3 : 在作用域内的组件 通过 useContext(Context) 来使用上下文
const Child = () => {
const { n, setN } = useContext(Context);
const onClick = () => {
setN((i) => i + 1);
};
return (
<div>
我是儿子 我得到的 n: {n}
<button onClick={onClick}>+1</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App/>, rootElement);

4.useEffect、useLayoutEffect

原来 React 的 Class 写法比较类似 Vue 的 Optional 写法,会将各个生命周期编写在Class组件内,而改用 Hook 写法后,可以用 useEffect 去模拟各个生命周期,useEffect的执行时机是在浏览器渲染完成之后,每次render后执行。

而 useLayoutEffect 则是在 浏览器渲染完成之前执行,就比较类似 Vue 的 beforeMount/onBeforeMount(Vue 3 setup中的钩子) 钩子。

4.1useEffect作为componentDidMount使用

1
2
3
4
5
6
7
8
9
10
import React from "react";

const App = () => {
const [x, setX] = React.useState(0)
// 仅执行一次
React.useEffect(() => {
// 只会执行一次
console.log('执行了')
}, [])
}

4.2useEffect作为componentDidUpdate使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";

const App = () => {
const [x, setX] = React.useState(0)
// 仅执行一次
React.useEffect(() => {
console.log('执行了')
}, [x]) // x 变换就执行
return (
<div>
{/*每次点击按钮 就会执行一次 useEffecrt*/}
<button onClick={() => set(x + 1)}>+1</button>
</div>
)
}

4.3useEffect作为componentWillUnmount使用

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";

const App = () => {
const [x, setX] = React.useState(null)
// 不会造成内存泄露
React.useEffect(() => {
let n = setInterval(() => {
console.log('hi')
}, 1000)
// 清掉副作用
return () => {
window.clearInterval(n)
}
}, [])
return (
<div>
{/*每次点击按钮 就会执行一次 useEffecrt*/}
<button onClick={() => set(x + 1)}>+1</button>
</div>
)
}

总结 :

  1. 组件内存在多个 useEffect 的时候,是按照顺序执行的。
  2. 尽量避免使用 useLayoutEffect ,因为该 api 将渲染提前。
  3. useLayoutEffect 总是比 useEffect 先执行。
  4. useEffectuseLayoutEffect中的 deps 如果是一个对象,对象的地址没变,是不会执行回调的。

5.useMemo、useCallback

useMemouseCallback 的前置知识需要先理解 React.memo ,memo的作用是缓存一个组件,因为react默认有多余的render,在 props 不变的情况下,没有必要执行一个函数组件。

5.1未经过React.memo处理的demo

未经处理的demo,在点击1或2的时候,都会引起另一个child的re-render。具体演示如下 : memo、useCallback-DEMO1

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 { render } from "react-dom";
import { useState } from 'react'

const Child1 = props => {
console.log('child1 渲染了')
const { click, n } = props
return (
<button onClick={click}>{n}</button>
)
}

const Child2 = props => {
console.log('child2 渲染了')
const { click, m } = props
return (
<button onClick={click}>{m}</button>
)
}

const App = () => {
const [n, setN] = useState(0)
const [m, setM] = useState(0)
const click1 = () => setN(n + 1)
const click2 = () => setM(m + 1)
return (
<div className="App">
<Child1 n={n} click={click1}/>
<Child2 m={m} click={click2}/>
</div>
);
}

5.2经过React.memo搭配useCallback处理后的demo

经过 memo 处理后,在点击1或2的时候,仅仅只引起自身的的re-render。具体演示如下 : memo、useCallback-DEMO2

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
import { render } from "react-dom";
import React, { useState } from "react";

const Child1 = React.memo((props) => {
console.log("child1 渲染了");
const { click, n } = props;
return <button onClick={click}>{n}</button>;
});

const Child2 = React.memo((props) => {
console.log("child2 渲染了");
const { click, m } = props;
return <button onClick={click}>{m}</button>;
});

const App = () => {
const [n, setN] = useState(0);
const [m, setM] = useState(0);
// useCallback 对每个函数进行缓存 , 是useMemo的简化版
const click1 = React.useCallback(() => setN(n + 1), [n]);
// useMemo 与 useCallback基本一致 , 只是函数返回一个函数
const click2 = React.useMemo(() => () => setM(m + 1), [m]);
return (
<div className="App">
<Child1 n={n} click={click1}/>
<Child2 m={m} click={click2}/>
<div>
{n},{m}
</div>
</div>
);
};

总结:

  1. useMemo能做的事情更多,类似vue的 computed
    功能,useMemo能接受两个参数,第一个参数是一个函数,并通过计算得出一个state,第二个参数是这个state的依赖,例如:const num = useMemo(()=> m + n,[m ,n])
  2. 一般情况下,现需要用 React.memo 对组件进行包裹,内部再搭配使用useCallbackuseMemo,才会生效。
  3. 在实际项目中,尽量减少使用这三个API,因为你并不知道优化后到底是提升了性能还是损失了性能,详见When to useMemo and useCallback,该篇博客仔细分析了何时使用 useMemouseCallback

6.useRef、forwardRef、useImperativeHandle

7.自定义hook

8.stale-closure

__END__

o0Chivas0o
文章作者:o0Chivas0o
文章出处React Hooks 学习&总结
作者签名:Rich ? DoSomethingLike() : DoSomethingNeed()
版权声明:文章除特别声明外,均采用 BY-NC-SA 许可协议,转载请注明出处