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
的出现是 : 在函数组件里面使用 class
的 setState
解决了我们当一个简单函数组件想要有自己的 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
}
}