Skip to content

🎙️ 前言

相传, react 17 出了一个很强大的功能, 也就是 react hooks 。实际上, react hooks 有点类似与 vue3composition API ,都是为了提升开发效率而出现。

那么,在下面的文章中,将从 0到1 开始,带大家了解 react hooks ,以及一些常用的 API

废话不多说,开始 react-hook 之旅叭~

一、📻 概述

1、关于 React Hooks

  • React Hooks 是一个可选功能,通常用 class 组件 来和它做比较;
  • 100% 向后兼容,没有破坏性改动
  • 不会取代 class 组件,尚无计划要移除 class 组件。

2、认识 React Hooks

(1)回顾 React 函数式组件

我们先来回顾下 class 组件函数组件,具体如下:

class 组件:

js
// class组件
class List extends React.Component {
  constructor() {
    super(props);
  }
  render() {
    const { List } = this.props;

    return (
      <ul>
        {list.map((item, index) => {
          return (
            <li key={item.id}>
              <span>{item.title}</span>
            </li>
          );
        })}
      </ul>
    );
  }
}
// class组件
class List extends React.Component {
  constructor() {
    super(props);
  }
  render() {
    const { List } = this.props;

    return (
      <ul>
        {list.map((item, index) => {
          return (
            <li key={item.id}>
              <span>{item.title}</span>
            </li>
          );
        })}
      </ul>
    );
  }
}

函数组件:

js
// 函数组件
function List(props) {
  const { list } = props;

  return (
    <ul>
      {list.map((item, index) => {
        return (
          <li key={item.id}>
            <span>{item.title}</span>
          </li>
        );
      })}
    </ul>
  );
}
// 函数组件
function List(props) {
  const { list } = props;

  return (
    <ul>
      {list.map((item, index) => {
        return (
          <li key={item.id}>
            <span>{item.title}</span>
          </li>
        );
      })}
    </ul>
  );
}

(2)函数组件的特点

函数组件的特点是:

  • 没有组件实例;
  • 没有生命周期;
  • 没有 statesetState ,只能接收 props

(3)class 组件的问题

上面我们说到了函数组件是一个纯函数,只能接收 props ,没有任何其他功能。而 class 组件拥有以上功能,但是呢,class 组件会存在以下问题:

  • 大型组件很难拆分和重构,很难测试(即 class 不易拆分);
  • 相同业务逻辑,分散到各个方法中,逻辑混乱;
  • 复用逻辑变得复杂,如 MixinsHOCRender Props

因此,有了以上问题的出现,也就有了 React Hooks

(4)React 组件

  • React 组件更易于用函数来表达:

  • React 提倡函数式编程,即 view=fn(props)

  • 函数更灵活,更易拆分,更易测试

  • 函数组件太简单,需要增强能力 ——因此,有了 React Hooks

二、🪕 几种 Hooks

1、State Hook🗞️

(1)让函数组件实现 state 和 setState

  • 默认函数组件是没有 state 的;
  • 函数组件是一个纯函数,执行完即销毁,无法存储 store
  • 需要 State Hook ,即把 store 功能 "钩" 到纯函数中。

(2)举例阐述

假设我们现在要通过点击按钮,来修改设定的某个值。现在我们用 hooks 中的 useState 来处理。具体代码如下:

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

function ClickCounter() {
  // 数组的解构
  // useState 就是一个 Hook “钩”,最基本的一个 Hook
  const [count, setCount] = useState(0); // 传入一个初始值,初始值可以是数值/字符串/数组/对象等类型

  const [name, setName] = useState('星期一研究室');

  // const arr = useState(0)
  // const count = arr[0]
  // const setCount = arr[1]

  function clickHandler() {
    setCount(count + 1);
    setName(name + '2021');
  }

  return (
    <div>
      <p>
        你点击了 {count} 次 {name}
      </p>
      <button onClick={clickHandler}>点击</button>
    </div>
  );
}

export default ClickCounter;
import React, { useState } from 'react';

function ClickCounter() {
  // 数组的解构
  // useState 就是一个 Hook “钩”,最基本的一个 Hook
  const [count, setCount] = useState(0); // 传入一个初始值,初始值可以是数值/字符串/数组/对象等类型

  const [name, setName] = useState('星期一研究室');

  // const arr = useState(0)
  // const count = arr[0]
  // const setCount = arr[1]

  function clickHandler() {
    setCount(count + 1);
    setName(name + '2021');
  }

  return (
    <div>
      <p>
        你点击了 {count} 次 {name}
      </p>
      <button onClick={clickHandler}>点击</button>
    </div>
  );
}

export default ClickCounter;

此时浏览器的显示效果如下:

useState

在上面的代码中, count 是一个 state 的值, setCount 是修改 state 的一个函数。同理, name 也是一个 state 的值, setName 是修改 name 值得一个函数。

如果使用 hooks 来修改 state 值的话,那么我们只需要以 const [count, setCount] = useState(0) 这种形式去进行,便可修改最后的值,而不需要像 class 组件那么复杂的去使用。


对于上面这个功能来说,如果使用 class 组件来实现的话,具体代码如下:

js
import React from 'react';

class ClickCounter extends React.Component {
  constructor() {
    super();

    // 定义 state
    this.state = {
      count: 0,
      name: '星期一研究室',
    };

    this.clickHandler = this.clickHandler.bind(this);
  }
  render() {
    return (
      <div>
        <p>
          你点击了 {this.state.count} 次 {this.state.name}
        </p>
        <button onClick={this.clickHandler}>点击</button>
      </div>
    );
  }
  clickHandler() {
    // 修改 state
    this.setState({
      count: this.state.count + 1,
      name: this.state.name + '2021',
    });
  }
}

export default ClickCounter;
import React from 'react';

class ClickCounter extends React.Component {
  constructor() {
    super();

    // 定义 state
    this.state = {
      count: 0,
      name: '星期一研究室',
    };

    this.clickHandler = this.clickHandler.bind(this);
  }
  render() {
    return (
      <div>
        <p>
          你点击了 {this.state.count} 次 {this.state.name}
        </p>
        <button onClick={this.clickHandler}>点击</button>
      </div>
    );
  }
  clickHandler() {
    // 修改 state
    this.setState({
      count: this.state.count + 1,
      name: this.state.name + '2021',
    });
  }
}

