React Hooks 全解:零基础入门

2,754 阅读13分钟

一、 前言:

一篇完整的React Hooks入门全解,适合人群:完全零基础想要入门React hooks以及对React hooks一知半解想要有一个体系化的学习,那么看这一篇就够了(注:本文不涉及原理性的知识,只是包含React Hooks 使用方法与使用技巧以及注意事项)。掘金上的文章和官网我都有参考,案例你创建一个脚手架就能直接运行,方便你理解。

二、 Hooks 的由来

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? ——拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function。

你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗? ——拥有了Hooks,生命周期钩子函数可以先丢一边了。

你在还在为组件中的this指向而晕头转向吗? ——既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this。

Hooks的出现解决了 React 长久以来存在的一些问题:

  • 带组件状态的逻辑很难重用

为了解决这个问题,需要引入render props(渲染属性)higher-order components(高阶组件)这样的设计模式,如react-redux提供的connect方法。这种方案不够直观,而且需要改变组件的层级结构,极端情况下会有多个wrapper嵌套调用的情况。

Hooks可以在不改变组件层级关系的前提下,方便的重用带状态的逻辑。

  • 复杂组件难于理解

大量的业务逻辑需要放在componentDidMountcomponentDidUpdate等生命周期函数中,而且往往一个生命周期函数中会包含多个不相关的业务逻辑,如日志记录和数据请求会同时放在componentDidMount中。另一方面,相关的业务逻辑也有可能会放在不同的生命周期函数中,如组件挂载的时候订阅事件,卸载的时候取消订阅,就需要同时在componentDidMountcomponentWillUnmount中写相关逻辑。

Hooks可以封装相关联的业务逻辑,让代码结构更加清晰。

  • 难于理解的 Class 组件

JS 中的this关键字让不少人吃过苦头,它的取值与其它面向对象语言都不一样,是在运行时决定的。为了解决这一痛点,才会有箭头函数的this绑定特性。另外 React 中还有Class ComponentFunction Component的概念,什么时候应该用什么组件也是一件纠结的事情。代码优化方面,对Class Component进行预编译和压缩会比普通函数困难得多,而且还容易出问题。

Hooks可以在不引入 Class 的前提下,使用 React 的各种特性。

三、 什么是 Hooks

Hooks are functions that let you “hook into” React state and lifecycle features from function components

上面是官方解释。从中可以看出 Hooks 是函数,有多个种类,每个 Hook 都为Function Component提供使用 React 状态和生命周期特性的通道。Hooks 不能在Class Component中使用。

React 提供了一些预定义好的 Hooks 供我们使用,下面我们来详细了解一下。

四、 常用hooks

image.png

1. useState

(1). State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作
(2). 语法: const [xxx, setXxx] = React.useState(initValue)  
(3). useState()说明:
    参数: 第一次初始化指定的值在内部作缓存
    返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数
(4). setXxx()2种写法:
    setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
    setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值

先来看一个传统的Class Component:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

使用 State Hook 来改写会是这个样子:

import React, { useState } from 'react';

function Example() {
  // 定义一个 State 变量,变量值可以通过 setCount 来改变
  const [count, setCount] = useState(0);

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

可以看到useState的入参只有一个,就是 state 的初始值。这个初始值可以是一个数字、字符串或对象,甚至可以是一个函数。当入参是一个函数的时候,这个函数只会在这个组件初始渲染的时候执行:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

useState的返回值是一个数组,数组的第一个元素是 state 当前的值,第二个元素是改变 state 的方法。这两个变量的命名不需要遵守什么约定,可以自由发挥。要注意的是如果 state 是一个对象,setState 的时候不会像Class Component的 setState 那样自动合并对象。要达到这种效果,可以这么做:

setState(prevState => {
  // Object.assign 也可以
  return {...prevState, ...updatedValues};
});

从上面的代码可以看出,setState 的参数除了数字、字符串或对象,还可以是函数。当需要根据之前的状态来计算出当前状态值的时候,就需要传入函数了,这跟Class Component的 setState 有点像。

另外一个跟Class Component的 setState 很像的一点是,当新传入的值跟之前的值一样时(使用Object.is比较),不会触发更新。

  • 注1: 如果想要性能优化,比如初始时内部逻辑比较复杂,将useState初始值改成用函数返回的形式,这样只会在初始时解析一次,不会每次渲染重新解析

  • 注2: setState尽量也写成函数形式,写成对象形式有一些小小的缺陷

  • 注3: 如果state是一个对象,不能局部更新setState,因为setState不会帮我们合并属性

  • 注4: setState地址要变,如果setState地址不变,React会认为数据没有变化

  • 注5:

//你敢这么写,他就敢把age给你删掉
const [user,setUser] = useState({name:'Frank', age: 18})
const onClick = ()=>{
   setUser({
     name: 'Jack'
   })
}
//想要连续两次调用,一次加2,用函数写法,不能用对象写法
const [n, setN] = useState(0)
const onClick = ()=>{
  // setN(n+1)
  // setN(n+1) // 你会发现 n 不能加 2
  // 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将回调函数当做参数传递给 setState。
  setN(i=>i+1)
  setN(i=>i+1)
}
  • 注6:useState 返回的更新状态方法是异步的,要在下次重绘时才能获取新值。
const [count, setCount] = useState(0);
setCount(1);
console.log(count);  // 是 0 不是 1
  • 注7:复杂状态--不能直接修改,需要先拷贝
const [students, setStudents] = useState([
    { id: 110, name: "why", age: 18 },
  ])
  
   function incrementAgeWithIndex(index) {
    const newStudents = [...students];
    newStudents[index].age += 1;
    setStudents(newStudents);
  }

2. Effect Hook

解释这个 Hook 之前先理解下什么是副作用。网络请求、订阅某个模块或者 DOM 操作都是副作用的例子,Effect Hook 是专门用来处理副作用的。正常情况下,在Function Component的函数体中,是不建议写副作用代码的,否则容易出 bug。

1. useEffect 做了什么?

默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。

通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。

2. 为什么在组件内部调用 useEffect

将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect会在每次 DOM 渲染后执行,不会阻塞页面渲染。它同时具备componentDidMountcomponentDidUpdatecomponentWillUnmount三个生命周期函数的执行时机。

useEffect(
  () => {
    // 如果第二个参数为空数组 这里的代码块 等价于 componentDidMount,初次渲染时,会执行一次useEffect
    // do something...

    // return的写法 等价于 componentWillUnmount ,返回一个函数,React 将会在执行清除操作时调用它。
    return () => {
       // do something...
    };
  },
  // 依赖列表,当依赖的值有变更时候,执行副作用函数,等价于 componentDidUpdate,如果不传第二个参数,useEffect 会在初次渲染和每次更新时,都会执行。
  [ xxx,obj.xxx ]
);

注意1:这里不只是组件销毁时才会打印“执行清除操作”,每次重新渲染时也都会执行。 至于原因,我觉得官网解释的很清楚,请参考 : 为什么每次更新的时候都要运行 Effect

注意2:依赖列表是灵活的,有三种写法

  • 当数组为空 [ ],表示不会应为页面的状态改变而执行回调方法【即仅在初始化时执行,等价于componentDidMount】,
  • 当这个参数不传递,表示页面的任何状态一旦变更都会执行回调方法
  • 当数组非空,数组里的值一旦有变化,就会执行回调方法

下面的Class Component例子中,副作用代码写在了componentDidMountcomponentDidUpdate中:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

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

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

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

可以看到componentDidMountcomponentDidUpdate中的代码是一样的。而使用 Effect Hook 来改写就不会有这个问题:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);
      //这样写表示组件挂载和每次更新时调用
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

