React 中常用的 Hook 使用方法
React 在 v16.8 的版本中推出了 React Hooks 新特性,本章主要记录下 React Hooks 大部分特性。在开始之前先对比一下原始的class写法和现在的 React Hooks 写法。
# 参考资料
React Hooks 官方文档:https://reactjs.org/docs/hooks-reference.html (opens new window)
# React Hooks 编写形式对比
先来写一个最简单的有状体组件,点我们点击按钮时,点击数不断增加为例:
# class组件编程写法
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
name: "alife"
};
}
render() {
const { count } = this.state;
return (
<div>
Count: {count}
<button onClick={() => this.setState({ count: count + 1 })}>+</button>
<button onClick={() => this.setState({ count: count - 1 })}>-</button>
</div>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# React Hooks 写法
import React, { useState } from "react";
function App() {
const [obj, setObject] = useState({
count: 0,
name: "alife"
});
return (
<div className="App">
Count: {obj.count}
<button onClick={() => setObject({ ...obj, count: obj.count + 1 })}>+</button>
<button onClick={() => setObject({ ...obj, count: obj.count - 1 })}>-</button>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# useState 保存组件状态
useState是react自带的一个hook函数,它的作用是用来声明状态变量。比如现在我们要声明多个状态,有年龄(age)、性别(sex)和工作(work)。代码可以这么写:
import React, { useState } from 'react';
function Example(){
const [ count , setCount ] = useState(0)
const [ age , setAge ] = useState(18)
const [ sex , setSex ] = useState('男')
const [ work , setWork ] = useState('前端程序员')
return (
<div>
<div>
<p>You clicked {count} times</p>
<button onClick={()=>{setCount(count+1)}}>click me</button>
</div>
<div>
<p>JSPang 今年:{age}岁</p>
<p>性别:{sex}</p>
<p>工作是:{work}</p>
</div>
</div>
)
}
export default Example;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
多状态声明的注意事项
在使用useState的时候只赋了初始值,并没有绑定任何的key,原因是React根据useState出现的顺序来执行的,意思就是useState不能在if...else...这样的条件语句中进行调用,必须要按照相同的顺序进行渲染。如果你还是不理解,你可以记住这样一句话就可以了:就是React Hooks不能出现在条件判断语句中,因为它必须有完全一样的渲染顺序。
# useEffect 处理副作用(代替常用生命周期函数)
函数组件能保存状态,但是对于异步请求,副作用的操作还是无能为力,所以 React 提供了 useEffect 来帮助开发者处理函数组件的副作用,在介绍新 API 之前,我们先来看看类组件是怎么做的:
import React, { Component } from "react";
class App extends Component {
state = {
count: 1
};
componentDidMount() {
const { count } = this.state;
document.title = "componentDidMount" + count;
this.timer = setInterval(() => {
this.setState(({ count }) => ({
count: count + 1
}));
}, 1000);
}
componentDidUpdate() {
const { count } = this.state;
document.title = "componentDidMount" + count;
}
componentWillUnmount() {
document.title = "componentWillUnmount";
clearInterval(this.timer);
}
render() {
const { count } = this.state;
return (
<div>
Count:{count}
<button onClick={() => clearInterval(this.timer)}>clear</button>
</div>
);
}
}
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
在例子中,组件每隔一秒更新组件状态,并且每次触发更新都会触发 document.title 的更新(副作用),而在组件卸载时修改 document.title(类似于清除),从例子中可以看到,一些重复的功能开发者需要在 componentDidMount 和 componentDidUpdate 重复编写,而如果使用 useEffect 则完全不一样。
import React, { useState, useEffect } from "react";
let timer = null;
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = "componentDidMount" + count;
},[count]);
useEffect(() => {
timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 一定注意下这个顺序:
// 告诉react在下次重新渲染组件之后,同时是下次执行上面setInterval之前调用
return () => {
document.title = "componentWillUnmount";
clearInterval(timer);
};
}, []);
return (
<div>
Count: {count}
<button onClick={() => clearInterval(timer)}>clear</button>
</div>
);
}
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
我们使用 useEffect 重写了上面的例子,useEffect 第一个参数接收一个函数,可以用来做一些副作用比如异步请求,修改外部参数等行为,而第二个参数称之为dependencies,是一个数组,如果数组中的值变化才会触发 执行useEffect 第一个参数中的函数。返回值(如果有)则在组件销毁或者调用函数前调用。
- 1.比如第一个 useEffect 中,理解起来就是一旦 count 值发生改变,则修改 documen.title 值;
- 2.而第二个 useEffect 中传递了一个空数组[],这种情况下只有在组件初始化或销毁的时候才会触发,用来代替 componentDidMount 和 componentWillUnmount,慎用;
还有另外一个情况,就是不传递第二个参数,也就是useEffect只接收了第一个函数参数,代表不监听任何参数变化。每次渲染DOM之后,都会执行useEffect中的函数。 基于这个强大 Hooks,我们可以模拟封装出其他生命周期函数,比如 componentDidUpdate 代码十分简单,例如:
function useUpdate(fn) {
// useRef 创建一个引用
const mounting = useRef(true);
useEffect(() => {
if (mounting.current) {
mounting.current = false;
} else {
fn();
}
});
}
2
3
4
5
6
7
8
9
10
11
# useContext 减少组件层级传值
useContext 是 React 帮你封装好的,用来处理多层级传递数据的方式,在以前组件树种,跨层级祖先组件想要给孙子组件传递数据的时候,除了一层层 props 往下透传之外,我们还可以使用 React Context API 来帮我们做这件事,举个简单的例子:
const { Provider, Consumer } = React.createContext(null);
function Bar() {
return <Consumer>{color => <div>{color}</div>}</Consumer>;
}
function Foo() {
return <Bar />;
}
function App() {
return (
<Provider value={"grey"}>
<Foo />
</Provider>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
通过 React createContext 的语法,在 APP 组件中可以跨过 Foo 组件给 Bar 传递数据。而在 React Hooks 中,我们可以使用 useContext 进行改造。
const colorContext = React.createContext("gray");
function Bar() {
const color = useContext(colorContext);
return <div>{color}</div>;
}
function Foo() {
return <Bar />;
}
function App() {
return (
<colorContext.Provider value={"red"}>
<Foo />
</colorContext.Provider>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
传递给 useContext 的是 context 而不是 consumer,返回值即是想要透传的数据了。用法很简单,使用 useContext 可以解决 Consumer 多状态嵌套的问题。
function HeaderBar() {
return (
<CurrentUser.Consumer>
{user =>
<Notifications.Consumer>
{notifications =>
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
}
}
</CurrentUser.Consumer>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
而使用 useContext 则变得十分简洁,可读性更强且不会增加组件树深度。
function HeaderBar() {
const user = useContext(CurrentUser);
const notifications = useContext(Notifications);
return (
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
);
}
2
3
4
5
6
7
8
9
10
# useReducer介绍和简单使用
# reducer到底是什么
reducer其实就是一个函数,这个函数接收两个参数,一个是状态,一个用来控制业务逻辑的判断参数。我们举一个最简单的例子:
function countReducer(state, action) {
switch(action.type) {
case 'add':
return state + 1;
case 'sub':
return state - 1;
default:
return state;
}
}
2
3
4
5
6
7
8
9
10
上面的代码就是Reducer,你主要理解的就是这种形式和两个参数的作用,一个参数是状态,一个参数是如何控制状态。
# useReducer的简单使用
useReducer 这个 Hooks 在使用上几乎跟 Redux/React-Redux 一模一样,唯一缺少的就是无法使用 redux 提供的中间件。我们用useReducer实现计数器的加减双向操作,示例如下:
import React, { useReducer } from 'react';
function ReducerDemo(){
const [ count , dispatch ] =useReducer((state,action)=>{
switch(action){
case 'add':
return state+1
case 'sub':
return state-1
default:
return state
}
},0)
return (
<div>
<h2>现在的分数是{count}</h2>
<button onClick={()=>dispatch('add')}>Increment</button>
<button onClick={()=>dispatch('sub')}>Decrement</button>
</div>
)
}
export default ReducerDemo
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# useReducer代替Redux实现共享数据
新建一个showArea.js文件
import React from 'react';
function ShowArea(){
return (<div style={{color:'blue'}}>字体颜色为blue</div>)
}
export default ShowArea
2
3
4
5
新建一个Buttons.js文件
import React from 'react';
function Buttons(){
return (
<div>
<button>红色</button>
<button>黄色</button>
</div>
)
}
export default Buttons
2
3
4
5
6
7
8
9
10
11
12
建立一个color.js文件,然后写入下面的代码:
import React, { createContext } from 'react';
export const ColorContext = createContext({})
export const Color = props=>{
return (
<ColorContext.Provider value={{color:"blue"}}>
{props.children}
</ColorContext.Provider>
)
}
2
3
4
5
6
7
8
9
10
11
代码中引入了createContext用来创建共享上下文ColorContext组件,然后我们要用{props.children}来显示对应的子组件。有了这个组件后,新建一个Example.js文件,让它可以共享状态。
import React, { useReducer } from 'react';
import ShowArea from './ShowArea';
import Buttons from './Buttons';
import { Color } from './color'; //引入Color组件
function Example(){
return (
<div>
<Color>
<ShowArea />
<Buttons />
</Color>
</div>
)
}
export default Example
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后再改写showArea.js文件,我们会引入useContext和在color.js中声明的ColorContext,让组件可以接收全局变量。
import React , { useContext } from 'react';
import { ColorContext } from './color';
function ShowArea(){
const {color} = useContext(ColorContext)
return (<div style={{color:color}}>字体颜色为{color}</div>)
}
export default ShowArea
2
3
4
5
6
7
8
9
这时候就通过useContext实现了状态的共享,可以到浏览器中看一下效果。
# useReducer代替Redux实现逻辑控制
我们只要在Color组件里使用useReducer,这样Color组件就有了那个共享状态和处理业务逻辑的能力,跟以前使用的Redux几乎一样了。我们来看代码:
import React, { createContext,useReducer } from 'react';
export const ColorContext = createContext({})
export const UPDATE_COLOR = "UPDATE_COLOR"
const reducer= (state,action)=>{
switch(action.type){
case UPDATE_COLOR:
return action.color
default:
return state
}
}
export const Color = props=>{
const [color,dispatch]=useReducer(reducer,'blue')
return (
<ColorContext.Provider value={{color,dispatch}}>
{props.children}
</ColorContext.Provider>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意
我们共享出去的状态变成了color和dispatch,如果不共享出去dispatch,你是没办法完成按钮的相应事件的。
通过dispatch修改状态,先引入useContext、ColorContext和UPDATE_COLOR,然后写onClick事件就可以了。代码如下:
import React ,{useContext} from 'react';
import {ColorContext,UPDATE_COLOR} from './color'
function Buttons(){
const { dispatch } = useContext(ColorContext)
return (
<div>
<button onClick={()=>{dispatch({type:UPDATE_COLOR,color:"red"})}}>红色</button>
<button onClick={()=>{dispatch({type:UPDATE_COLOR,color:"yellow"})}}>黄色</button>
</div>
)
}
export default Buttons
2
3
4
5
6
7
8
9
10
11
12
13
14
这样代码就编写完成了,用useContext和useReducer实现了Redux的效果。
# useCallback 记忆函数
在类组件中,我们经常犯下面这样的错误:
class App {
render() {
return (
<div>
<SomeComponent style={{ fontSize: 14 }} doSomething={ () => { console.log('do something'); }} />
</div>
);
}
}
2
3
4
5
6
7
8
9
这样写有什么坏处呢?一旦 App 组件的 props 或者状态改变了就会触发重渲染,即使跟 SomeComponent 组件不相关,由于每次 render 都会产生新的 style 和 doSomething(因为重新render前后, style 和 doSomething分别指向了不同的引用),所以会导致 SomeComponent 重新渲染,倘若 SomeComponent 是一个大型的组件树,这样的 Virtual Dom 的比较显然是很浪费的,解决的办法也很简单,将参数抽离成变量。
const fontSizeStyle = { fontSize: 14 };
class App {
doSomething = () => {
console.log('do something');
}
render() {
return <div>
<SomeComponent style={fontSizeStyle} doSomething={ this.doSomething } />
</div>;
}
}
2
3
4
5
6
7
8
9
10
11
在类组件中,我们还可以通过 this 这个对象来存储函数,而在函数组件中没办法进行挂载了。所以函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。
function App() {
const handleClick = () => {
console.log('Click happened');
}
return <SomeComponent onClick={handleClick}>Click Me</SomeComponent>;
}
2
3
4
5
6
提示
我们一般把函数式组件理解为class组件render函数的语法糖,所以每次重新渲染的时候,函数式组件内部所有的代码都会重新执行一遍。所以上述代码中每次render,handleClick都会是一个新的引用,所以也就是说传递给SomeComponent组件的props.onClick一直在变(因为每次都是一个新的引用),所以才会说这种情况下,函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。
可以通过 useCallback 获得一个记忆后的函数,可以有效避免不必要的 VDOM 渲染。
function App() {
const memoizedHandleClick = useCallback(() => {
console.log('Click happened')
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}
2
3
4
5
6
第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCallback 就会重新返回一个新的记忆函数提供给后面进行渲染。
# useMemo 记忆组件
useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的,前面使用 useCallback 的例子可以使用 useMemo 进行改写:
function App() {
const memoizedHandleClick = useMemo(() => () => {
console.log('Click happened')
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}
2
3
4
5
6
::: info 区别 useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你。所以在前面的例子中,可以返回 handleClick 来达到存储函数的目的。 ::: 所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 优化React Hooks程序性能,例如:
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
当 a/b 改变时,child1/child2 才会重新渲染。从例子可以看出来,只有在第二个参数数组的值发生变化时,才会触发子组件的更新,节省了性能的消耗。
# useRef获取DOM元素和保存变量
# useRef获取DOM元素
useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用,看个简单的例子:
import React, { useRef} from 'react';
function Example(){
const inputEl = useRef(null)
const onButtonClick=()=>{
inputEl.current.value="Hello ,JSPang"
console.log(inputEl) //输出获取到的DOM节点
}
return (
<>
{/*保存input的ref到inputEl */}
<input ref={inputEl} type="text"/>
<button onClick = {onButtonClick}>在input上展示文字</button>
</>
)
}
export default Example
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# useRef保存普通变量
利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离,看个简单的例子:
import React, { useRef ,useState,useEffect } from 'react';
function Example(){
const inputEl = useRef(null)
const onButtonClick=()=>{
inputEl.current.value="Hello ,useRef"
console.log(inputEl)
}
//-----------关键代码--------start
const [text, setText] = useState('jspang')
const textRef = useRef()
useEffect(()=>{
textRef.current = text;
console.log('textRef.current:', textRef.current)
})
//----------关键代码--------------end
return (
<>
{/*保存input的ref到inputEl */}
<input ref={inputEl} type="text"/>
<button onClick = {onButtonClick}>在input上展示文字</button>
<br/>
<br/>
<input value={text} onChange={(e)=>{setText(e.target.value)}} />
</>
)
}
export default Example
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
# useImperativeHandle 透传 Ref
通过 useImperativeHandle 用于让父组件获取子组件内的索引
import React, { useRef, useEffect, useImperativeHandle, forwardRef } from "react";
function ChildInputComponent(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => inputRef.current);
return <input type="text" name="child input" ref={inputRef} />;
}
const ChildInput = forwardRef(ChildInputComponent);
function App() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<ChildInput ref={inputRef} />
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
通过这种方式,App 组件可以获得子组件的 input 的 DOM 节点。
# useLayoutEffect 同步执行副作用
大部分情况下,使用 useEffect 就可以帮我们处理组件的副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行。
function App() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
const title = document.querySelector("#title");
const titleWidth = title.getBoundingClientRect().width;
console.log("useLayoutEffect");
if (width !== titleWidth) {
setWidth(titleWidth);
}
});
useEffect(() => {
console.log("useEffect");
});
return (
<div>
<h1 id="title">hello</h1>
<h2>{width}</h2>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
::: info useEffect和useLayoutEffect区别 简单来说就是调用时机不同,useLayoutEffect和原来componentDidMount&componentDidUpdate一致,在react完成DOM更新后马上同步调用的代码,会阻塞页面渲染。而useEffect是会在整个页面渲染完才会调用的代码。官方建议优先使用useEffect。useLayoutEffect在服务端渲染时会出现一个warning,要消除的话得用useEffect代替或者推迟渲染时机。 :::
- 02
- Node与GLIBC_2.27不兼容解决方案08-19
- 03
- Git清空本地文件跟踪缓存08-13