# hooks

返回:react | 返回:大前端

How to fetch data with React Hooks? 为什么顺序调用对 React Hooks 很重要?
🐉 经典hooks详解
React Hooks 在蚂蚁金服的实践 彻底搞懂react hooks 用法
hooks与dva,涉及antd Form表单 facebook官方文档Hooks
🐉 常见问题 经典实例中学习hooks

16a04a93f73ff19e.jpg

React Hooks 是 React 库的新增功能,推出后席卷了 React 开发界。Hooks 允许你编写状态逻辑并使用其他 React 功能,同时无需编写类组件。你可以单独使用 Hooks 来制作自己的应用程序,这对 React 相关的从业者来说是一次重大变革。

  • 你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?
    拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function。
  • 你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?
    拥有了Hooks,生命周期钩子函数就可以成为弃婴,先丢一边了。
  • 你在还在为组件中的this指向而晕头转向吗?
    既然Class都丢掉了,哪里还有this?有生之年第一次不再需要面对this。
  • React Hooks作为近两年最流行的新特性,为广大程序员同胞带来了便利。

Hooks是可从函数组件“挂钩”到React状态和生命周期功能的函数
只在最顶层使用Hook,不在判断,循环语句中使用Hook
只在React函数中使用Hook

# 6个超实用的ReactHooks

back

# react-fetch-hook

# 准备开始

  • Yarn:yarn add react-fetch-hook
  • NPM:npm i react-fetch-hook --save
import React from "react";
import useFetch from "react-fetch-hook";
const Component = () => {
    const { isLoading, data } =useFetch("https://swapi.co/api/people/1");
    return isLoading ? (
        <div>Loading...</div>
        ) : (
        <UserProfile {...data} />);
    };
1
2
3
4
5
6
7
8
9
  • 多个请求:支持在同一文件/组件里的多个useFetch示例:
const result1 = useFetch("https://swapi.co/api/people/1");
const result2 = useFetch("https://swapi.co/api/people/2");
if(result1.isLoading && result2.isLoading) {
    return<div>Loading...</div>;
} return <div>
        <UserProfile {...result1.data}/>
        <UserProfile {...result2.data}/>
    </div>
1
2
3
4
5
6
7
8

# 用于决定窗口-滚动位置的ReactHooks:@rehooks/window-scroll-position

@rehooks/window-scroll-position是用于决定窗口-滚动位置的ReactHooks。当用户根据滚动动作对对象进行动画处理时,Hooks用处颇大。

  • 安装
    yarn add@rehooks/window-scroll-position
  • 使用
import useWindowScrollPositionfrom '@rehooks/window-scroll-position'
function MyComponent() {
// optionally you can pass options,those are default:
    let options = {
        throttle: 100,
    }
    let position = useWindowScrollPosition(options)
    // position == { x: 0, y: 0 }
    return <div />
}
1
2
3
4
5
6
7
8
9
10

# 负责同步本地存储的React Hooks:@rehooks/local-storage

back

@rehooks/local-storage 是负责同步本地存储的React Hooks。

  • 安装:
    Yarn:yarn add @rehooks/local-storage
    NPM:npm i @rehooks/local-storage
  • 使用

writeStorage:可以在应用程序中任何地方。

import React from 'react';
import { writeStorage } from '@rehooks/local-storage';
    let counter = 0;constMyButton = () => (
    <button onClick={_ =>writeStorage('i', ++counter)}>
        Click Me
    </button>
);
1
2
3
4
5
6
7

useLocalStorage:该组件将从本地储存中接收更新。

import React from 'react';
import { useLocalStorage } from '@rehooks/local-storage';
function MyComponent(){
    const [counterValue] =useLocalStorage('i'); // send the key to be tracked.
    return (
        <div>
            <h1>{counterValue}</h1>
        </div>
    );
}
1
2
3
4
5
6
7
8
9
10

deleteFromStorage: 也可以从本地储存中删除项目。