export default ClickCounter;

大家可以看到,如果使用 class 组件来解决的话,那么需要先定义 state ,然后再定义一个函数,再使用 setState 才能去修改值,这样似乎还麻烦了点。

相信到这里,大家已经感受到 hooks 的快乐之处了。

下面,我们来总结关于 useState 的一些知识点。

(3)useState 使用总结

  • useState(xxx) 传入初始值,返回数组 [state, setState]
  • 通过 state 获取值;
  • 通过 setState(xxx) 修改值。

(4)Hooks 命名规范

  • 规定所有的 Hooks 都要使用 use 开头,如 useXxx
  • 自定义 Hook 也要以 use 开头;
  • Hooks 的地方,尽量不要使用 useXxx 写法,不然容易造成误解。

2、Effect Hook🗞️

(1)让函数组件模拟生命周期

  • 默认函数组件没有生命周期
  • 函数组件是一个纯函数,执行完即销毁,自己无法实现生命周期;
  • 使用 Effect Hook 把生命周期 “钩” 到纯函数中。

(2)举例阐述

同样地,我们用于 useState 相同的例子,来体验下 useEffect

我们先在 src|components 下建立一个文件,命名为 LiftCycles.js具体代码如下:

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

function LifeCycles() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 模拟 class 组件的 DidMount 和 DidUpdate
  useEffect(() => {
    console.log('在此发送一个 ajax 请求');
  });

  function clickHandler() {
    setCount(count + 1);
    setName(name + '2020');
  }

  return (
    <div>
      <p>
        你点击了 {count} 次 {name}
      </p>
      <button onClick={clickHandler}>点击</button>
    </div>
  );
}

export default LifeCycles;
import React, { useState, useEffect } from 'react';

function LifeCycles() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 模拟 class 组件的 DidMount 和 DidUpdate
  useEffect(() => {
    console.log('在此发送一个 ajax 请求');
  });

  function clickHandler() {
    setCount(count + 1);
    setName(name + '2020');
  }

  return (
    <div>
      <p>
        你点击了 {count} 次 {name}
      </p>
      <button onClick={clickHandler}>点击</button>
    </div>
  );
}

export default LifeCycles;

现在,我们在 App.js 中注册该组件。具体代码如下:

js
import React, { useState } from 'react';
import LifeCycles from './components/LifeCycles'

function App() {
  const [flag, setFlag] = useState(true)

  return (
    <div
      {flag && <LifeCycles/>}
    </div>
  );
}

export default App;
import React, { useState } from 'react';
import LifeCycles from './components/LifeCycles'

function App() {
  const [flag, setFlag] = useState(true)

  return (
    <div
      {flag && <LifeCycles/>}
    </div>
  );
}

export default App;

此时,浏览器的显示效果如下:

useEffect①

大家可以看到, useEffect 如果只传入一个函数的话,那么它模拟的是 DidMountDidUpdate 这两个生命周期的功能。每当我们点击一次的时候,浏览器就会打印一次,也就是组件加载和组件更新是在一起进行的。


那如果我们想要让组件加载和组件更新分开实现呢,同样有办法。我们来改造下 LiftCycles.js 的代码。具体代码如下:

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

function LifeCycles() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 模拟 class 组件的 DidMount
  useEffect(() => {
    console.log('加载完了');
  }, []); // 第二个参数是 [] (不依赖于任何 state)

  // 模拟 class 组件的 DidUpdate
  useEffect(() => {
    console.log('更新了');
  }, [count, name]); // 第二个参数就是依赖的 state

  function clickHandler() {
    setCount(count + 1);
    setName(name + '2020');
  }

  return (
    <div>
      <p>
        你点击了 {count} 次 {name}
      </p>
      <button onClick={clickHandler}>点击</button>
    </div>
  );
}

export default LifeCycles;
import React, { useState, useEffect } from 'react';

function LifeCycles() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 模拟 class 组件的 DidMount
  useEffect(() => {
    console.log('加载完了');
  }, []); // 第二个参数是 [] (不依赖于任何 state)

  // 模拟 class 组件的 DidUpdate
  useEffect(() => {
    console.log('更新了');
  }, [count, name]); // 第二个参数就是依赖的 state

  function clickHandler() {
    setCount(count + 1);
    setName(name + '2020');
  }

  return (
    <div>
      <p>
        你点击了 {count} 次 {name}
      </p>
      <button onClick={clickHandler}>点击</button>
    </div>
  );
}

export default LifeCycles;

此时浏览器的显示效果如下:

useEffect②

大家可以看到,第一次的时候分别加载和更新了 1 次。等到我们去点击的时候,因为已经加载完了,所以这个时候就是更新了。

那我们来梳理一下, useEffect 在加载和更新的时候,分别是怎么进行处理的?

看上面的代码中我们可以发现,useEffect 还可以接收第二个参数,第二个参数如果空值,那么它不依赖于任何 state ,表示 componentDidMount 这个生命周期。相反,如果第二个参数没有依赖值或者接收依赖于 state 的值这两种情况时,那么它模拟 componentDidUpdate 这个生命周期。


说到这里,可能有的小伙伴还想问,那跟销毁相关的生命周期,又怎么处理呢?我们继续来对 LiftCycles.js 的代码进行改造。具体如下:

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

function LifeCycles() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 模拟 class 组件的 DidMount
  useEffect(() => {
    let timerId = window.setInterval(() => {
      console.log(Date.now());
    }, 1000);

    // 返回一个函数
    // 模拟 WillUnMount
    return () => {
      window.clearInterval(timerId);
    };
  }, []);

  function clickHandler() {
    setCount(count + 1);
    setName(name + '2020');
  }

  return (
    <div>
      <p>
        你点击了 {count} 次 {name}
      </p>
      <button onClick={clickHandler}>点击</button>
    </div>
  );
}