此外还有一些副作用需要组件卸载的时候做一些额外的清理工作的,例如订阅某个功能:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  }

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

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

componentDidMount订阅后,需要在componentWillUnmount取消订阅。使用 Effect Hook 来改写会是这个样子:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    
    // 返回一个函数来进行额外的清理工作:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

useEffect的返回值是一个函数的时候,React 会在下一次执行这个副作用之前执行一遍清理工作,整个组件的生命周期流程可以这么理解:

组件挂载 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 组件卸载

上文提到useEffect会在每次渲染后执行,但有的情况下我们希望只有在 state 或 props 改变的情况下才执行。如果是Class Component,我们会这么做:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

使用 Hook 的时候,我们只需要传入第二个参数:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改变的时候才执行 Effect

第二个参数是一个数组,可以传多个值,一般会将 Effect 用到的所有 props 和 state 都传进去。

当副作用只需要在组件挂载的时候和卸载的时候执行,第二个参数可以传一个空数组[],实现的效果有点类似componentDidMountcomponentWillUnmount的组合。

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

使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。

import React, { useState, useEffect } from "react";
function Example() {
  useEffect(() => {
    // 逻辑一
  });
  useEffect(() => {
    // 逻辑二
  });
   useEffect(() => {
    // 逻辑三
  });
  return (
    <div>
      useEffect的使用
    </div>
  );
}
export default Example;

Hook 允许我们按照代码的用途分离他们,  而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

4. useEffect中使用异步函数

useEffect是不能直接用 async await 语法糖的

/* 错误用法 ,effect不支持直接 async await*/
 useEffect(async ()=>{
        /* 请求数据 */
      const res = await getData()
 },[])

useEffect 的回调参数返回的是一个清除副作用的 clean-up 函数。因此无法返回 Promise,更无法使用 async/await

那我们应该如何让useEffect支持async/await呢?

方法一

  useEffect(() => {
    (async function getDatas() {
      await getData();
    })();
  }, []);

方法二

  useEffect(() => {
    const getDatas = async () => {
      const data = await getData();
      setData(data);
    };
    getDatas();
  }, []);

总结

(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
(2). React中的副作用操作:
        发ajax请求数据获取
        设置订阅 / 启动定时器
        手动更改真实DOM
(3). 语法和说明: 
    useEffect(() => { 
      // 在此可以执行任何带副作用操作(挂载+更新)
      return () => { // 在组件卸载前执行
        // 在此做一些收尾工作, 比如清除定时器/取消订阅等
      }
    }, [stateValue])
 // 第二个参数如果指定的是[], 回调函数只会在第一次render()后执行,相当于 componentDidMount()。
 //数组内的元素即是被监视的更新元素,如果第二个参数为空,则监视所有元素。
    
(4). 可以把 useEffect Hook 看做如下三个函数的组合
    componentDidMount()
    componentDidUpdate()
    componentWillUnmount() 

3. useLayoutEffect

useLayoutEffect的用法跟useEffect的用法是完全一样的,都可以执行副作用和清理操作。它们之间唯一的区别就是执行的时机。

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新 即useEffect不会阻塞浏览器的绘制任务,它在浏览器渲染完成,页面更新后才会执行。

useLayoutEffectcomponentDidMountcomponentDidUpdate的执行时机一样,会阻塞页面的渲染。如果在里面执行耗时任务的话,页面就会卡顿。

在绝大多数情况下,useEffectHook 是更好的选择。唯一例外的就是需要根据新的 UI 来进行 DOM 操作的场景。useLayoutEffect会保证在页面渲染前执行,也就是说页面渲染出来的是最终的效果。如果使用useEffect,页面很可能因为渲染了 2 次而出现抖动。

  • 注1:多个useEffect按序执行,但useLayoutEffect总会比useEffect先执行
  • 注2: useEffect会在页面渲染完成之后再执行,如下代码就会发生抖动,但如果用useLayoutEffect,会在页面还未渲染时就执行,就不会有抖动发生
//useEffect
import React, { useState, useEffect } from 'react'

export default function EffectCounterDemo() {
  const [count, setCount] = useState(10);

  useEffect(() => {
    if (count === 0) {
      setCount(Math.random() + 200)
    }
  }, [count]);

  return (
    <div>
      <h2>数字: {count}</h2>
      <button onClick={e => setCount(0)}>修改数字</button>
    </div>
  )
}

//useLayoutEffect
import React, { useState, useEffect, useLayoutEffect } from 'react'

export default function LayoutEffectCounterDemo() {
  const [count, setCount] = useState(10);

  useLayoutEffect(() => {
    if (count === 0) {
      setCount(Math.random() + 200)
    }
  }, [count]);

  return (
    <div>
      <h2>数字: {count}</h2>
      <button onClick={e => setCount(0)}>修改数字</button>
    </div>
  )
}

4. useContext

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树 的逐层传递 props

useContext可以很方便的去订阅 context 的改变,并在合适的时候重新渲染组件。我们先来熟悉下标准的 context API 用法:

const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间层组件
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 通过定义静态属性 contextType 来订阅
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

除了定义静态属性的方式,还有另外一种针对Function Component的订阅方式:

function ThemedButton() {
    // 通过定义 Consumer 来订阅
    return (
        <ThemeContext.Consumer>
          {value => <Button theme={value} />}
        </ThemeContext.Consumer>
    );
}

使用useContext来订阅,代码会是这个样子,没有额外的层级和奇怪的模式:

function ThemedButton() {
  const value = useContext(ThemeContext);
  return <Button theme={value} />;
}

在需要订阅多个 context 的时候,就更能体现出useContext的优势。传统的实现方式:

function HeaderBar() {
  return (
    <CurrentUser.Consumer>
      {user =>
        <Notifications.Consumer>
          {notifications =>
            <header>
              Welcome back, {user.name}!
              You have {notifications.length} notifications.
            </header>
          }
      }
    </CurrentUser.Consumer>
  );
}

useContext的实现方式更加简洁直观:

function HeaderBar() {
  const user = useContext(CurrentUser);
  const notifications = useContext(Notifications);

  return (
    <header>
      Welcome back, {user.name}!
      You have {notifications.length} notifications.
    </header>
  );
}

const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

别忘记 useContext 的参数必须是 context 对象本身

  • 正确:  useContext(MyContext)
  • 错误:  useContext(MyContext.Consumer)
  • 错误:  useContext(MyContext.Provider) 实例:
const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);
  //themes.light初始默认值