import { writeStorage,deleteFromStorage } from '@rehooks/local-storage';
    writeStorage('name', 'HomerSimpson');
    // Add an item firstdeleteFromStorage('name');
    // Deletes theitemconst thisIsNull = localStorage.getItem('name');
    // This is indeed null
1
2
3
4
5
  • 完整示例
import React from 'react';
import ReactDOM from 'react-dom';
import { writeStorage, deleteFromStorage, useLocalStorage } from'@rehooks/local-storage';
const startingNum = 0;
const App = () => {
    const [getNum, setNum] =useLocalStorage('num');
    return (
    <>
        <p>{getNum}</p>
        <button onClick={_ =>setNum(getNum ? getNum + 1 : 0)}>Increment</button>
        <button onClick={_ =>deleteFromStorage('num')}>Delete</button>
    </>
);

};// Assuming there is a div in index.html with an ID of 'root'
ReactDOM.render(<App />, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

@rehooks/local-storage API 文档可以从这里找到。

# react-use-form-state

# form_state准备开始

back

  • 首先,在项目中添加react-use-form-state :
    npm install --save react-use-form-state

请注意, react-use-form-state 需要 react@^16.8.0作为一个对等依赖。

# form_state基本用法

back

import { useFormState } from'react-use-form-state';
export default function SignUpForm({ onSubmit }) {
    const [formState, { text, email,password, radio }] = useFormState();
    function handleSubmit(e) {
    // ...
    } return (
        <form onSubmit={handleSubmit}>
            <input {...text('name')} />
            <input {...email('email')}required />
            <input {...password('password')}required minLength="8" />
            <input {...radio('plan','free')} />
            <input {...radio('plan','premium')} />
        </form>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

由以上例子,当用户填写表单时,formState对象内容如下:

{values: {
    name: 'Mary Poppins',
    email: 'mary@example.com',
    password: '1234',
    plan: 'free',
},touched: {
    name: true,
    email: true,
    password: true,
    plan: true,
},validity: {
    name: true,
    email: true,
    password: false,
    plan: true,
},errors: {
    password: 'Please lengthen this textto 8 characters or more',
},
clear: Function,
clearField: Function,
reset: Function,
resetField: Function,
setField: Function,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# form_state初始状态

back

useFormState 需要一个初始状态对象,其键与输入名称匹配。

export default functionRentCarForm() {
    const [formState, { checkbox, radio,select }] = useFormState({
    trip: 'roundtrip',
    type: ['sedan', 'suv', 'van'],
});
return (
    <form>
        <select {...select('trip')}>
            <optionvalue="roundtrip">Same Drop-off</option>
            <optionvalue="oneway">Different Drop-off</option>
        </select>
        <input {...checkbox('type','sedan')} />
        <input {...checkbox('type','suv')} />
        <input {...checkbox('type','van')} />
        <button>Submit</button>
    </form>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 是一个决定组件大小的React Hooks:@rehooks/component-size

back

@rehooks/component-size是一个决定组件大小的React Hooks。当调整响应式图片和组件大小需要局部刷新时十分有用。

  • 安装
    yarn add @rehooks/component-size
  • 使用
import { useRef } from 'react'
import useComponentSize from '@rehooks/component-size'function MyComponent() {
    let ref = useRef(null);
    let size = useComponentSize(ref);
    // size == { width: 100, height: 200 }
    let { width, height } = size;
    let imgUrl =`https://via.placeholder.com/${width}x${height}`;
    return (
        <div style={{ width: '100%',height: '100%' }}>
            <img ref={ref} src={imgUrl}/>
        </div>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 是一个用于监听元素外部点击的React Hooks:use-onclickoutside

back

这是一个用于监听元素外部点击的React Hooks。在与模态窗口、弹出窗口、警报或者配置文件导航的结合中,这种 Hooks十分有用。

  • 入门指南
    Yarn:yarn add use-onclickoutside
  • 用法
    创建一个ref并把它传递给useOnClickOutside Hooks。
import React, { useRef } from 'react';
import useOnClickOutside from 'use-onclickoutside';
export default function Modal({close }) {
    const ref = useRef(null);
    useOnClickOutside(ref, close);
    return <div ref={ref}>{'Modalcontent'}</div>
}
1
2
3
4
5
6
7

请注意useRef的用法,它是由标准React包提供的标准ReactHooks。
useRef返回一个可变的ref对象,其.current特性被初始化为传递的参数(initialValue)。这个返回的对象将持续存在于整个组件的生命周期。

# React Hooks 详解+项目实战

back

  • React 一直都提倡使用函数组件,但是有时候需要使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有
  • Hooks 是 React 16.8 新增的特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
  • 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以直接在现有的函数组件中使用 Hooks
  • 凡是 use 开头的 React API 都是 Hooks

类组件的不足

  • 状态逻辑难复用: 在组件之间复用状态逻辑很难,可能要用到 render props (渲染属性)或者 HOC(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余

  • 趋向复杂难以维护:

    • 在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug )
    • 类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件
  • this 指向问题:父组件给子组件传递函数时,必须绑定 this

    • react 中的组件四种绑定 this 方法的区别
class App extends React.Component<any, any> {
    handleClick2;

    constructor(props) {
        super(props);
        this.state = {
            num: 1,
            title: ' react study'
        };
        this.handleClick2 = this.handleClick1.bind(this);
    }

    handleClick1() {
        this.setState({
            num: this.state.num + 1,
        })
    }

    handleClick3 = () => {
        this.setState({
            num: this.state.num + 1,
        })
    };

    render() {
        return (<div>
            <h2>Ann, {this.state.num}</h2>
            <button onClick={this.handleClick2}>btn1</button>
            <button onClick={this.handleClick1.bind(this)}>btn2</button>
            <button onClick={() => this.handleClick1()}>btn3</button>
            <button onClick={this.handleClick3}>btn4</button>
        </div>)
    }
}
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

# useState & useMemo & useCallback

back

  • React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。
  • 通过在函数组件里调用它来给组件添加一些内部 state,React会 在重复渲染时保留这个 state
  • useState 唯一的参数就是初始 state
  • useState 会返回一个数组:一个 state,一个更新 state 的函数
    • 在初始化渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同
    • 你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并,而是直接替换
// 这里可以任意命名,因为返回的是数组,数组解构
const [state, setState] = useState(initialState);
1
2

实例

import React, { useState } from "react";
import ReactDOM from "react-dom";

function Child1(porps) {
  console.log(porps);
  const { num, handleClick } = porps;
  return (
    <div
      onClick={() => {
        handleClick(num + 1);
      }}
    >
      child
    </div>
  );
}

function Child2(porps) {
  // console.log(porps);
  const { text, handleClick } = porps;
  return (
    <div>
      child2
      <Grandson text={text} handleClick={handleClick} />
    </div>
  );
}

function Grandson(porps) {
  console.log(porps);
  const { text, handleClick } = porps;
  return (
    <div
      onClick={() => {
        handleClick(text + 1);
      }}
    >
      grandson
    </div>
  );
}

function Parent() {
  let [num, setNum] = useState(0);
  let [text, setText] = useState(1);

  return (
    <div>
      <Child1 num={num} handleClick={setNum} />
      <Child2 text={text} handleClick={setText} />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Parent />, rootElement);
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
51
52
53
54
55
56

# 每次渲染都是独立的闭包

back

  • 每一次渲染都有它自己的 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>
      </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 函数式更新

back

  • 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将回调函数当做参数传递给 setState。该回调函数将接收先前的 state,并返回一个更新后的值。
function Counter(){
    let [number,setNumber] = useState(0);
    function lazy(){
        setTimeout(() => {
            // setNumber(number+1);
            // 这样每次执行时都会去获取一遍 state,而不是使用点击触发时的那个 state
            setNumber(number=>number+1);
        }, 3000);
    }
    return (
        <>
           <p>{number}</p>
           <button onClick={()=>setNumber(number+1)}>+</button>
           <button onClick={lazy}>lazy</button>
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 惰性初始化state

back

  • 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>
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 性能优化

back

# Object.is (浅比较)

  • Hook 内部使用 Object.is 来比较新/旧 state 是否相等
  • 与 class 组件中的 setState 方法不同,如果你修改状态的时候,传的状态值没有变化,则不重新渲染
  • 与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果
function Counter(){
    const [counter,setCounter] = useState({name:'计数器',number:0});
    console.log('render Counter')
    // 如果你修改状态的时候,传的状态值没有变化,则不重新渲染
    return (
        <>
            <p>{counter.name}:{counter.number}</p>
            <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button>
            <button onClick={()=>setCounter(counter)}>++</button>
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12

# 减少渲染次数

back

import React,{useState,memo,useMemo,useCallback} from 'react';

function SubCounter({onClick,data}){
    console.log('SubCounter render');
    return (
        <button onClick={onClick}>{data.number}</button>
    )
}
SubCounter = memo(SubCounter);
export  default  function Counter6(){
    console.log('Counter render');
    const [name,setName]= useState('计数器');
    const [number,setNumber] = useState(0);
    const data ={number};
    const addClick = ()=>{
        setNumber(number+1);
    };
    return (
        <>
            <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
            <SubCounter data={data} onClick={addClick}/>
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 更深入的优化

back

  • useCallback:接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
  • useMemo:把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算
import React,{useState,memo,useMemo,useCallback} from 'react';

function SubCounter({onClick,data}){
    console.log('SubCounter render');
    return (
        <button onClick={onClick}>{data.number}</button>
    )
}
SubCounter = memo(SubCounter);

let oldData,oldAddClick;
export  default  function Counter2(){
    console.log('Counter render');
    const [name,setName]= useState('计数器');
    const [number,setNumber] = useState(0);
    // 父组件更新时,这里的变量和函数每次都会重新创建,那么子组件接受到的属性每次都会认为是新的
    // 所以子组件也会随之更新,这时候可以用到 useMemo
    // 有没有后面的依赖项数组很重要,否则还是会重新渲染
    // 如果后面的依赖项数组没有值的话,即使父组件的 number 值改变了,子组件也不会去更新
    //const data = useMemo(()=>({number}),[]);
    const data = useMemo(()=>({number}),[number]);
    console.log('data===oldData ',data===oldData);
    oldData = data;

    // 有没有后面的依赖项数组很重要,否则还是会重新渲染
    const addClick = useCallback(()=>{
        setNumber(number+1);
    },[number]);
    console.log('addClick===oldAddClick ',addClick===oldAddClick);
    oldAddClick=addClick;
    return (
        <>
            <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
            <SubCounter data={data} onClick={addClick}/>
        </>
    )
}
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

# useReducer

back

  • useReducer 和 redux 中 reducer 很像
  • useState 内部就是靠 useReducer 来实现的
  • useState 的替代方案,它接收一个形如 (state, action) => newState 的 reducer,并返回当前state 以及与其配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
let initialState = 0;
// 如果你希望初始状态是一个{number:0}
// 可以在第三个参数中传递一个这样的函数 ()=>({number:initialState})
// 这个函数是一个惰性初始化函数,可以用来进行复杂的计算,然后返回最终的 initialState
const [state, dispatch] = useReducer(reducer, initialState, init);
1
2
3
4
5
const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter(){
    const [state, dispatch] = useReducer(reducer, initialState,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# useContext

back

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
import React,{useState,memo,useMemo,useCallback,useReducer,createContext,useContext} from 'react';
import ReactDOM from 'react-dom';

const initialState = 0;
function reducer(state=initialState,action){
    switch(action.type){
        case 'ADD':
            return {number:state.number+1};
        default:
            break;
    }
}

const CounterContext = createContext();
// 第一种获取 CounterContext 方法:不使用 hook
function SubCounter_one(){
    return (
        <CounterContext.Consumer>
            {
                value=>(
                    <>
                    <p>{value.state.number}</p>
                    <button onClick={()=>value.dispatch({type:'ADD'})}>+</button>
                    </>
                )
            }
        </CounterContext.Consumer>
    )
}
// 第二种获取 CounterContext 方法:使用 hook ,更简洁
function SubCounter(){
    const {state, dispatch} = useContext(CounterContext);
    return (
        <>
            <p>{state.number}</p>
            <button onClick={()=>dispatch({type:'ADD'})}>+</button>
        </>
    )
}
/* class SubCounter extends React.Component{
    static contextTypes = CounterContext
    this.context =  {state, dispatch}
} */

function Counter(){
    const [state, dispatch] = useReducer((reducer), initialState, ()=>({number:initialState}));
    return (
        <CounterContext.Provider value={{state, dispatch}}>
            <SubCounter/>
        </CounterContext.Provider>
    )
}
ReactDOM.render(<Counter  />, document.getElementById('root'));
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
51
52
53

# useEffect

back

  • 使用class组件实现修改标题

  • 使用useEffect来实现修改标题

  • 清除副作用

  • 跳过effect进行性能优化

  • 使用多个Effect实现关注点分离

  • effect(副作用):指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。

  • 副作用操作可以分两类:需要清除的不需要清除的

  • 原先在函数组件内(这里指在 React 渲染阶段)改变 dom 、发送 ajax 请求以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性

  • useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API

  • useEffect 接收一个函数,该函数会在组件渲染到屏幕之后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容

  • 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

# 使用class组件实现修改标题

back

  • 在这个 class 中,我们需要在两个生命周期函数中编写重复的代码,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。我们希望它在每次渲染之后执行,但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。而 useEffect 会在第一次渲染之后和每次更新之后都会执行
class Counter extends React.Component{
    state = {number:0};
    add = ()=>{
        this.setState({number:this.state.number+1});
    };
    componentDidMount(){
        this.changeTitle();
    }
    componentDidUpdate(){
        this.changeTitle();
    }
    changeTitle = ()=>{
        document.title = `你已经点击了${this.state.number}`;
    };
    render(){
        return (
            <>
              <p>{this.state.number}</p>
              <button onClick={this.add}>+</button>
            </>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 使用useEffect来实现修改标题

back

  • 每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。
import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
    const [number,setNumber] = useState(0);
    // useEffect里面的这个函数会在第一次渲染之后和更新完成后执行
    // 相当于 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        document.title = `你点击了${number}`;
    });
    return (
        <>
            <p>{number}</p>
            <button onClick={()=>setNumber(number+1)}>+</button>
        </>
    )
}
ReactDOM.render(<Counter />, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 清除副作用

back

  • 副作用函数还可以通过返回一个函数来指定如何清除副作用,为防止内存泄漏,清除函数会在组件卸载前执行。如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除。
function Counter(){
    let [number,setNumber] = useState(0);
    let [text,setText] = useState('');
    // 相当于componentDidMount 和 componentDidUpdate
    useEffect(()=>{
        console.log('开启一个新的定时器')
        let $timer = setInterval(()=>{
            setNumber(number=>number+1);
        },1000);
        // useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
        // useEffect 在执行副作用函数之前,会先调用上一次返回的函数
        // 如果要清除副作用,要么返回一个清除副作用的函数
       /*  return ()=>{
            console.log('destroy effect');
            clearInterval($timer);
        } */
    });
    // },[]);//要么在这里传入一个空的依赖项数组,这样就不会去重复执行
    return (
        <>
          <input value={text} onChange={(event)=>setText(event.target.value)}/>
          <p>{number}</p>
          <button>+</button>
        </>
    )
}
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

# 跳过effect进行性能优化

back

  • 依赖项数组控制着 useEffect 的执行
  • 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
  • 推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。
function Counter(){
    let [number,setNumber] = useState(0);
    let [text,setText] = useState('');
    // 相当于componentDidMount 和 componentDidUpdate
    useEffect(()=>{
        console.log('useEffect');
        let $timer = setInterval(()=>{
            setNumber(number=>number+1);
        },1000);
    },[text]);// 数组表示 effect 依赖的变量,只有当这个变量发生改变之后才会重新执行 efffect 函数
    return (
        <>
          <input value={text} onChange={(event)=>setText(event.target.value)}/>
          <p>{number}</p>
          <button>+</button>
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 使用多个Effect实现关注点分离

back

  • 使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
// 类组件版
class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
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
  • Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的 每一个 effect。
// Hooks 版
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);
    };
  });
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# useLayoutEffect

🔝back

  • useEffect 在全部渲染完毕后才会执行
  • useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行
  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • 可以使用它来读取 DOM 布局并同步触发重渲染
  • 在浏览器执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新
  • 尽可能使用标准的 useEffect 以避免阻塞视图更新
function LayoutEffect() {
    const [color, setColor] = useState('red');
    useLayoutEffect(() => {
        alert(color);
    });
    useEffect(() => {
        console.log('color', color);
    });
    return (
        <>
            <div id="myDiv" style={{ background: color }}>颜色</div>
            <button onClick={() => setColor('red')}></button>
            <button onClick={() => setColor('yellow')}></button>
            <button onClick={() => setColor('blue')}></button>
        </>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# useRef useImperativeHandle

🔝back

# useRef

类组件、React 元素用 React.createRef,函数组件使用 useRef
useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)

const refContainer = useRef(initialValue);
1

useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref

import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
    let [number, setNumber] = useState(0);
    return (
        <>
            <Child />
            <button onClick={() => setNumber({ number: number + 1 })}>+</button>
        </>
    )
}
let input;
function Child() {
    const inputRef = useRef();
    console.log('input===inputRef', input === inputRef);
    input = inputRef;
    function getFocus() {
        inputRef.current.focus();
    }
    return (
        <>
            <input type="text" ref={inputRef} />
            <button onClick={getFocus}>获得焦点</button>
        </>
    )
}
ReactDOM.render(<Parent />, document.getElementById('root'));
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

# forwardRef

因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性

function Parent() {
    return (
        <>
         // <Child ref={xxx} /> 这样是不行的
            <Child />
            <button>+</button>
        </>
    )
}
1
2
3
4
5
6
7
8
9
  • forwardRef 可以在父组件中操作子组件的 ref 对象
  • forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上
  • 子组件接受 props 和 ref 作为参数
function Child(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}
Child = React.forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0);
  // 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
  // 只有当它被赋给某个元素的 ref 属性时,才会有值
  // 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
  // 那么父组件就可以操作子组件中的某个元素
  // 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的
  // 所以就需要用到 forwardRef 进行转发
  const inputRef = useRef();//{current:''}
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}
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

# useImperativeHandle

useImperativeHandle可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛
在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
父组件可以使用操作子组件中的多个 ref

import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';

function Child(props,parentRef){
    // 子组件内部自己创建 ref
    let focusRef = useRef();
    let inputRef = useRef();
    useImperativeHandle(parentRef,()=>(
      // 这个函数会返回一个对象
      // 该对象会作为父组件 current 属性的值
      // 通过这种方式,父组件可以使用操作子组件中的多个 ref
        return {
            focusRef,
            inputRef,
            name:'计数器',
            focus(){
                focusRef.current.focus();
            },
            changeText(text){
                inputRef.current.value = text;
            }
        }
    });
    return (
        <>
            <input ref={focusRef}/>
            <input ref={inputRef}/>
        </>
    )

}
Child = forwardRef(Child);
function Parent(){
  const parentRef = useRef();//{current:''}
  function getFocus(){
    parentRef.current.focus();
    // 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
    parentRef.current.addNumber(666);
    parentRef.current.changeText('<script>alert(1)</script>');
    console.log(parentRef.current.name);
  }
  return (
      <>
        <ForwardChild ref={parentRef}/>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}
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

# hooks与redux

react和redux建立通信的方式

老方案connect
新方案hook