export default LifeCycles;
import React, { useState, useEffect } from 'react';

function LifeCycles() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 模拟 class 组件的 DidMount
  useEffect(() => {
    let timerId = window.setInterval(() => {
      console.log(Date.now());
    }, 1000);

    // 返回一个函数
    // 模拟 WillUnMount
    return () => {
      window.clearInterval(timerId);
    };
  }, []);

  function clickHandler() {
    setCount(count + 1);
    setName(name + '2020');
  }

  return (
    <div>
      <p>
        你点击了 {count} 次 {name}
      </p>
      <button onClick={clickHandler}>点击</button>
    </div>
  );
}

export default LifeCycles;

大家可以看以上代码,在这里,我们通过 return 一个函数,去模拟 componentWillUnMount 这个生命周期,来销毁每一次定时器中执行的任务。

到这里,相信大家对 useEffect 已经有了一定的了解。现在,我们来对 useEffect 做个小结。

(3)useEffect 使用总结

  • 模拟 componentDidMount - useEffect 依赖 []
  • 模拟 componentDidUpdate - useEffect 无依赖,或者依赖 [a, b]
  • 模拟 componentWillUnMount - useEffect返回一个函数

(4)useEffect 让纯函数有了副作用

  • 默认情况下,执行纯函数的时候,只需要输入参数并返回结果即可,是没有任何副作用的;
  • 所谓副作用,就是对函数之外造成影响,如设置全局定时任务
  • 而组件需要副作用,所以需要 useEffect “钩” 到纯函数中;
  • 因此, useEffect 这个副作用并不是一件坏事。

(5)useEffect 中返回函数 fn

有一种值得注意的情况是,在上面我们通过返回一个函数来模拟 WillUnMount ,但这个模拟后的结果还不完全等于 WillUnMount 。现在,我们在 src|components 下建立一个文件,命名为 FriendStatus.js具体代码如下:

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

function FriendStatus({ friendId }) {
  const [status, setStatus] = useState(false);

  // DidMount 和 DidUpdate
  useEffect(() => {
    console.log(`开始监听 ${friendId} 在线状态`);

    // 【特别注意】
    // 此处并不完全等同于 WillUnMount
    // props 发生变化,即更新,也会执行结束监听
    // 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
    return () => {
      console.log(`结束监听 ${friendId} 在线状态`);
    };
  });

  return (
    <div>
      好友 {friendId} 在线状态:{status.toString()}
    </div>
  );
}

export default FriendStatus;
import React, { useState, useEffect } from 'react';

function FriendStatus({ friendId }) {
  const [status, setStatus] = useState(false);

  // DidMount 和 DidUpdate
  useEffect(() => {
    console.log(`开始监听 ${friendId} 在线状态`);

    // 【特别注意】
    // 此处并不完全等同于 WillUnMount
    // props 发生变化,即更新,也会执行结束监听
    // 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
    return () => {
      console.log(`结束监听 ${friendId} 在线状态`);
    };
  });

  return (
    <div>
      好友 {friendId} 在线状态:{status.toString()}
    </div>
  );
}

export default FriendStatus;

App.js 的代码如下:

js
import React, { useState } from 'react';
import FriendStatus from './components/FriendStatus';

function App() {
  const [flag, setFlag] = useState(true);
  const [id, setId] = useState(1);

  return (
    <div>
      <div>
        <button onClick={() => setFlag(false)}>flag = false</button>
        <button onClick={() => setId(id + 1)}>id++</button>
      </div>

      {flag && <FriendStatus friendId={id} />}
    </div>
  );
}

export default App;
import React, { useState } from 'react';
import FriendStatus from './components/FriendStatus';

function App() {
  const [flag, setFlag] = useState(true);
  const [id, setId] = useState(1);

  return (
    <div>
      <div>
        <button onClick={() => setFlag(false)}>flag = false</button>
        <button onClick={() => setId(id + 1)}>id++</button>
      </div>

      {flag && <FriendStatus friendId={id} />}
    </div>
  );
}

export default App;

此时,浏览器的显示效果如下:

useEffect③

大家可以看到,当开始下一个监听时,会先结束掉上一个监听再开始下一个。正如图中所看到的,一开始我们正在监听 id1 的好友,那当我们想要点击 id++ 按钮去监听好友 2 时,useEffect 会先去结束掉 1 的状态,再让好友 2 上线。

此时,我们要注意的一个点是,执行结束监听的这个函数,执行的是 DidUpdate 生命周期,而不是 WillUnMount 生命周期。函数在这个时候是属于更新状态而不是销毁状态

依据以上内容,我们来做个小结。具体如下:

  • useEffect 依赖于 [] 的时候,在组件销毁时执行 return 的函数 fn ,等于 WillUnMount 生命周期;
  • useEffect 无依赖或依赖于 [a, b] 的时候,组件是在更新时执行 return 的函数 fn ;即在下一次执行 useEffect 之前,就会先执行 fn ,此时模拟的是 DidUpdate 生命周期。

3、其他 Hooks🗞️

上面我们讲了两个比较常用的 hooksuseStateuseEffect 。接下来,我们来了解其他几个比较不常用的 hooks

(1)useRef

Ref 是用于对 值的修改DOM 元素的获取先来看一段代码:

js
import React, { useRef, useEffect } from 'react';

function UseRef() {
  const btnRef = useRef(null); // 初始值

  const numRef = useRef(0);
  numRef.current = 2;

  useEffect(() => {
    console.log(btnRef.current); // DOM 节点
    console.log(numRef.current); // 得到值
  }, []);

  return (
    <div>
      <button ref={btnRef}>click</button>
    </div>
  );
}

export default UseRef;
import React, { useRef, useEffect } from 'react';

function UseRef() {
  const btnRef = useRef(null); // 初始值

  const numRef = useRef(0);
  numRef.current = 2;

  useEffect(() => {
    console.log(btnRef.current); // DOM 节点
    console.log(numRef.current); // 得到值
  }, []);

  return (
    <div>
      <button ref={btnRef}>click</button>
    </div>
  );
}