function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
//接收传递过来的themes.dark
function ThemedButton() {
const theme = useContext(ThemeContext); 
return (   
  <button style={{ background: theme.background, color: theme.foreground }}>   
        I am styled by theme context!    
  </button>  );
}

多个嵌套

//父
import React, { useState, createContext } from "react";
export const UserContext = createContext();
export const ThemeContext = createContext();

<UserContext.Provider value={{name: "why", age: 18}}>
  <ThemeContext.Provider value={{fontSize: "30px", color: "red"}}>
    <ContextHookDemo/>
  </ThemeContext.Provider>
</UserContext.Provider>
//子
import React, { useContext } from 'react';

import { UserContext, ThemeContext } from "../App";

export default function ContextHookDemo(props) {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);

  console.log(user, theme);
  return (
    <div>
      <h2>ContextHookDemo</h2>
    </div>
  )
}

5. useReducer

很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。 useReducer仅仅是useState的一种替代方案: 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分; 当然,useReducer的用法跟 Redux 非常相似,当 state 的计算逻辑比较复杂又或者需要根据以前的值来计算时,使用这个 Hook 比useState会更好。下面是一个例子:

案例1

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {...state,count: state.count + 1};
    case 'decrement':
      return {...state,count: state.count - 1};
    default:
       return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

案例2

import { useReducer } from "react";
const initialState = [
  { id: 1, name: "张三" },
  { id: 2, name: "李四" },
];

const reducer = (state: any, { type, payload }: any) => {
  switch (type) {
    case "add":
      return [...state, payload];
    case "remove":
      return state.filter((item: any) => item.id !== payload.id);
    case "update":
      return state.map((item: any) =>
        item.id === payload.id ? { ...item, ...payload } : item
      );
    case "clear":
      return [];
    default:
      throw new Error();
  }
};

const List = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      List: {JSON.stringify(state)}
      <button
        onClick={() =>
          dispatch({ type: "add", payload: { id: 3, name: "周五" } })
        }
      >
        add
      </button>
      <button onClick={() => dispatch({ type: "remove", payload: { id: 1 } })}>
        remove
      </button>
      <button
        onClick={() =>
          dispatch({ type: "update", payload: { id: 2, name: "李四-update" } })
        }
      >
        update
      </button>
      <button onClick={() => dispatch({ type: "clear" })}>clear</button>
    </>
  );
};
export default List;

案例3

共享一个reducer方法时,数据是不会共享的,它们只是使用了相同的Reducer的函数而已。 所以,useReducer只是useState的一种替代品,并不能替代Redux。 1641354702(1).png

//Home
import React, { useState, useReducer } from 'react';

import reducer from './reducer';

export default function Home() {
  const [state, dispatch] = useReducer(reducer, {counter: 0});

  return (
    <div>
      <h2>Home当前计数: {state.counter}</h2>
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
      <button onClick={e => dispatch({type: "decrement"})}>-1</button>
    </div>
  )
}

//Profile
import React, { useReducer } from 'react';

import reducer from './reducer';

export default function Profile() {
  const [state, dispatch] = useReducer(reducer, { counter: 0 });

  return (
    <div>
      <h2>Profile当前计数: {state.counter}</h2>
      <button onClick={e => dispatch({ type: "increment" })}>+1</button>
      <button onClick={e => dispatch({ type: "decrement" })}>-1</button>
    </div>
  )
}

//reducer
export default function reducer(state, action) {
  switch(action.type) {
    case "increment":
      return {...state, counter: state.counter + 1};
    case "decrement":
      return {...state, counter: state.counter - 1};
    default:
      return state;
  }
}

6. useCallback / useMemo / React.memo

1. React.memo

 import React from "react";

 function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      {/* 点击button,状态n每次更新,Child组件也会被跟着更新,即使他所依赖的状态m没有变化 */}
      {/* <Child data={m} /> */}
      {/* 点击button,状态n每次更新,Child2组件不会跟着更新,因为他所依赖的状态m没有变化  */}
      <Child2 data={m} />
    </div>
  );
}
function Child(props: any) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div>child: {props.data}</div>;
}
//将Child组件用React.memo包裹,就可以实现组件依赖的状态不变,组件就不会更新

const Child2 = React.memo(Child);
// const Child2 =memo((props) => {
//   console.log("child 执行了");
//   console.log("假设这里有大量代码");
//   return <div>child: {props.data}</div>;
// }) 
export default App;

//注意:这玩意有个bug,给子组件添加了监听函数之后一秒破功,因为父组件变化导致app函数重新渲染,导致生成新的onClickChild,虽然功能一样,但地址不一样

import React from "react";
function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClickChild = () => {
    console.log(m);
  };
  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child2 data={m} onClickChild={onClickChild} />
      {/* Child2 居然又执行了 */}
    </div>
  );
}
function Child(props: any) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div onClick={props.onClickChild}>child: {props.data}</div>;
}

const Child2 = React.memo(Child);
export default App;

2. useCallback

如何进行性能的优化呢?

而 React 给出的方案是useCallback Hook。

  • useCallback会返回一个函数的 memoized(记忆的)值;
  • 在依赖不变的情况下,它会返回相同的引用,避免子组件进行无意义的重复渲染:
// 除非 `a` 或 `b` 改变,否则不会变
const memoizedCallback = useCallback(
  () => {
   xxxx
  },
  [a, b],
);
  • useCallback在什么时候使用?
  • 场景: 在将一个组件中的函数, 传递给子元素进行回调使用时, 使用useCallback对函数进行处理.同一组件中使用不会有性能优化作用

解决方案

import React, { useCallback } from "react";
function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  // const onClickChild = () => {
  //   console.log(m);
  // }; 
  // 每一次都重新渲染
  const onClickChild = useCallback(() => {
    console.log(m);
  }, [m]);
  // m不变就不会重新渲染

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
    </div>
  );
}
function Child(props: any) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div onClick={props.onClick}>child: {props.data}</div>;
}
//将Child组件用React.memo包裹,就可以实现组件依赖的状态不变,组件就不会更新
const Child2 = React.memo(Child);
export default App;

通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;

3. useMemo

如何进行性能的优化呢?

  • useMemo返回的也是一个 memoized(记忆的)值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的; useMemo复杂计算的应用:
import React, {useState, useMemo} from 'react';

function calcNumber(count) {
  console.log("calcNumber重新计算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total;
}

export default function MemoHookDemo01() {
  const [count, setCount] = useState(10);
  const [show, setShow] = useState(true);

  // const total = calcNumber(count); 
  // 每次渲染都会重新调用函数
  
  const total = useMemo(() => {
    return calcNumber(count);
  }, [count]);

  return (
    <div>
      <h2>计算数字的和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}

useMemo传入子组件应用类型:

import React, { useState, memo, useMemo } from "react";

const HYInfo = memo((props) => {
  console.log("HYInfo重新渲染");
  return (
    <h2>
      名字: {props.info.name} 年龄: {props.info.age}
    </h2>
  );
});

export default function MemoHookDemo02() {
  console.log("MemoHookDemo02重新渲染");
  const [show, setShow] = useState(true);

  // const info = { name: "why", age: 18 };
  // 这样每次重新渲染时会定义一个新的info对象,导致子组件重新渲染

  // 解决方法1:将info放入useState中
  // 解决方法2:useMemo
  const info = useMemo(() => {
    return { name: "why", age: 18 };
  }, []);

  return (
    <div>
      <HYInfo info={info} />
      <button onClick={(e) => setShow(!show)}>show切换</button>
    </div>
  );
}

4. useCallback 和 useMemo 区别:

我们可以将 useMemo 的返回值定义为返回一个函数这样就可以变通的实现了 useCallback。

`useCallback(fn, deps)` 相当于 `useMemo(() => fn, deps)`

案例

import React, { useMemo } from "react";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClick2 = () => {
    setM(m + 1);
  };
  //useMemo实现函数的重用,接收一个函数,函数的返回值就是你要缓存的东西
  const onClickChild = useMemo(() => {
    return () => {
      console.log("on click child, m: " + m);
    };
  }, [m]); // 这里呃 [m] 改成 [n] 就会打印出旧的 m
 // const onClickChild = useCallback(() => {
 //console.log("on click child, m: " + m);
 //}, [m]); // 这里呃 [m] 改成 [n] 就会打印出旧的 m
  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
        <button onClick={onClick2}>update m {m}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
    </div>
  );
}

function Child(props: any) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div onClick={props.onClick}>child: {props.data}</div>;
}
//将Child组件用React.memo包裹,就可以实现组件依赖的状态不变,组件就不会更新
const Child2 = React.memo(Child);
export default App;

简单理解呢 useCallback 与 useMemo 一个缓存的是函数,一个缓存的是函数的返回的结果。useCallback 是来优化子组件的,防止子组件的重复渲染。useMemo 可以优化当前组件也可以优化子组件,优化当前组件主要是通过 memoize 来将一些复杂的计算逻辑进行缓存。当然如果只是进行一些简单的计算也没必要使用 useMemo。

7. useRef

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

1. useRef 获取dom

使用类组件

class App extends React.Component {
  refInput = React.createRef();
  componentDidMount() {
    this.refInput.current && this.refInput.current.focus();
  }
  render() {
    return <input ref={this.refInput} />;
  }
}

使用函数组件

我们用它来访问DOM,从而操作DOM,如点击按钮聚焦文本框:

const Index = () => {
  const inputEl = useRef(null);
  const handleFocus = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={handleFocus}>Focus</button>
    </>
  );
};

注意:返回的 ref 对象在组件的整个生命周期内保持不变。 它类似于一个 class 的实例属性,我们利用了它这一点。

刚刚举例的是访问DOM,那如果我们要访问的是一个组件,操作组件里的具体DOM呢?我们就需要用到 React.forwardRef 这个高阶组件,来转发ref:

  • 通过forwardRef可以将ref转发到子组件;
  • 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
const Index = () => {
  const inputEl = useRef(null);
  const handleFocus = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <Child ref={inputEl} />
      <button onClick={handleFocus}>Focus</button>
    </>
  );
};

const Child = forwardRef((props, ref) => {
  return <input ref={ref} />;
});

2. useRef 缓存数据

useRef还有一个很重要的作用就是缓存数据,我们知道useState ,useReducer 是可以保存当前的数据源的,但是如果它们更新数据源的函数执行必定会带来整个组件从新执行到渲染,如果在函数组件内部声明变量,则下一次更新也会重置,如果我们想要悄悄的保存数据,而又不想触发函数的更新,那么useRef是一个很棒的选择。

得到state 上一次的值

import React, { useRef, useState, useEffect } from "react";

export default function RefHookDemo02() {
  const [count, setCount] = useState(0);

  const numRef = useRef(count);

  // 界面渲染完成之后执行,得到当前的count值
  useEffect(() => {
    numRef.current = count;
  }, [count]);

  return (
    <div>
      {/* <h2>numRef中的值: {numRef.current}</h2>
      <h2>count中的值: {count}</h2> */}
      <h2>count上一次的值: {numRef.current}</h2>
      <h2>count这一次的值: {count}</h2>
      <button onClick={(e) => setCount(count + 10)}>+10</button>
    </div>
  );
}

count改变后,组件重新渲染,由于useRef 返回的 ref 对象在组件的整个生命周期内保持不变,所以显示结果和上次一致,当页面挂载完成,执行useEffect,得到当前count值赋给numRef.current,current的值发生改变,但是current值发生改变不会引起重新渲染,下一次点击渲染时得到state 上一次的值。 如果想要保存初始值,将useffect注掉就行--ref 对象在组件的整个生命周期内保持不变

