Skip to main content

React hooks 使用指南

Hook 简介

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

拓展一下以上这句话:

  • Hook 是 React 16.8 的新增特性。
  • 一直以来react提倡使用函数组件,但有时候需要再函数内使用 state,之前只能转换为 class 组件,现在可以直接使用hooks
  • 约定 use 开头的 React Api 是 hooks,开发中也遵守这个规范。

随着在项目中使用了更多的hooks来开发组件,累积了一些日常使用中遇到的问题和思考,罗列在此。

Hooks的使用规则

  • 只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用
  • 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用

这里如果使用违规,会在控制台报错 Hooks can only be called inside the body of a function component 会罗列出你可能触发警告的三个原因:

  • 你的 React 和 React DOM 可能版本不匹配。
  • 你可能打破了 Hook 的规则。
  • 你可能在同一个应用中拥有多个 React 副本。

具体根据提示逐一排查。

使用Hooks的优势

  • 使用hooks,如果业务变更,就不需要把函数组件修改成类组件。
  • 告别了繁杂的this和合并了难以记忆的生命周期。
  • 更好的完成状态之间的共享,解决原来class组件内部封装的问题,也解决了高阶组件和函数组件的嵌套过深。一个组件一个自己的state,一个组件内可以公用。
  • 支持包装自己的Hooks(自定义Hooks),是基于纯命令式的api。

内置API介绍理解

React一共内置了9种Hook。

  • useState
  • usEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect

从前到后简单说一下使用方法,注意事项。

我们使用官方的 create-react-app 快速创建一个工程demo

useState

useState 的出现是 : 在函数组件里面使用 classsetState

解决了我们当一个简单函数组件想要有自己的 state 的时候,之前只能转为class组件的麻烦。

🔧 使用方法:

import React, { useState } from 'react';

function Example() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<h1>{count}</h1>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
)
}

export default Example;

💡 注意事项:

通过 useState 得到的状态 count ,在 Example 组件中的表现为一个常量,每一次通过 setCount 进行修改后,又重新通过 useState 获取到一个新的常量。

usEffect

之前很多具有副作用的操作,例如网络请求,修改 UI 等,一般都是在 class 组件的 componentDidMount 或者 componentDidUpdate 等生命周期中进行操作。而在函数组件中是没有这些生命周期的概念的,只能 return 想要渲染的元素。但是现在,在函数组件中也有执行副作用操作的地方了,就是使用 useEffect 函数。

语法

useEffect(() => { doSomething });

两个参数:

  • 第一个是一个函数,是在第一次渲染以及之后更新渲染之后会进行的副作用。

    • 这个函数可能会有返回值,倘若有返回值,返回值也必须是一个函数,会在组件被销毁时执行。
  • 第二个参数是可选的,是一个数组,数组中存放的是第一个函数中使用的某些副作用属性。用来优化 useEffect

    • 如果使用此优化,请确保该数组包含外部作用域中随时间变化且 effect 使用的任何值。 否则,您的代码将引用先前渲染中的旧值。
    • 如果要运行 effect 并仅将其清理一次(在装载和卸载时),则可以将空数组([])作为第二个参数传递。 这告诉React你的 effect 不依赖于来自 props 或 state 的任何值,所以它永远不需要重新运行。
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

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

// 对比 componentDidMount/componentDidUpdate
useEffect(() => {
// update
document.title = `You clicked ${count} times`;
// 对比 componentWillUnMount
return function cleanup() {
document.title = 'app';
}
}, [count]);

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

ReactDOM.render(<Example />, document.getElementById('root'));

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性初始化为传递的参数(initialValue)。返回的对象将持续整个组件的生命周期。

如果我们需要保存一些改变的值,就可能会用到 useRef

function TextInputChangeButton() {
// 使用 useRef 创建 inputEl
const inputEl = useRef(null);
const [text, updateText] = useState('');
// 使用 useRef 创建 textRef
const textRef = useRef();
useEffect(() => {
// 将 text 值存入 textRef.current 中
textRef.current = text;
console.log('textRef.current:', textRef.current);
});
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.value = "Hello, useRef";
};
return (
<>
{/* 保存 input 的 ref 到 inputEl */}
<input ref={ inputEl } type="text" />
<button onClick={ onButtonClick }>在 input 上展示文字</button>
<br />
<br />
<input value={text} onChange={e => updateText(e.target.value)} />
</>
);
}

点击 在 input 上展示文字 按钮,就可以看到第一个 input 上出现 Hello, useRef; 在第二个 input 中输入内容,可以看到控制台打印出对应的内容。

useMemo

useMemo 主要用于渲染过程优化,两个参数依次是计算函数(通常是组件函数)和依赖状态列表,当依赖的状态发生改变时,才会触发计算函数的执行。如果没有指定依赖,则每一次渲染过程都会执行该计算函数。

举个 🌰

useMemo 可以帮助我们优化子组件的渲染,比如这种场景: 在 A 组件中有两个子组件 B 和 C,当 A 组件中传给 B 的 props 发生变化时,A 组件状态会改变,重新渲染。此时 B 和 C 也都会重新渲染。其实这种情况是比较浪费资源的,现在我们就可以使用 useMemo 进行优化,B 组件用到的 props 变化时,只有 B 发生改变,而 C 却不会重新渲染。

const CompA = (props)  => {
console.log('CompA', props)
return <h2 style={{color: `red`}}>Comp A {props.text}</h2>
}