export default UseRef;

此时,浏览器的显示效果为:

useRef

大家可以看到,如果将 btnRef 绑定到 ref={btnRef} 上,那么 btnRef.current 获取到的是当前的 DOM 节点。

另外一种情况是,大家定位到 numRef ,如果我们给其进行赋值并修改,那么 numRef.current 最终得到的值是修改后的值。

(2)useContext

很多时候,我们经常会有一些比较静态的属性需要做切换,比如:主题颜色切换。这个时候就需要用到 context 上下文来处理。那在 hook 中,就有 useContext 可以处理这件事情。先来看一段演示代码:

js
import React, { useContext } from 'react';

// 主题颜色
const themes = {
  light: {
    foreground: '#000',
    background: '#eee',
  },
  dark: {
    foreground: '#fff',
    background: '#222',
  },
};

// 创建 Context
const ThemeContext = React.createContext(themes.light); // 初始值

function ThemeButton() {
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      hello world
    </button>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemeButton></ThemeButton>
    </div>
  );
}

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar></Toolbar>
    </ThemeContext.Provider>
  );
}

export default App;
import React, { useContext } from 'react';

// 主题颜色
const themes = {
  light: {
    foreground: '#000',
    background: '#eee',
  },
  dark: {
    foreground: '#fff',
    background: '#222',
  },
};

// 创建 Context
const ThemeContext = React.createContext(themes.light); // 初始值

function ThemeButton() {
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      hello world
    </button>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemeButton></ThemeButton>
    </div>
  );
}

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar></Toolbar>
    </ThemeContext.Provider>
  );
}

export default App;

此时浏览器的显示效果如下:

useContext

现在属于黑色背景的主题。如果我们要切换为白色背景的主题,那么只需要把最底下的代码 value={themes.dark} 改为 value={themes.light} 即可。

(3)useReducer

redux 中,我们会用到 reducer ,但 useReducerredux 还有一点区别。 useReducer 是对单个组件进行状态

先来看一段代码:

js
import React, { useReducer } from 'react';

const initialState = { count: 0 };

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

function App() {
  // 很像 const [count, setCount] = useState(0)
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
    </div>
  );
}

export default App;
import React, { useReducer } from 'react';

const initialState = { count: 0 };

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

function App() {
  // 很像 const [count, setCount] = useState(0)
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
    </div>
  );
}

export default App;

此时浏览器的显示效果如下:

useReducer

我们将要改变的值放到 reducer 当中,之后再将 reducer 传递给 useRouter 。最后,通过 dispatch 来对值进行绑定。

下面我们来梳理一下 useRuducerredux 的区别:

  • useReduceruseState 的代替方案,用于处理 state 的复杂变化;
  • useReducer单个组件状态管理,组件通讯还需要 props
  • redux 是全局的状态管理多组件共享数据

(4)useMemo

useMemohooks 中性能优化的一部分。当我们不使用 useMemo 时,状态时这样的。看以下这段代码:

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

// 子组件
function Child({ userInfo }) {
  console.log('Child render...', userInfo);

  return (
    <div>
      <p>
        This is Child {userInfo.name} {userInfo.age}
      </p>
    </div>
  );
}

// 父组件
function App() {
  console.log('Parent render...');

  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  const userInfo = { name, age: 20 };

  return (
    <div>
      <p>
        count is {count}
        <button onClick={() => setCount(count + 1)}>click</button>
      </p>
      <Child userInfo={userInfo}></Child>
    </div>
  );
}

export default App;
import React, { useState } from 'react';

// 子组件
function Child({ userInfo }) {
  console.log('Child render...', userInfo);

  return (
    <div>
      <p>
        This is Child {userInfo.name} {userInfo.age}
      </p>
    </div>
  );
}

// 父组件
function App() {
  console.log('Parent render...');

  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  const userInfo = { name, age: 20 };

  return (
    <div>
      <p>
        count is {count}
        <button onClick={() => setCount(count + 1)}>click</button>
      </p>
      <Child userInfo={userInfo}></Child>
    </div>
  );
}

export default App;

此时,控制台的打印效果如下:

useMemo①

大家可以看到,每每我们点击一次,不管 state 的值是否更新时,子组件 Child 都会重新打印一次。 这对于程序来说,是比较耗费性能的。因此,我们要用 useMemo 来阻止 Child 事件频繁打印,提高程序的性能。代码改造如下:

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

// 子组件
// 类似 class PureComponent ,对 props 进行浅层比较
const Child = memo(({ userInfo }) => {
  console.log('Child render...', userInfo);

  return (
    <div>
      <p>
        This is Child {userInfo.name} {userInfo.age}
      </p>
    </div>
  );
});

// 父组件
function App() {
  console.log('Parent render...');

  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 用 useMemo 缓存数据,依赖于 [name]
  //  当name有更新时,{ name, age: 18 } 这个缓存就会失效
  const userInfo = useMemo(() => {
    return { name, age: 18 };
  }, [name]);

  return (
    <div>
      <p>
        count is {count}
        <button onClick={() => setCount(count + 1)}>click</button>
      </p>
      <Child userInfo={userInfo}></Child>
    </div>
  );
}

export default App;
import React, { useState, memo, useMemo } from 'react';

// 子组件
// 类似 class PureComponent ,对 props 进行浅层比较
const Child = memo(({ userInfo }) => {
  console.log('Child render...', userInfo);

  return (
    <div>
      <p>
        This is Child {userInfo.name} {userInfo.age}
      </p>
    </div>
  );
});

// 父组件
function App() {
  console.log('Parent render...');

  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 用 useMemo 缓存数据,依赖于 [name]
  //  当name有更新时,{ name, age: 18 } 这个缓存就会失效
  const userInfo = useMemo(() => {
    return { name, age: 18 };
  }, [name]);

  return (
    <div>
      <p>
        count is {count}
        <button onClick={() => setCount(count + 1)}>click</button>
      </p>
      <Child userInfo={userInfo}></Child>
    </div>
  );
}

