本文对 16.8 版本以后 React 发布的新特性 Hooks 进行了详细讲解,并对一些经常使用的 Hooks 进行代码演示,但愿能够对须要的朋友提供点帮助。css
Hooks
是 React v16.7.0-alpha
中加入的新特性。它可让你在 class
之外使用 state
和其余 React
特性。 本文就是演示各类 Hooks API 的使用方式,对于内部的原理这里就不作详细说明。html
Example.jsreact
import React, { useState } from 'react';
function Example() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
);
}
export default Example;
复制代码
useState
就是一个 Hook
,能够在咱们不使用 class
组件的状况下,拥有自身的 state
,而且能够经过修改 state
来控制 UI 的展现。npm
const [state, setState] = useState(initialState)json
initialState
,能够是数字,字符串等,也能够是对象或者数组。state
变量,setState
修改 state值的方法。setState
的异同点:onClick
事件中,调用两次 setState
,数据只改变一次。setState
是合并,而函数组件中的 setState
是替换。以前想要使用组件内部的状态,必须使用 class 组件,例如:数组
Example.js浏览器
import React, { Component } from 'react';
export default class Example extends 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 了。bash
Example.js网络
import React, { useState } from 'react';
function Example() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
);
}
export default Example;
复制代码
建立初始状态是比较昂贵的,因此咱们能够在使用 useState
API 时,传入一个函数,就能够避免从新建立忽略的初始状态。异步
普通的方式:
// 直接传入一个值,在每次 render 时都会执行 createRows 函数获取返回值
const [rows, setRows] = useState(createRows(props.count));
复制代码
优化后的方式(推荐):
// createRows 只会被执行一次
const [rows, setRows] = useState(() => createRows(props.count));
复制代码
以前不少具备反作用的操做,例如网络请求,修改 UI 等,通常都是在 class
组件的 componentDidMount
或者 componentDidUpdate
等生命周期中进行操做。而在函数组件中是没有这些生命周期的概念的,只能 return
想要渲染的元素。 可是如今,在函数组件中也有执行反作用操做的地方了,就是使用 useEffect
函数。
useEffect(() => { doSomething });
两个参数:
第一个是一个函数,是在第一次渲染以及以后更新渲染以后会进行的反作用。
第二个参数是可选的,是一个数组,数组中存放的是第一个函数中使用的某些反作用属性。用来优化 useEffect
虽然传递 [] 更接近熟悉的
componentDidMount
和componentWillUnmount
执行规则,但咱们建议不要将它做为一种习惯,由于它常常会致使错误。
假如此时咱们有一个需求,让 document 的 title 与 Example 中的 count 次数保持一致。
使用 class 组件:
Example.js
import React, { Component } from 'react';
export default class Example extends 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>
);
}
}
复制代码
而如今在函数组件中也能够进行反作用操做了。
Example.js
import React, { useState, useEffect } from 'react';
function Example() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
// 相似于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
});
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
);
}
export default Example;
复制代码
不只如此,咱们可使用 useEffect 执行多个反作用(可使用一个 useEffect 执行多个反作用,也能够分开执行)
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
});
const handleClick = () => {
console.log('鼠标点击');
}
useEffect(() => {
// 给 window 绑定点击事件
window.addEventListener('click', handleClick);
});
复制代码
如今看来功能差很少了。可是在使用类组件时,咱们通常会在
componentWillMount
生命周期中进行移除注册的事件等操做。那么在函数组件中又该如何操做呢?
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
});
const handleClick = () => {
console.log('鼠标点击');
}
useEffect(() => {
// 给 window 绑定点击事件
window.addEventListener('click', handleClick);
return () => {
// 给 window 移除点击事件
window.addEventListener('click', handleClick);
}
});
复制代码
能够看到,咱们传入的第一个参数,能够 return 一个函数出去,在组件被销毁时,会自动执行这个函数。
上面咱们一直使用的都是 useEffect
中的第一个参数,传入了一个函数。那么 useEffect
的第二个参数呢?
useEffect
的第二个参数是一个数组,里面放入在 useEffect 使用到的 state 值,能够用做优化,只有当数组中 state 值发生变化时,才会执行这个 useEffect
。
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
}, [ count ]);
复制代码
Tip:若是想模拟 class 组件的行为,只在 componetDidMount 时执行反作用,在 componentDidUpdate 时不执行,那么
useEffect
的第二个参数传一个 [] 便可。(可是不建议这么作,可能会因为疏漏出现错误)
const value = useContext(MyContext);
接受上下文对象(从中React.createContext返回的值)并返回该上下文的当前上下文值。当前上下文值由树中调用组件上方value最近的prop 肯定<MyContext.Provider>。
useContext(MyContext)
则至关于 static contextType = MyContext
在类中,或者 <MyContext.Consumer>
。
在 App.js
文件中建立一个 context
,并将 context
传递给 Example
子组件
App.js
import React, { createContext } from 'react';
import Example from './Example';
import './App.css';
export const ThemeContext = createContext(null);
export default () => {
return (
<ThemeContext.Provider value="light"> <Example /> </ThemeContext.Provider> ) } 复制代码
在 Example
组件中,使用 useContext
API 能够获取到传入的 context
值
Example.js
import React, { useContext } from 'react';
import { ThemeContext } from './App';
export default () => {
const context = useContext(ThemeContext);
return (
<div>Example 组件:当前 theme 是:{ context }</div>
)
}
复制代码
useContext必须是上下文对象自己的参数:
useContext(MyContext)只容许您阅读上下文并订阅其更改。您仍然须要<MyContext.Provider>在树中使用以上内容来为此上下文提供值。
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。 接受类型为 (state, action) => newState 的reducer
,并返回与 dispatch
方法配对的当前状态。
当你涉及多个子值的复杂
state
(状态) 逻辑时,useReducer
一般优于useState
。
Example.js
import React, { useReducer } from 'react';
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
export default () => {
// 使用 useReducer 函数建立状态 state 以及更新状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<> Count: {state.count} <br /> <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 复制代码
还能够懒惰地建立初始状态。为此,您能够将init函数做为第三个参数传递。初始状态将设置为 init(initialArg)
。
它容许您提取用于计算 reducer
外部的初始状态的逻辑。这对于稍后重置状态以响应操做也很方便:
Example.js
import React, { useReducer } from 'react';
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
export default ({initialCount = 0}) => {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<> Count: {state.count} <br /> <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 复制代码
state
状态值结构比较复杂时,使用 useReducer
更有优点。useState
获取的 setState
方法更新数据时是异步的;而使用 useReducer
获取的 dispatch
方法更新数据是同步的。针对第二点区别,咱们能够演示一下: 在上面 useState
用法的例子中,咱们新增一个 button
:
useState 中的 Example.js
import React, { useState } from 'react';
function Example() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={() => { setCount(count + 1); setCount(count + 1); }}> 测试可否连加两次 </button> </div>
);
}
export default Example;
复制代码
点击 测试可否连加两次 按钮,会发现,点击一次,
count
仍是只增长了 1,因而可知,useState
确实是 异步 更新数据;
在上面 useReducer
用法的例子中,咱们新增一个 button
: useReducer 中的 Example.js
import React, { useReducer } from 'react';
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
export default () => {
// 使用 useReducer 函数建立状态 state 以及更新状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<> Count: {state.count} <br /> <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => { dispatch({type: 'increment'}); dispatch({type: 'increment'}); }}> 测试可否连加两次 </button> </> ); } 复制代码
点击 测试可否连加两次 按钮,会发现,点击一次,
count
增长了 2,因而可知,每次dispatch 一个 action 就会更新一次数据,useReducer
确实是 同步 更新数据;
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
返回值 memoizedCallback
是一个 memoized
回调。传递内联回调和一系列依赖项。useCallback将返回一个回忆的memoized版本,该版本仅在其中一个依赖项发生更改时才会更改。当将回调传递给依赖于引用相等性的优化子组件以防止没必要要的渲染(例如shouldComponentUpdate)时,这很是有用。
这个 Hook 的 API 不可以一两句解释的清楚,建议看一下这篇文章:useHooks 第一期:聊聊 hooks 中的 useCallback。里面介绍的比较详细。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个memoized值。 传递“建立”函数和依赖项数组。useMemo只会在其中一个依赖项发生更改时从新计算memoized值。此优化有助于避免在每一个渲染上进行昂贵的计算。
useMemo在渲染过程当中传递的函数会运行。不要作那些在渲染时一般不会作的事情。例如,反作用属于useEffect,而不是useMemo。
useMemo
能够帮助咱们优化子组件的渲染,好比这种场景: 在 A 组件中有两个子组件 B 和 C,当 A 组件中传给 B 的 props
发生变化时,A 组件状态会改变,从新渲染。此时 B 和 C 也都会从新渲染。其实这种状况是比较浪费资源的,如今咱们就可使用 useMemo
进行优化,B 组件用到的 props 变化时,只有 B 发生改变,而 C 却不会从新渲染。
例子:
ExampleA.js
import React from 'react';
export default ({ text }) => {
console.log('Example A:', 'render');
return <div>Example A 组件:{ text }</div>
}
复制代码
ExampleB.js
import React from 'react';
export default ({ text }) => {
console.log('Example B:', 'render');
return <div>Example B 组件:{ text }</div>
}
复制代码
App.js
import React, { useState } from 'react';
import ExampleA from './ExampleA';
import ExampleB from './ExampleB';
import './App.css';
export default () => {
const [a, setA] = useState('ExampleA');
const [b, setB] = useState('ExampleB');
return (
<div>
<ExampleA text={ a } />
<ExampleB text={ b } />
<br />
<button onClick={ () => setA('修改后的 ExampleA') }>修改传给 ExampleA 的属性</button>
<button onClick={ () => setB('修改后的 ExampleB') }>修改传给 ExampleB 的属性</button>
</div>
)
}
复制代码
此时咱们点击上面任意一个按钮,都会看到控制台打印了两条输出, A 和 B 组件都会被从新渲染。
如今咱们使用 useMemo
进行优化
App.js
import React, { useState, useMemo } from 'react';
import ExampleA from './ExampleA';
import ExampleB from './ExampleB';
import './App.css';
export default () => {
const [a, setA] = useState('ExampleA');
const [b, setB] = useState('ExampleB');
+ const exampleA = useMemo(() => <ExampleA />, [a]);
+ const exampleB = useMemo(() => <ExampleB />, [b]);
return (
<div>
+ {/* <ExampleA text={ a } />
+ <ExampleB text={ b } /> */}
+ { exampleA }
+ { exampleB }
<br />
<button onClick={ () => setA('修改后的 ExampleA') }>修改传给 ExampleA 的属性</button>
<button onClick={ () => setB('修改后的 ExampleB') }>修改传给 ExampleB 的属性</button>
</div>
)
}
复制代码
此时咱们点击不一样的按钮,控制台都只会打印一条输出,改变 a 或者 b,A 和 B 组件都只有一个会从新渲染。
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。
注意:useRef() 比 ref 属性更有用。与在类中使用 instance(实例) 字段的方式相似,它能够 方便地保留任何可变值。
注意,内容更改时useRef 不会通知您。变异.current属性不会致使从新渲染。若是要在React将引用附加或分离到DOM节点时运行某些代码,则可能须要使用回调引用。
下面这个例子中展现了能够在 useRef()
生成的 ref
的 current
中存入元素、字符串
Example.js
import React, { useRef, useState, useEffect } from 'react';
export default () => {
// 使用 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 中输入内容,能够看到控制台打印出对应的内容。
useLayoutEffect(() => { doSomething });
与 useEffect
Hooks 相似,都是执行反作用操做。可是它是在全部 DOM 更新完成后触发。能够用来执行一些与布局相关的反作用,好比获取 DOM 元素宽高,窗口滚动距离等等。
进行反作用操做时尽可能优先选择 useEffect,以避免阻止视觉更新。与 DOM 无关的反作用操做请使用
useEffect
。
用法与 useEffect 相似。
Example.js
import React, { useRef, useState, useLayoutEffect } from 'react';
export default () => {
const divRef = useRef(null);
const [height, setHeight] = useState(100);
useLayoutEffect(() => {
// DOM 更新完成后打印出 div 的高度
console.log('useLayoutEffect: ', divRef.current.clientHeight);
})
return <> <div ref={ divRef } style={{ background: 'red', height: height }}>Hello</div> <button onClick={ () => setHeight(height + 50) }>改变 div 高度</button> </> } 复制代码
这里咱们就仿照官方的 useReducer
作一个自定义的 Hooks
。
在 src
目录下新建一个 useReducer.js
文件:
useReducer.js
import React, { useState } from 'react';
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
复制代码
tip: Hooks 不只能够在函数组件中使用,也能够在别的 Hooks 中进行使用。
好了,自定义 useReducer
编写完成了,下面咱们看一下能不能正常使用呢?
改写 Example 组件
Example.js
import React from 'react';
// 从自定义 useReducer 中引入
import useReducer from './useReducer';
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
export default () => {
// 使用 useReducer 函数建立状态 state 以及更新状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<> Count: {state.count} <br /> <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 复制代码
JavaScript
函数调用 Hooks
;Hooks
;Hooks
;React
功能组件调用 Hooks
;Hooks
中调用 Hooks
;Hooks
必须使用 use
开头,这是一种约定;根据上一段所写,在 React
中使用 Hooks
须要遵循一些特定规则。可是在代码的编写过程当中,可能会忽略掉这些使用规则,从而致使出现一些不可控的错误。这种状况下,咱们就可使用 React 提供的 ESLint 插件:eslint-plugin-react-hooks。下面咱们就看看如何使用吧。
$ npm install eslint-plugin-react-hooks --save
复制代码
// 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
}
}
复制代码