8. useImperativeHandle

useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例值(典型的应用是向上传递 func)。在大多数情况下,应当避免使用ref这样的命令式代码。useImperativeHandle应当与forwardRef一起使用。

我们先来回顾一下ref和forwardRef结合使用:

  • 通过forwardRef可以将ref转发给子组件
  • 子组件拿到父组件创建的ref, 绑定到自己的某一个元素中
import React, { useRef, forwardRef } from 'react'

// 子:forwardRef可以将ref转发给子组件
const JMInput = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />
})
//父:
export default function ForwardDemo() {
  // forward用于获取函数式组件DOM元素
  const inputRef = useRef()
  const getFocus = () => {
    inputRef.current.focus()
  }

  return (
    <div>
      <button onClick={getFocus}>聚焦</button>
      <JMInput ref={inputRef} />
    </div>
  )
}

forwardRef的做法本身没有什么问题, 但是我们是将子组件的DOM直接暴露给了父组件:

  • 直接暴露给父组件带来的问题是某些情况的不可控
  • 父组件可以拿到DOM后进行任意的操作
  • 我们只是希望父组件可以操作的focus,其他并不希望它随意操作其他方法

useImperativeHandle(ref, createHandle, [deps])

  • 通过useImperativeHandle可以只暴露特定的操作
    1. 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
    2. 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
  • 作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
    1. 参数1: 父组件传递的ref属性
    2. 参数2: 返回一个对象, 以供给父组件中通过ref.current调用该对象中的方法
import React, { useRef, forwardRef, useImperativeHandle } from "react";
//子:
const JMInput = forwardRef((props, ref) => {
  const inputRef2 = useRef();
  // 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
  // 参数1: 父组件传递的ref属性
  // 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef2.current.focus();
    },
  }));
  return <input type="text" ref={inputRef2} />;
});
//父:
export default function ImperativeHandleDemo() {
  // useImperativeHandle 主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
  // 为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
  // 解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多
  const inputRef1 = useRef();

  return (
    <div>
      <button onClick={() => inputRef1.current.focus()}>聚焦</button>
      <JMInput ref={inputRef1} />
    </div>
  );
}

案例

import { useRef } from "react";
import FancyInput from "./child1";

function Foo() {
  const fancyInputRef = useRef<HTMLInputElement | null>(null);
  return (
    <>
      <span onClick={() => fancyInputRef.current?.focus()}>111</span>
      <FancyInput ref={fancyInputRef} />
    </>
  );
}
export default Foo;

import { forwardRef, useImperativeHandle, useRef } from "react";

const FancyInput = forwardRef((props: any, ref: any) => {
  const inputRef = useRef<HTMLInputElement | null>(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
  }));
  return <input ref={inputRef} />;
});
export default FancyInput;

9. 自定义 Hooks

还记得我们上一篇提到的 React 存在的问题吗?其中一点是:

  • 带组件状态的逻辑很难重用

通过自定义 Hooks 就能解决这一难题。 自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。

废话就不多说了,通过下方5个小案例了解自定义Hook的使用

案例1:认识自定义Hook

import React, { useEffect } from 'react';

export default function CustomLifeHookDemo01() {
  useEffect(() => {
    console.log(`CustomLifeHookDemo01组件被创建出来了`);

    return () => {
      console.log(`CustomLifeHookDemo01组件被销毁掉了`);
    }
  }, []);
  return (
    <div>
      <h2>CustomLifeHookDemo01</h2>
    </div>
  )
}

假设现在我有其他组件有类似的逻辑,简单的复制粘贴虽然可以实现需求,但太不优雅:

import React, { useEffect } from 'react';

const Home = (props) => {
  useEffect(() => {
    console.log(`Home组件被创建出来了`);

    return () => {
      console.log(`Home组件被销毁掉了`);
    }
  }, []);
  return <h2>Home</h2>
}

const Profile = (props) => {
  useEffect(() => {
    console.log(`Profile组件被创建出来了`);

    return () => {
      console.log(`Profile组件被销毁掉了`);
    }
  }, []);
  return <h2>Profile</h2>
}

export default function CustomLifeHookDemo01() {
  useEffect(() => {
    console.log(`CustomLifeHookDemo01组件被创建出来了`);

    return () => {
      console.log(`CustomLifeHookDemo01组件被销毁掉了`);
    }
  }, []);
  return (
    <div>
      <h2>CustomLifeHookDemo01</h2>
      <Home />
      <Profile />
    </div>
  )
}

这时我们就可以自定义一个 Hook 来封装订阅的逻辑:

import React, { useState, useEffect } from 'react';

function useLoggingLife(name) {
  useEffect(() => {
    console.log(`${name}组件被创建出来了`);

    return () => {
      console.log(`${name}组件被销毁掉了`);
    }
  }, [name]);
}

自定义 Hook 的命名有讲究,必须以use开头,在里面可以调用其它的 Hook。入参和返回值都可以根据需要自定义,没有特殊的约定。使用也像普通的函数调用一样,Hook 里面其它的 Hook(如useEffect)会自动在合适的时候调用:

import React, { useEffect } from 'react';

const Home = (props) => {
  useLoggingLife("Home");
  return <h2>Home</h2>
}

const Profile = (props) => {
  useLoggingLife("Profile");
  return <h2>Profile</h2>
}

export default function CustomLifeHookDemo01() {
  useLoggingLife("CustomLifeHookDemo01");
  return (
    <div>
      <h2>CustomLifeHookDemo01</h2>
      <Home />
      <Profile />
    </div>
  )
}

自定义 Hook 其实就是一个普通的函数定义,以use开头来命名也只是为了方便静态代码检测,不以它开头也完全不影响使用。在此不得不佩服 React 团队的巧妙设计。

案例2:自定义Hook-Context共享

//父
import React, { useState, createContext } from 'react';

import CustomContextShareHook from './zgc';


export const UserContext = createContext();
export const TokenContext = createContext();

export default function App() {

  return (
    <div>
      <UserContext.Provider value={{ name: "why", age: 18 }}>
        <TokenContext.Provider value="fdafdafafa">
          <CustomContextShareHook />
        </TokenContext.Provider>
      </UserContext.Provider>
    </div>
  )
}

//子--正常写法
import React, { useContext } from 'react';
import { UserContext, TokenContext } from "../App";