export default App;

此时,浏览器的显示效果如下:

useMemo②

大家可以看到, Child 组件除了第一次更新有打印之外,之后的没有更新就不会再进行打印。那它是怎么使用的呢,首先,我们要用 memo 把子组件给包裹下来,之后呢,通过 useMemo 去缓存数据,同时这个数据要依赖于 [name] ;值得注意的是,如果不依赖于 [name] ,它是不会生效的。

现在,我们来对 useMemo 做个总结,具体如下:

  • React 默认会更新所有子组件
  • class 组件使用 shouldComponentUpdatePureComponent 做优化;
  • Hooks 中使用 useMemo 做优化,但优化原理和 class 组件是相同的。

(5)useCallback

上面我们说到的是使用 useMemo缓存数据,现在我们来讲下 useCallbackuseCallback 主要是用来缓存函数

假设我们现在不使用 useCallback ,来运行下面这段代码:

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

// 子组件,memo 相当于 PureComponent
const Child = memo(({ userInfo, onChange }) => {
  console.log('Child render...', userInfo);

  return (
    <div>
      <p>
        This is Child {userInfo.name} {userInfo.age}
      </p>
      <input onChange={onChange}></input>
    </div>
  );
});

// 父组件
function App() {
  console.log('Parent render...');

  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 用 useMemo 缓存数据
  const userInfo = useMemo(() => {
    return { name, age: 21 };
  }, [name]);

  function onChange(e) {
    console.log(e.target.value);
  }

  return (
    <div>
      <p>
        count is {count}
        <button onClick={() => setCount(count + 1)}>click</button>
      </p>
      <Child userInfo={userInfo} onChange={onChange}></Child>
    </div>
  );
}

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

// 子组件,memo 相当于 PureComponent
const Child = memo(({ userInfo, onChange }) => {
  console.log('Child render...', userInfo);

  return (
    <div>
      <p>
        This is Child {userInfo.name} {userInfo.age}
      </p>
      <input onChange={onChange}></input>
    </div>
  );
});

// 父组件
function App() {
  console.log('Parent render...');

  const [count, setCount] = useState(0);
  const [name, setName] = useState('星期一研究室');

  // 用 useMemo 缓存数据
  const userInfo = useMemo(() => {
    return { name, age: 21 };
  }, [name]);

  function onChange(e) {
    console.log(e.target.value);
  }

  return (
    <div>
      <p>
        count is {count}
        <button onClick={() => setCount(count + 1)}>click</button>
      </p>
      <Child userInfo={userInfo} onChange={onChange}></Child>
    </div>
  );
}

export default App;

此时浏览器的打印效果如下:

useCallback①

大家可以看到,如果没有使用 useCallback ,那么其会一直将子组件打印出来,也就是 onChange 事件会一直运转。现在,我们将 onChange 事件改造一下,具体如下:

js
// 用 useCallback 缓存函数
const onChange = useCallback((e) => {
  console.log(e.target.value);
}, []);
// 用 useCallback 缓存函数
const onChange = useCallback((e) => {
  console.log(e.target.value);
}, []);

此时我们来看下浏览器运行的效果:

useCallback②

大家可以看到,加上 useCallback 之后,且 Child 没有更新时,也就不会再更新啦!

接下来我们对 useMemouseCallback 做个小结:

  • useMemo 缓存数据;
  • useCallback 缓存函数;
  • 两者都是 React Hooks 的常见优化策略。

4、自定义 Hook🗞️

(1)为什么要使用自定义 Hook

  • 用来封装通用的功能
  • 可以开发使用第三方 Hooks
  • 自定义 Hook 带来了无限的扩展性,解耦代码。

(2)举例阐述

假设我们现在想要封装一个 useAxios ,那怎么实现呢?

首先我们需要先在项目中安装 axios具体代码如下:

bash
npm i axios --save
npm i axios --save

接着,我们在项目的 src|customHooks 文件夹下新建一个文件,命名为 useAxios.js具体代码如下:

js
import { useState, useEffect } from 'react';
import axios from 'axios';

// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
  // loading 模拟当前是否再等待中
  const [loading, setLoading] = useState(false);
  // 请求成功时返回的数据
  const [data, setData] = useState();
  // 请求失败时返回的一些信息
  const [error, setError] = useState();

  useEffect(() => {
    // 利用 axios 发送网络请求
    setLoading(true);
    axios
      .get(url) // 发送一个 get 请求
      .then((res) => setData(res))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [url]);

  return [loading, data, error];
}

export default useAxios;
import { useState, useEffect } from 'react';
import axios from 'axios';

// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
  // loading 模拟当前是否再等待中
  const [loading, setLoading] = useState(false);
  // 请求成功时返回的数据
  const [data, setData] = useState();
  // 请求失败时返回的一些信息
  const [error, setError] = useState();

  useEffect(() => {
    // 利用 axios 发送网络请求
    setLoading(true);
    axios
      .get(url) // 发送一个 get 请求
      .then((res) => setData(res))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [url]);

  return [loading, data, error];
}

export default useAxios;

继续,我们在项目的 src|components 文件夹,新建一个文件,命名为 CustomHookUsage.js 。这个组件主要用来使用上面所自定义的 useAxios 这个 hook具体代码如下:

js
import React from 'react';
import useAxios from '../customHooks/useAxios';

function App() {
  const url = 'http://localhost:3000/';
  // 数组解构
  const [loading, data, error] = useAxios(url);

  if (loading) return <div>loading...</div>;

  return error ? (
    <div>{JSON.stringify(error)}</div>
  ) : (
    <div>{JSON.stringify(data)}</div>
  );
}

export default App;
import React from 'react';
import useAxios from '../customHooks/useAxios';

function App() {
  const url = 'http://localhost:3000/';
  // 数组解构
  const [loading, data, error] = useAxios(url);

  if (loading) return <div>loading...</div>;

  return error ? (
    <div>{JSON.stringify(error)}</div>
  ) : (
    <div>{JSON.stringify(data)}</div>
  );
}

export default App;