const CompB = (props) => {
console.log('CompB', props)
return <h2 style={{color: `blue`}}>Comp B {props.text}</h2>
}

const CompAandB = (props) => {
const [count , setCount] = useState(0)
const [a, setA] = useState('ExampleA');
const [b, setB] = useState('ExampleB');

function clickA() {
setCount(count + 1)
setA('修改后的 ExampleA' + count)
}

function clickB() {
setCount(count + 1)
setB('修改后的 ExampleB' + count)
}

// 利用useMemo进行优化:
const exampleA = useMemo(() => <CompA text={ a } />, [a]);
const exampleB = useMemo(() => <CompB text={ b } />, [b]);

return (
<div>
{exampleA}
{exampleB}
<br />
<button type="primary" onClick={clickA}>我是A按钮</button>
<br />
<button type="primary" onClick={clickB}>我是B按钮</button>
</div>
)
}

useContext

context 是在外部 create ,内部 use 的 state ,它和全局变量的区别在于,如果多个组件同时 useContext ,那么这些组件都会 rerender ,如果多个组件同时 useState 同一个全局变量,则只有触发 setState 的当前组件 rerender

// import { useState, useContext, createContext } from 'react';

// 1. 使用 createContext 创建上下文
const UserContext = new createContext();

// 2. 创建 Provider
const UserProvider = props => {
let [name, changeName] = useState('');
return (
<UserContext.Provider value={{ name, changeName }}>
{props.children}
</UserContext.Provider>
);
};

const Form = () => {
const { name, changeName } = useContext(UserContext); // 3. 使用 Context
return (
<Fragment>
<h2>Name: {name}</h2>
<input onChange={e => changeName(e.target.value)} />
</Fragment>
);
};

const ContextMain = () => (
<div>
<UserProvider>
<Form />
</UserProvider>
</div>
);

useReducer

看到 useReducer,肯定会想到 Redux ,没错它和 Redux 的工作方式是一样的。 useReducer 的出现是useState 的替代方案,能够让我们更好的管理状态。 useReducer 一共可以接受三个参数并返回当前的 state 与其配套的 dispatch

举个 🌰:

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

function ReducerCount() {
const [state, dispatch] = useReducer(reducer, { count: 1 })
return (
<div>
<h3>{state.count}</h3>
<button onClick={() => dispatch({ type: 'up' })}>加 + </button>
<button onClick={() => dispatch({ type: 'down' })}>减 - </button>
</div>
);
}

useCallback

useMemo 解决了值的缓存的问题,那么函数呢?

useCallback 可以认为是对依赖项的监听,把接受一个回调函数和依赖项数组,返回一个该回调函数的 memoized(记忆)版本,该回调函数仅在某个依赖项改变时才会更新。

下面这个 🌰 就是,当点击 count 的按钮时

const CallbackComponent = () => {
let [count, setCount] = useState(1);
let [num, setNum] = useState(1);

const memoized = useCallback( () => {
return num;
},[count])
return (
<Fragment>
<h2>记忆:{memoized()}</h2>
<h3>原始:{num}</h3>
<button onClick={() => {setCount(count + 1)}}> count+ </button>
<button onClick={() => {setNum(num + 1)}}> num+ </button>
</Fragment>
)
}

自定义Hook

自定义 hooks 可以说成是一种约定而不是功能。当一个函数以 use 开头并且在函数内部调用其他 hooks ,那么这个函数就可以成为自定义 hooks ,比如说 useSomething

自定义 Hooks 可以封装状态,能够更好的实现状态共享。

我们来封装一个数字加减的Hook

const useCount = (num) => {
let [count, setCount] = useState(num);
return [count,()=>setCount(count + 1), () => setCount(count - 1)]
};

这个自定义Hook内部使用useState定义一个状态,返回一个数组,数组中有状态的值、状态++的函数,状态--的函数。

自定义hooks的使用方法

const CustomComp = () => {
let [count, addCount, redCount] = useCount(1);
return (
<>
<h1>{count}</h1>
<button onClick={addCount}> +++ </button>
<button onClick={redCount}> --- </button>
</>
)
}

主函数中使用解构赋值的方式接受这三个值使用,这是一种非常简单的自定义Hook。 如果项目大的话使用自定义Hook会抽离可以抽离公共代码,极大的减少我们的代码量,提高开发效率。

Hooks 使用及编写规范

  • 不要从常规 JavaScript 函数调用 Hooks;
  • 不要在循环,条件或嵌套函数中调用 Hooks;
  • 必须在组件的顶层调用 Hooks;
  • 可以从 React 功能组件调用 Hooks;
  • 可以从自定义 Hooks 中调用 Hooks;
  • 自定义 Hooks 必须使用 use 开头,这是一种约定;

使用 React 提供的 ESLint 插件

根据上一段所写,在 React 中使用 Hooks 需要遵循一些特定规则。但是在代码的编写过程中,可能会忽略掉这些使用规则,从而导致出现一些不可控的错误。这种情况下,我们就可以使用 React 提供的 ESLint 插件:eslint-plugin-react-hooks。下面我们就看看如何使用吧。

安装 ESLint 插件

$ npm install eslint-plugin-react-hooks --save

复制代码在 .eslintrc 中使用插件

// Your ESLint configuration
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
}
}

React 官网 React Hooks FAQ