export default function CustomContextShareHook() {
  const user = useContext(UserContext);
  const token = useContext(TokenContext);
  console.log(user, token);

  return (
    <div>
      <h2>CustomContextShareHook</h2>
    </div>
  )
}

//子--自定义hook写法
import React from 'react';
import useUserContext from '../hooks/user-hook';

export default function CustomContextShareHook() {
  const [user, token] = useUserContext();
  console.log(user, token);

  return (
    <div>
      <h2>CustomContextShareHook</h2>
    </div>
  )
}

//自定义hook文件
import { useContext } from "react";
import { UserContext, TokenContext } from "../App";

function useUserContext() {
  const user = useContext(UserContext);
  const token = useContext(TokenContext);

  return [user, token];
}

export default useUserContext;

案例3:自定义Hook-获取滚动位置

//父
import React, { useState, createContext } from 'react';

import CustomScrollPositionHook from './zgc';


export default function App() {

  return (
    <div>
          <CustomScrollPositionHook />
    </div>
  )
}

//子--正常写法
import React, { useEffect, useState } from 'react'

export default function CustomScrollPositionHook() {
  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition(window.scrollY);
    }
    document.addEventListener("scroll", handleScroll);

    return () => {
      document.removeEventListener("scroll", handleScroll)
    }
  }, []);
  return (
    <div style={{ padding: "1000px 0" }}>
      <h2 style={{ position: "fixed", left: 0, top: 0 }}>CustomScrollPositionHook: {scrollPosition}</h2>
    </div>
  )
}

//子--自定义hook写法
import React from 'react'
import useScrollPosition from '../hooks/scroll-position-hook'

export default function CustomScrollPositionHook() {
  const position = useScrollPosition();

  return (
    <div style={{ padding: "1000px 0" }}>
      <h2 style={{ position: "fixed", left: 0, top: 0 }}>CustomScrollPositionHook: {position}</h2>
    </div>
  )
}

//自定义hook文件
import { useState, useEffect } from 'react';

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition(window.scrollY);
    }
    document.addEventListener("scroll", handleScroll);

    return () => {
      document.removeEventListener("scroll", handleScroll)
    }
  }, []);

  return scrollPosition;
}

export default useScrollPosition;

案例4:自定义Hook-localStorage存储

//父
import React, { useState, createContext } from 'react';

import CustomDataStoreHook from './zgc';

export default function App() {

  return (
    <div>
          <CustomDataStoreHook />
    </div>
  )
}

//子--正常写法
import React, { useState, useEffect } from "react";

export default function CustomDataStoreHook() {
  const [name, setName] = useState(() => {
    const name = JSON.parse(window.localStorage.getItem("name"));
    return name;
  });

  useEffect(() => {
    window.localStorage.setItem("name", JSON.stringify(name));
  }, [name]);

  return (
    <div>
      <h2>CustomDataStoreHook: {name}</h2>
      <button onClick={(e) => setName("wf")}>设置name</button>
    </div>
  );
}

//子--自定义hook写法
import React from "react";

import useLocalStorage from "../hooks/local-store-hook";

export default function CustomDataStoreHook() {
  const [name, setName] = useLocalStorage("name");

  return (
    <div>
      <h2>CustomDataStoreHook: {name}</h2>
      <button onClick={(e) => setName("zgc")}>设置name</button>
    </div>
  );
}

//自定义hook文件
import { useState, useEffect } from "react";

function useLocalStorage(key) {
  const [data, setData] = useState(() => {
    const data = JSON.parse(window.localStorage.getItem(key));
    return data;
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(data));
  }, [data, key]);

  return [data, setData];
}

export default useLocalStorage;

案例5

利用鼠标事件简单的实现了一个拖拽方格的效果:

import { useEffect, useRef } from "react";

export default function CustomHook() {
  const refs = useRef<HTMLDivElement | null>(null);
  let X: number,
    Y: number,
    isMove = false,
    left,
    top;
  //基于鼠标事件实现拖拽
  useEffect(() => {
    refs.current!.onmousedown = function (e) {
      isMove = true;
      X = e.clientX - refs.current!.offsetLeft;
      Y = e.clientY - refs.current!.offsetTop;
    };
    refs.current!.onmousemove = function (e) {
      if (isMove) {
        left = e.clientX - X;
        top = e.clientY - Y;
        refs.current!.style.top = top + "px";
        refs.current!.style.left = left + "px";
      }
    };
    refs.current!.onmouseup = function (e) {
      isMove = false;
    };
  }, []);
  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "100vh",
      }}
    >
      <div
        ref={refs}
        style={{
          width: "70px",
          height: "70px",
          backgroundColor: "#2C6CF9",
          position: "absolute",
        }}
      ></div>
    </div>
  );
}

那如果在其他页面也需要这个效果呢?😏于是,我们可以考虑把这段相同的逻辑封装起来,就像我们提取公共组件一般。来看下面这个例子:

import { useEffect, useRef } from "react";
function useDrop() {
  let refs = useRef<HTMLDivElement | null>(null);
  let X: number,
    Y: number,
    isMove = false,
    left,
    top;
  //基于鼠标事件实现拖拽
  useEffect(() => {
    const dom = refs.current;
    dom!.onmousedown = function (e: { clientX: number; clientY: number }) {
      isMove = true;
      X = e.clientX - dom!.offsetLeft;
      Y = e.clientY - dom!.offsetTop;
    };
    dom!.onmousemove = function (e: { clientX: number; clientY: number }) {
      if (isMove) {
        left = e.clientX - X;
        top = e.clientY - Y;
        dom!.style.top = top + "px";
        dom!.style.left = left + "px";
      }
    };
    dom!.onmouseup = function (e: any) {
      isMove = false;
    };
  }, []);
  return refs;
}
export default function CustomHook() {
  let refone = useDrop();
  let reftwo = useDrop();
  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "100vh",
      }}
    >
      <div
        ref={refone}
        style={{
          width: "70px",
          height: "70px",
          backgroundColor: "#2C6CF9",
          position: "absolute",
        }}
      ></div>
      <div
        ref={reftwo}
        style={{
          width: "70px",
          height: "70px",
          backgroundColor: "red",
          position: "absolute",
        }}
      ></div>
    </div>
  );
}

使用第三方库

React 官方提供的钩子都非常基础,实际业务中的逻辑其实很多都是可以复用的。实战中极力推荐大家使用第三方钩子库来提效。字节内部的项目目前还没有开源,这里先推荐阿里的 ahooks.js.org/

10. useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 Hook 的标签。
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。
因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

11. redux中的hooks: useSelector & useDispatch

// 传统react-redux
import React, { memo, useEffect } from "react";
import { connect } from "react-redux";

import { getTopBannerAction } from "./store/actionCreators";

function Recommend(props) {
  const { getBanners, topBanners } = props;

  // 调用映射到props的getBanners方法
  useEffect(() => {
    getBanners();
  }, [getBanners]);

  return <div>Recommend:{topBanners.length}</div>;
}

// 将下方的state与方法映射到props中
const mapStateToProps = (state) => {
  return {
    topBanners: state.recommend.topBanners,
  };
};
const mapDispatchToProps = (dispatch) => {
  return {
    getBanners: () => {
      dispatch(getTopBannerAction());
    },
  };
};
export default connect(mapStateToProps, mapDispatchToProps)(memo(Recommend));

//采用hooks
import React, { memo, useEffect } from "react";
import { useDispatch, useSelector, shallowEqual } from "react-redux";

import { getTopBannerAction } from "./store/actionCreators";

function Recommend(props) {
  // 使用hooks代替传统redux,获取数据和进行操作

  // 返回Redux store中对dispatch函数的引用,你可以根据需要使用它,而无需在mapDispatchToProps中使用
  const dispatch = useDispatch();

  // 从redux的store对象中提取数据(state)并解构,第一个参数是一个回调函数,参数为state,
  //第二个参数是shallowEqual--浅层比较函数,对其进行性能优化--当前组件没有用到的数据变化不需要更新组件
  //大多数时候都要用到第二个参数,而采用connect方法自带性能优化。
  const { topBanners } = useSelector(
    (state) => ({
      topBanners: state.recommend.topBanners,
    }),
    shallowEqual
  );
  // console.log(banners);
  useEffect(() => {
    dispatch(getTopBannerAction());
  }, [dispatch]);

  return <div>Recommend:{topBanners.length}</div>;
}

export default memo(Recommend);

五、 Hooks 使用规则

使用 Hooks 的时候必须遵守 2 条规则:

  • 只能在代码的第一层调用 Hooks,不能在循环、条件分支或者嵌套函数中调用 Hooks。 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
  • 只能在Function Component或者自定义 Hook 中调用 Hooks,不能在普通的 JS 函数中调用。 React官方文档中的Hook规则:《Hook 规则》,可以使用插件eslint-plugin-react-hooks来帮助我们检查这些规则

六、 使用 React Hooks 时要避免的错误

问题概览:

  1. 不要改变 hooks 的调用顺序;
  2. 不要使用旧的状态;
  3. 不要创建旧的闭包;
  4. 不要忘记清理副作用;
  5. 不要在不需要重新渲染时使用useState;
  6. 不要缺少useEffect依赖
  7. 不要将状态用于基础结构数据

1. 不要改变 hooks 的调用顺序

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:

// ------------
// 首次渲染
// ------------
useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm)     // 2. 添加 effect 以保存 form 操作
useState('Poppins')        // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle)     // 4. 添加 effect 以更新标题

// -------------
// 二次渲染
// -------------
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm)     // 2. 替换保存 form 的 effect
useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle)     // 4. 替换更新标题的 effect

// ...

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?

  // 🔴 在条件语句中使用 Hook 违反第一条规则
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

在第一次渲染中 name !== '' 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:

useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm)  // 🔴 此 Hook 被忽略!
useState('Poppins')        // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle)     // 🔴 3 (之前为 4)。替换更新标题的 effect 失败

React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。

这就是为什么 Hook 需要在我们组件的最顶层调用。 如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部

  useEffect(function persistForm() {
    // 👍 将条件判断放置在 effect 中
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

注意:如果使用了提供的 lint 插件,就无需担心此问题。  不过你现在知道了为什么 Hook 会这样工作,也知道了这个规则是为了避免什么问题。

2. 不要使用旧的状态

先来看一个计数器的例子:

import { useCallback, useState } from "react";

const Increaser = () => {
  const [count, setCount] = useState(0);

  const increase = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  const handleClick = () => {
    increase();
    increase();
    increase();
  };

  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Counter: {count}</div>
    </>
  );
};
export default Increaser;

这里的handleClick方法会在点击按钮后执行三次增加状态变量count的操作。那么点击一次是否会增加3呢?事实并非如此。点击按钮之后,count只会增加1。问题就在于,当我们点击按钮时,相当于下面的操作:

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};

当第一次调用setCount(count + 1)时是没有问题的,它会将count更新为1。接下来第2、3次调用setCount时,count还是使用了旧的状态(count为0),所以也会计算出count为1。发生这种情况的原因就是状态变量会在下一次渲染才更新。 ​

解决这个问题的办法就是,使用函数的方式来更新状态:

import { useCallback, useState } from "react";

const Increaser = () => {
  const [count, setCount] = useState(0);

  const increase = useCallback(() => {
    setCount((count) => count + 1);
  }, [count]);

  const handleClick = () => {
    increase();
    increase();
    increase();
  };

  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Counter: {count}</div>
    </>
  );
};
export default Increaser;

这样改完之后,React就能拿到最新的值,当点击按钮时,就会每次增加3。所以需要记住:如果要使用当前状态来计算下一个状态,就要使用函数的式方式来更新状态:

setValue(prevValue => prevValue + someResult)

3. 不要创建旧的闭包

众所周知,React Hooks是依赖闭包实现的。当使用接收一个回调作为参数的钩子时,比如:

useEffect(callback, deps)
useCallback(callback, deps)

此时,我们就可能会创建一个旧的闭包,该闭包会捕获过时的状态或者prop变量。这么说可能有些抽象,下面来看一个例子,这个例子中,useEffect每2秒会打印一次count的值:

const WatchCount = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setInterval(function log() {
      console.log(`Count: ${count}`);
    }, 2000);
  }, []);
  
  const handleClick = () => setCount(count => count + 1);
  
  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Count: {count}</div>
    </>
  );
}

最终的输出的结果如下:

image.png

可以看到,每次打印的count值都是0,和实际的count值并不一样。为什么会这样呢?

在第一次渲染时应该没啥问题,闭包log会将count打印出0。从第二次开始,每次当点击按钮时,count会增加1,但是setInterval仍然调用的是从初次渲染中捕获的count为0的旧的log闭包。log方法就是一个旧的闭包,因为它捕获的是一个过时的状态变量count。 ​