最后,我们在项目的 App.js 中来使用它。具体代码如下:

js
import React, { useState } from 'react';
import CustomHookUsage from './components/CustomHookUsage';

function App() {
  return <div>{<CustomHookUsage />}</div>;
}

export default App;
import React, { useState } from 'react';
import CustomHookUsage from './components/CustomHookUsage';

function App() {
  return <div>{<CustomHookUsage />}</div>;
}

export default App;

此时,我们来看下浏览器的显示效果:

useAxios

大家可以看到,首先是在等待的时候,它会先出现 loading 。之后呢,如果在等待结束并请求成功后,会返回 data 。当然,我们这里没有演示 error 的情况。大家可以把 url 修改为不存在的请求地址,即可测试 error 的情况。

(3)总结

看完上面的演示代码以后,我们来做个小结:

  • 自定义 hook 本质是一个函数,以 use 开头;
  • 内部可以正常使用 useStateuseEffect 或者其他 Hooks
  • 自定义返回结果,格式不限

这里给大家推荐两个第三方自定义 Hook 的库:

5、Hooks 的两条重要规则 🗞️

(1)Hooks 使用规范

  • Hooks 只能用于 react 函数组件和自定义 Hooks 中,其他地方都不可以。比如:class 组件 和 普通函数;

  • 只能用于顶层代码,不能在循环判断中使用 Hooks

  • eslint 中的插件 eslint-plugin-react-hooks ,可以帮到你使用 react-hooks如下配置所示:

    json
    // ESLint 配置文件
    {
      "plugins": [
        // ...此处省略 N 行...
        "react-hooks"
      ],
      "rules": {
        // ...此处省略 N 行 ...
        "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
        "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
      }
    }
    // ESLint 配置文件
    {
      "plugins": [
        // ...此处省略 N 行...
        "react-hooks"
      ],
      "rules": {
        // ...此处省略 N 行 ...
        "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
        "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
      }
    }

(2)Hooks 调用顺序必须保持一致

react-hooks 中,调用顺序是尤为需要注意的一个关键点。为什么呢?

下面我们用一段代码来演示:

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

function Teach({ couseName }) {
  // 函数组件,纯函数,执行完即销毁
  // 所以,无论组件初始化(render)还是组件更新(re-render)
  // 都会重新执行一次这个函数,获取最新的组件
  // 这一点和 class 组件不一样

  // render: 初始化 state 的值 '张三'
  // re-render: 读取 state 的值 '张三'
  const [studentName, setStudentName] = useState('张三');

  // if (couseName) {
  //     const [studentName, setStudentName] = useState('张三')
  // }

  // render: 初始化 state 的值 '双越'
  // re-render: 读取 state 的值 '双越'
  const [teacherName, setTeacherName] = useState('星期一研究室');

  // if (couseName) {
  //     useEffect(() => {
  //         // 模拟学生签到
  //         localStorage.setItem('name', studentName)
  //     })
  // }

  // render: 添加 effect 函数
  // re-render: 替换 effect 函数(内部的函数也会重新定义)
  useEffect(() => {
    // 模拟学生签到
    localStorage.setItem('name', studentName);
  });

  // render: 添加 effect 函数
  // re-render: 替换 effect 函数(内部的函数也会重新定义)
  useEffect(() => {
    // 模拟开始上课
    console.log(`${teacherName} 开始上课,学生 ${studentName}`);
  });

  return (
    <div>
      课程:{couseName}, 讲师:{teacherName}, 学生:{studentName}
    </div>
  );
}

export default Teach;
import React, { useState, useEffect } from 'react';

function Teach({ couseName }) {
  // 函数组件,纯函数,执行完即销毁
  // 所以,无论组件初始化(render)还是组件更新(re-render)
  // 都会重新执行一次这个函数,获取最新的组件
  // 这一点和 class 组件不一样

  // render: 初始化 state 的值 '张三'
  // re-render: 读取 state 的值 '张三'
  const [studentName, setStudentName] = useState('张三');

  // if (couseName) {
  //     const [studentName, setStudentName] = useState('张三')
  // }

  // render: 初始化 state 的值 '双越'
  // re-render: 读取 state 的值 '双越'
  const [teacherName, setTeacherName] = useState('星期一研究室');

  // if (couseName) {
  //     useEffect(() => {
  //         // 模拟学生签到
  //         localStorage.setItem('name', studentName)
  //     })
  // }

  // render: 添加 effect 函数
  // re-render: 替换 effect 函数(内部的函数也会重新定义)
  useEffect(() => {
    // 模拟学生签到
    localStorage.setItem('name', studentName);
  });

  // render: 添加 effect 函数
  // re-render: 替换 effect 函数(内部的函数也会重新定义)
  useEffect(() => {
    // 模拟开始上课
    console.log(`${teacherName} 开始上课,学生 ${studentName}`);
  });

  return (
    <div>
      课程:{couseName}, 讲师:{teacherName}, 学生:{studentName}
    </div>
  );
}

export default Teach;

上面的代码演示的是一个函数组件。那对于函数组件来说,它是一个纯函数,一般执行完就会销毁。因此,无论是组件初始化(render)还是组件更新(re-render),都会重新执行一次这个函数,获取最新的组件。这一点和 class 组件不一样。

因此,假设我们把上面的 const [studentName, setStudentName] = useState('张三') 给用 if 语句包裹起来,那么它在执行时会受到阻碍。同时,沿着底下的顺序走下去,有可能就会把 张三 这个值,赋值给 teacherName 。因此,在 react-hooks 中,我们不能将 react-hooks 用在循环判断里面,不然会很容易导致调用顺序错乱

依据以上的解析,我们来对这个规则做个小结:

  • 无论是 render 还是 re-renderHooks 调用顺序必须是一致的;
  • 如果 Hooks 出现在循环判断里,则无法保证顺序一致
  • Hooks 严重依赖于调用顺序!重要!

三、⌨️React-Hooks 组件逻辑复用

1、class 组件的逻辑复用

class 组件有三种逻辑复用形式。分别是:

  • Mixin
  • 高阶组件 HOC
  • Render Prop

下面说下它们三者各自的缺点。

(1)Mixin

  • 变量作用域来源不清
  • 属性重名
  • mixins 引入过多会导致顺序冲突

(2)高阶组件 HOC

  • 组件层级嵌套过多,不易渲染,不易调试
  • HOC 会劫持 props ,必须严格规范,容易出现疏漏

(3)Render Prop

  • 学习成本高,不易理解
  • 只能传递纯函数,而默认情况下纯函数功能有限

了解了三种 class 组件的缺点之后,现在,我们来看下如何使用 Hooks 做组件逻辑复用。

2、使用 Hooks 做组件逻辑复用

使用 hooks 来使得组件可以进行逻辑复用的本质是:自定义 hooks 。下面我们用一个例子来展示。

(1)例子阐述

首先,我们在项目的 src|customHooks 文件夹下新建一个文件,命名为 useMousePosition.js具体代码如下:

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

function useMousePosition() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  useEffect(() => {
    function mouseMoveHandler(event) {
      // event.clientX 可以拿到鼠标横坐标和纵坐标的位置
      setX(event.clientX);
      setY(event.clientY);
    }

    // 绑定事件
    document.body.addEventListener('mousemove', mouseMoveHandler);

    // 解绑事件
    return () =>
      document.body.removeEventListener('mousemove', mouseMoveHandler);
  }, []);

  return [x, y];
}

export default useMousePosition;
import { useState, useEffect } from 'react';

function useMousePosition() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  useEffect(() => {
    function mouseMoveHandler(event) {
      // event.clientX 可以拿到鼠标横坐标和纵坐标的位置
      setX(event.clientX);
      setY(event.clientY);
    }

    // 绑定事件
    document.body.addEventListener('mousemove', mouseMoveHandler);

    // 解绑事件
    return () =>
      document.body.removeEventListener('mousemove', mouseMoveHandler);
  }, []);

  return [x, y];
}

export default useMousePosition;

继续,我们在项目的 src|components 文件夹,新建一个文件,命名为 CustomHookUsage.js 。这个组件主要用来使用上面 useMousePosition 中的逻辑。具体代码如下:

js
import React from 'react';
import useMousePosition from '../customHooks/useMousePosition';

function App() {
  const [x, y] = useMousePosition();
  return (
    <div style={{ height: '500px', backgroundColor: '#ccc' }}>
      <p>
        鼠标位置 {x} {y}
      </p>
    </div>
  );
}

export default App;
import React from 'react';
import useMousePosition from '../customHooks/useMousePosition';

function App() {
  const [x, y] = useMousePosition();
  return (
    <div style={{ height: '500px', backgroundColor: '#ccc' }}>
      <p>
        鼠标位置 {x} {y}
      </p>
    </div>
  );
}

export default App;

最后,我们在项目的 App.js 中来使用它。具体代码如下:

js
import React, { useState } from 'react';
import CustomHookUsage from './components/CustomHookUsage';

function App() {
  return <div>{<CustomHookUsage />}</div>;
}

export default App;
import React, { useState } from 'react';
import CustomHookUsage from './components/CustomHookUsage';

function App() {
  return <div>{<CustomHookUsage />}</div>;
}

export default App;

此时,我们来看下浏览器的显示效果:

useMousePosition

使用 hooks 做组件逻辑复用,要比 class 组件要简便和灵活许多。下面我们来梳理下 hooks 做组件逻辑复用的好处。

(2)好处

Hooks 做组件逻辑复用的好处:

  • 完全符合 Hooks 原有的规则,没有其他要求,易理解和记忆;
  • 变量作用域很明确;
  • 不会产生组件嵌套。

四、📀React Hooks 注意事项

1、useState 初始化值,只有第一次有效

我们先来看一段代码,具体如下:

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

// 子组件
function Child({ userInfo }) {
  // render: 初始化 state
  // re-render: 只恢复初始化的 state 值,不会再重新设置新的值
  //            只能用 setName 修改
  const [name, setName] = useState(userInfo.name);

  return (
    <div>
      <p>Child, props name: {userInfo.name}</p>
      <p>Child, state name: {name}</p>
    </div>
  );
}

function App() {
  const [name, setName] = useState('星期一');
  const userInfo = { name };

  return (
    <div>
      <div>
        Parent &nbsp;
        <button onClick={() => setName('星期二')}>setName</button>
      </div>
      <Child userInfo={userInfo} />
    </div>
  );
}

export default App;
import React, { useState } from 'react';

// 子组件
function Child({ userInfo }) {
  // render: 初始化 state
  // re-render: 只恢复初始化的 state 值,不会再重新设置新的值
  //            只能用 setName 修改
  const [name, setName] = useState(userInfo.name);

  return (
    <div>
      <p>Child, props name: {userInfo.name}</p>
      <p>Child, state name: {name}</p>
    </div>
  );
}

function App() {
  const [name, setName] = useState('星期一');
  const userInfo = { name };

  return (
    <div>
      <div>
        Parent &nbsp;
        <button onClick={() => setName('星期二')}>setName</button>
      </div>
      <Child userInfo={userInfo} />
    </div>
  );
}

export default App;

此时,浏览器的显示效果如下:

useState初始化值

大家可以看到,当我们点击的时候,只改变了 useInfo.name 的值,也就是第一个 Child 的值。而此时,可能有的小伙伴会产生疑惑说, useInfo.name 的值是赋值给 name 的,那为什么第二个 Child 不会改变呢。

原因在于,对于 useState 来说,刚开始我们总是会给 state 设置一个初始化的值,而这个初始化的值,即使是数据更新了,它只有第一次的值是有效的,更新的值不会被赋值到其中。所以下面第二个 Childname 值就一直是星期一

2、useEffect 内部不能修改 state

先来模拟一段代码:

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