这里的解决方案就是,当count发生变化时,就重置定时器:

const WatchCount = () => {
  const [count, setCount] = useState(0);
  
  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count: ${count}`);
    }, 2000);
    return () => clearInterval(id);
  }, [count]);
  
  const handleClick = () => setCount(count => count + 1);
  
  return (
    <>
      <button onClick={handleClick}>+</button>
      <div>Count: {count}</div>
    </>
  );
}

这样,当状态变量count发生变化时,就会更新闭包。为了防止闭包捕获到旧值,就要确保在提供给hook的回调中使用的prop或者state都被指定为依赖性。

4. 不要忘记清理副作用

有很多副作用,比如fetch请求、setTimeout等都是异步的,如果不需要这些副作用或者组件在卸载时,不要忘记清理这些副作用。下面来看一个计数器的例子:

const DelayedIncreaser = () => {
  const [count, setCount] = useState(0);
  const [increase, setShouldIncrease] = useState(false);
  
  useEffect(() => {
    if (increase) {
      setInterval(() => {
        setCount(count => count + 1)
      }, 1000);
    }
  }, [increase]);
  
  return (
    <>
      <button onClick={() => setShouldIncrease(true)}>
        +
      </button>
      <div>Count: {count}</div>
    </>
  );
}

const MyApp = () => {
  const [show, setShow] = useState(true);
  
  return (
    <>
      {show ? <DelayedIncreaser /> : null}
      <button onClick={() => setShow(false)}>卸载</button>
    </>
  );
}

这个组件很简单,就是在点击按钮时,状态变量count每秒会增加1。当我们点击+按钮时,它会和我们预期的一样。但是当我们点击“卸载”按钮时,控制台就会出现警告:

image.png

修复这个问题只需要使用useEffect来清理定时器即可:

useEffect(() => {
    if (increase) {
      const id = setInterval(() => {
        setCount(count => count + 1)
      }, 1000);
      return () => clearInterval(id);
    }
  }, [increase]);

当我们编写一些副作用时,我们需要知道这个副作用是否需要清除。

5. 不要在不需要重新渲染时使用useState

在React hooks 中,我们可以使用useState hook来进行状态的管理。虽然使用起来比较简单,但是如果使用不恰当,就可能会出现意想不到的问题。来看下面的例子:

const Counter = () => {
  const [counter, setCounter] = useState(0);

  const onClickCounter = () => {
    setCounter(counter => counter + 1);
  };

  const onClickCounterRequest = () => {
    apiCall(counter);
  };

  return (
    <div>
      <button onClick={onClickCounter}>Counter</button>
      <button onClick={onClickCounterRequest}>Counter Request</button>
    </div>
  );
}

在上面的组件中,有两个按钮,第一个按钮会触发计数器加一,第二个按钮会根据当前的计数器状态发送一个请求。可以看到,状态变量counter并没有在渲染阶段使用。所以,每次点击第一个按钮时,都会有不需要的重新渲染。 ​

因此,当遇到这种需要在组件中使用一个变量在渲染中保持其状态,并且不会触发重新渲染时,那么useRef会是一个更好的选择,下面来对上面的例子使用useRef进行改编:

const Counter = () => {
  const counter = useRef(0);

  const onClickCounter = () => {
    counter.current++;
  };

  const onClickCounterRequest = () => {
    apiCall(counter.current);
  };

  return (
    <div>
      <button onClick={onClickCounter}>Counter</button>
      <button onClick={onClickCounterRequest}>Counter Request</button>
    </div>
  );
}

6. 不要缺少useEffect依赖

useEffect是React Hooks中最常用的Hook之一。默认情况下,它总是在每次重新渲染时运行。但这样就可能会导致不必要的渲染。我们可以通过给useEffect设置依赖数组来避免这些不必要的渲染。 ​

来看下面的例子:

const Counter = () => {
  const [count, setCount] = useState(0);

  const showCount = (count) => {
    console.log("Count", count);
  };

  useEffect(() => {
    showCount(count);
  }, []);

  return (
      <div>Counter: {count}</div>
  );
}

这个组件可能没有什么实际的意义,只是打印了count的值。这时就会有一个警告:

image.png

这里是说,useEffect缺少一个count依赖,这样是不安全的。我们需要包含一个依赖项或者移除依赖数组。否则useEffect中的代码可能会使用旧的值。

const Counter = () => {
  const [count, setCount] = useState(0);

  const showCount = (count) => {
    console.log("Count", count);
  };

  useEffect(() => {
    showCount(count);
  }, [count]);

  return (
      <div>Counter: {count}</div>
  );
}

如果useEffect中没有用到状态变量count,那么依赖项为空也会是安全的:

useEffect(() => {
  showCount(996);
}, []);

7.不要将状态用于基础结构数据

有一次,我需要在状态更新上调用副作用,在第一个渲染不用调用副作用。 useEffect(callback, deps)总是在挂载组件后调用回调函数:所以我想避免这种情况。

我找到了以下的解决方案

function MyComponent() {
  const [isFirst, setIsFirst] = useState(true);
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (isFirst) {
      setIsFirst(false);
      return;
    }
    console.log('The counter increased!');
  }, [count]);

  return (
    <button onClick={() => setCount(count => count + 1)}> Increase </button>
  );
}

状态变量isFirst用来判断是否是第一次渲染。一旦更新setIsFirst(false),就会出现另一个无缘无故的重新渲染。

保持count状态是有意义的,因为界面需要渲染 count 的值。 但是,isFirst不能直接用于计算输出。

是否为第一个渲染的信息不应存储在该状态中。 基础结构数据,例如有关渲染周期(即首次渲染,渲染数量),计时器ID(setTimeout()setInterval()),对DOM元素的直接引用等详细信息,应使用引用useRef()进行存储和更新。

我们将有关首次渲染的信息存储到 Ref 中:

 const isFirstRef = useRef(true);  const [count, setCount] = useState(0);

  useEffect(() => {
    if (isFirstRef.current) {
      isFirstRef.current = false;
      return;
    }
    console.log('The counter increased!');
  }, [count]);

  return (
    <button onClick={() => setCounter(count => count + 1)}>
 Increase
 </button>
  );
}

isFirstRef是一个引用,用于保存是否为组件的第一个渲染的信息。 isFirstRef.current属性用于访问和更新引用的值。

重要说明:更新参考isFirstRef.current = false不会触发重新渲染。