function UseEffectChangeState() {
  const [count, setCount] = useState(0);

  // 模拟 DidMount
  useEffect(() => {
    console.log('useEffect...', count);

    // 定时任务
    const timer = setInterval(() => {
      console.log('setInterval...', count);
      setCount(count + 1);
    }, 1000);

    // 清除定时任务
    return () => clearTimeout(timer);
  }, []); // 依赖为 []

  // 依赖为 [] 时: re-render 不会重新执行 effect 函数
  // 没有依赖时:re-render 会重新执行 effect 函数

  return <div>count: {count}</div>;
}

export default UseEffectChangeState;
import React, { useState, useEffect } from 'react';

function UseEffectChangeState() {
  const [count, setCount] = useState(0);

  // 模拟 DidMount
  useEffect(() => {
    console.log('useEffect...', count);

    // 定时任务
    const timer = setInterval(() => {
      console.log('setInterval...', count);
      setCount(count + 1);
    }, 1000);

    // 清除定时任务
    return () => clearTimeout(timer);
  }, []); // 依赖为 []

  // 依赖为 [] 时: re-render 不会重新执行 effect 函数
  // 没有依赖时:re-render 会重新执行 effect 函数

  return <div>count: {count}</div>;
}

export default UseEffectChangeState;

此时我们来看下浏览器的效果:

useEffect内部不能修改state①

大家可以看到,如果按照我们预期所想的,那么应该会递增地打印出 1234 ,但是最终没有这么显示,timer 里面打印的一直是初始值 0 ,也就是说,这个 stateuseEffect 里面并不起作用。

因此,这一点在我们日常的开发中需要尤为注意,有可能代码写着写着 bug 就出现在这了。


那既然问题出现了,我们就需要来想一下它的解决方案了。改造如下:

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

function UseEffectChangeState() {
  const [count, setCount] = useState(0);

  // 模拟 DidMount
  const countRef = useRef(0);
  useEffect(() => {
    console.log('useEffect...', count);

    // 定时任务
    const timer = setInterval(() => {
      console.log('setInterval...', countRef.current);
      // setCount(count + 1)
      setCount(++countRef.current);
    }, 1000);

    // 清除定时任务
    return () => clearTimeout(timer);
  }, []); // 依赖为 []

  // 依赖为 [] 时: re-render 不会重新执行 effect 函数
  // 没有依赖:re-render 会重新执行 effect 函数

  return <div>count: {count}</div>;
}

export default UseEffectChangeState;
import React, { useState, useRef, useEffect } from 'react';

function UseEffectChangeState() {
  const [count, setCount] = useState(0);

  // 模拟 DidMount
  const countRef = useRef(0);
  useEffect(() => {
    console.log('useEffect...', count);

    // 定时任务
    const timer = setInterval(() => {
      console.log('setInterval...', countRef.current);
      // setCount(count + 1)
      setCount(++countRef.current);
    }, 1000);

    // 清除定时任务
    return () => clearTimeout(timer);
  }, []); // 依赖为 []

  // 依赖为 [] 时: re-render 不会重新执行 effect 函数
  // 没有依赖:re-render 会重新执行 effect 函数

  return <div>count: {count}</div>;
}

export default UseEffectChangeState;

此时浏览器的显示效果如下:

useEffect内部不能修改state②

看上面的代码我们可以发现,通过把 countRef 放到 useEffect 外部,并使用 useRef ,以此来对 state 的值进行修改,达到我们最终的目的。

3、useEffect 可能出现死循环

假设我们要利用 axios 发送网络请求,并且有想要给 url 加一些配置,那么我们有可能这么处理:

js
import { useState, useEffect } from 'react';
import axios from 'axios';

// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url, config = {}) {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState();
  const [error, setError] = useState();

  useEffect(() => {
    // 利用 axios 发送网络请求
    setLoading(true);
    axios(url, config) // 发送一个 get 请求
      .then((res) => setData(res))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [url, config]);

  return [loading, data, error];
}

export default useAxios;
import { useState, useEffect } from 'react';
import axios from 'axios';

// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url, config = {}) {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState();
  const [error, setError] = useState();

  useEffect(() => {
    // 利用 axios 发送网络请求
    setLoading(true);
    axios(url, config) // 发送一个 get 请求
      .then((res) => setData(res))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [url, config]);

  return [loading, data, error];
}

export default useAxios;

看着是挺合理,但是呢,当config 使用的是引用数据类型时,也就是 config={} 或者 confgi=[] 时, useEffect 会出现死循环,无限的发送请求。因此,当我们想要给 axios 传值时,只能传基本数据类型的。改造代码如下:

js
import { useState, useEffect } from 'react'
import axios from 'axios'

// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
    const [loading, setLoading] = useState(false
    const [data, setData] = useState()
    const [error, setError] = useState()

    useEffect(() => {
        // 利用 axios 发送网络请求
        setLoading(true)
        axios.get(url) // 发送一个 get 请求
            .then(res => setData(res))
            .catch(err => setError(err))
            .finally(() => setLoading(false))
    }, [url])

    return [loading, data, error]
}

export default useAxios
import { useState, useEffect } from 'react'
import axios from 'axios'

// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
    const [loading, setLoading] = useState(false
    const [data, setData] = useState()
    const [error, setError] = useState()

    useEffect(() => {
        // 利用 axios 发送网络请求
        setLoading(true)
        axios.get(url) // 发送一个 get 请求
            .then(res => setData(res))
            .catch(err => setError(err))
            .finally(() => setLoading(false))
    }, [url])

    return [loading, data, error]
}

export default useAxios

这样,才不会出现死循环地一直翻滚的对接口发送数据请求

五、🔍 结束语

在上面的文章中,我们首先认识了 hooks 的基本概念,接着,我们重点阐述了 state hookeffect hook ,同时,我们也简单地了解了其他几种 hooks 。最后,谈到了 react-hooks 在组件逻辑复用方面的贡献,以及 react-hooks 需要注意一些事项。

到这里,关于 react-hooks 的介绍就结束啦!不知道大家对 react-hooks 又有了一些新的了解呢?

🐣 彩蛋 One More Thing

(:往期推荐

react 专栏戳这里 https://juejin.cn/column/7018019097656950815

Released under the MIT License.