(阅读指南:建议将每个小demo都手动执行一遍,保证你会不虚此行。)
Hook是React 16.8(包括react-dom 16.8)新增的特性,它可以让你在不编写class的情况下使用state及其它的React特性,Hook是一个特殊的函数。
React-router 从V5.1开始存在Hook方法并支持hook。
React Redux 从 v7.1.0 开始支持 Hook API 并暴露了 useDispatch
和 useSelector
等 hook。
接下来详细的说说各个API,并给出demo,觉得不错那就点个鼓励一下。
个人强烈建议:眼过千遍,不如手过一遍。
useState用来初始化state参数
import React from 'react';
function Example(){
// 声明一个count的state变量
const [count, setCount] = useState(0);
return (
点击{count}次
)
}
useState功能:
var demoData = useState(0);
var count = demoData[0]; // 第一个值为当前的state
var setCount = demoDate[1]; // 第二个值为更新state的函数
useEffect在函数组件中执行副作用(钩子函数)的操作
语法:useEffect(() => {}, [])
接收两个参数,第一个函数,第二个为数组;
import React, { useState, useEffect } from 'React';
function Example(){
const [count, setCount] = useState(0);
const btnClick = () => {
setCount(count + 1);
}
useEffect(() => {
console.log('执行了useEffect');
document.title = `点击了{count}次`
})
return (
点击{count}次
)
}
提示:熟悉React Class的生命周期函数,可以把useEffect Hook看做是componentDidMount,componentDidUpdate和componentWillUnmount这三个函数的组合。
useEffect在每次渲染页面后都执行。默认情况下,它在第一次渲染之后和每次更新之后都会执行;同一个函数组件中可以同时出现多个useEffect,React将按照useEffect声明的顺序依次调用组件中的每一个effect。
无需清除的effect
class的写法,即不需要在componentWillUnmount中执行操作
componentDidMount(){
document.title = `点击了{count}次`;
}
componentDidUpdate(){
document.title = `点击了{count}次`;
}
hook的写法
useEffect(() => {
document.title = `点击了{count}次`;
})
需要清除的effect
即就是需要在componentWillUnmount中清除掉,例如我们使用setInterval来更新当前时间。代码如下
class FriendStatus extends React.Component{
constructor(props){
super(props);
this.state = { nowTime: null};
this.timer = null;
}
componentDidMount(){
this.timer = setInterval(() => {
this.setState({
nowTime: new Date()
})
}, 1000)
}
componentWillUnmount(){
if (this.timer !== null) {
clearInterval(timer);
}
}
render(){
let time = this.state.nowTime;
return(
{time.toString()}
)
}
}
使用hook,如下:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [nowTime, setNowTime] = useState(new Date());
useEffect(() => {
let timer = setInterval(() => {
setNowTime(new Date())
}, 1000)
return () => { // 返回一个清理函数
clearInterval(timer);
};
}, []);
return({nowTime.toString()})
}
通过跳过Effect来进行性能优化
我们知道每次渲染后都执行清理或者执行effect会导致性能问题,在class中我们在componentDidUpdate中添加prevProps和prevState的比较逻辑来解决;
componentDidUpdate(prevProps, prevState){
if(this.state.count !== prevState.count){
document.title = `点击了{this.state.count}次`
}
}
在hook中使用effect
useEffect(() => {
console.log('执行了--useEffect')
document.title = `点击了{count}次`;
}, [count]); // 在初次渲染和count发生变化时更新
如果第二数组参数为[],则Effect会在初次渲染执行一次及包含清除函数Effect再执行一次(可将上述代码中的[count]替换为[]测试);下面分别给出两种情况的执行代码:
// 带清除函数即为useEffect的第一个参数(函数)中再返回一个函数
// 不带清除函数+第二个参数为[];
// --> 整个生命周期只执行一次,相当于componentDidMount;
useEffect(() => {
console.log('执行了--useEffect');
document.title = `点击了${count}次`;
}, []);
// 带清除函数+第二个参数为[];
// --> 整个生命周期中执行了两次,相当于componentDidMount和componentWillUnmount;
useEffect(() => {
console.log('执行了--useEffect');
document.title = `点击了${count}次`;
return () => { // 相当于componentWillUnmount;
console.log('执行了--清除函数');
document.title = "";
}
}, [])
如果不加第二个数组参数,则Effect除了会在初次渲染执行一次外,还会在每次更新都执行。
接收一个由React.createContext()创建的context对象(此处定义为Mycontext),并返回这个context属性vaule绑定的值;通过useContext获取到最近的
// Mycontext为React.createContext的返回值
const Mycontext = React.createContext();
...
--------------------------
const value = useContext(Mycontext);
调用了useContext的组件总会在context值变化时重新渲染。
完整的demo如下:
import React, { useContext } from 'react';
const themes = {
light: {
color: '#ddd',
background: 'yellow'
},
dark: {
color: '#fff',
background: '#333'
}
}
const ThemeContext = React.createContext();
function TestContext(){
return (
)
}
function Child(){
const theme = useContext(ThemeContext);
return (
测试一下useContext
)
}
function Father(){
return (
)
}
export default TestContext;
语法:
const [state, dispatch] = useReducer(reducer, initalArg, init);
是useState的替代方案。在某些场景下,useReducer会比useState更适用。(熟悉Redux,就知道它怎么工作啦)
完整demo:
import React, { useReducer } from 'react';
const init = {
count: 0
};
function reducer(state, action){
switch(action.type){
case 'add':
return {count: state.count + 1};
case 'minus':
return {count: state.count - 1};
default: throw new Error();
}
}
function TestReducer(){
const [state, dispatch] = useReducer(reducer, init);
return (
count: {state.count}
)
}
export default TestReducer;
语法:
const refObj = useRef(initVal);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initValue
)。返回的 ref 对象在组件的整个生命周期内保持不变;
完整demo:
import React, { useRef } from 'react';
export default function TestUseRef(){
// const InputEl = React.createRef(null);
const InputEl = useRef(null);
const getInputFocus = () => {
InputEl.current.placeholder = "输入中";
InputEl.current.focus();
}
return (
)
}
测试发现跟React.createRef()功能相同。
语法:返回一个回调函数;
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b])
useCallback(fn, deps) 相当于useMemo(() => fn, deps),该回调函数仅在某个依赖项发生改变时才会更新。
完整demo:
import React, { useState, useCallback, useEffect } from 'react';
function TestUseCallback(){
const [ count, setCount ] = useState(0);
const [ inputVal, setInputVal ] = useState('');
const backCount = useCallback(() => {
return count;
}, [count]);
return (
setInputVal(e.target.value)} />
)
}
function Child(props){
const {count, callback} = props;
const [mycount, setMycount] = useState(() => callback());
useEffect(() => {
setMycount(callback());
}, [callback]);
return (
child组件
myCount---{mycount}
)
}
export default TestUseCallback;
useMemo接收一个自定义的函数和数组参数,仅在某个数组参数发生改变时才返回值并触发render,这种优化有助于避免在每次渲染时都进行高开销的计算;
语法:返回一个值;
const memoizedValue = useMemo(() => computedExpensiveValue(a, b),[a,b])
不使用useMemo的完整demo:
import React, { useState, useMemo } from 'react';
function TestUseMemo(){
const [count, setCount] = useState(0);
const [value, setValue] = useState('');
const AddSum = () => {
console.log('addSum');
let sum = 0;
for(let i = 0; i < count*1000; i++){
sum += i;
}
return sum;
}
return (
点击了{count}次
计算求和所得{AddSum()}
setValue(event.target.value)}/>
)
}
export default TestUseMemo;
发现无论是修改count,还是修改value,都会触发AddSum()函数进行计算;
使用useMemo的完整demo:
import React, { useState, useMemo } from 'react';
function TestUseMemo(){
const [count, setCount] = useState(0);
const [value, setValue] = useState('');
const AddSum = useMemo(() => {
console.log('useMemo');
let sum = count;
for(let i = 0; i < count*1000; i++){
sum += i;
}
return sum;
}, [count]); // 只在count值发生变化时才重新计算值
return (
点击了{count}次
计算求和所得{AddSum}
setValue(event.target.value)}/>
)
}
export default TestUseMemo;
通过使用useMemo,我们发现只有在count值发生变化时才重新执行AddSum进行计算并重新渲染,从而避免了不必要的性能开销。
语法与useEffect一样,但是与useEffect的执行时机不一样;
完整demo验证:
import React, { useState, useEffect, useRef, useLayoutEffect } from 'react';
export default function TestuseLayout(){
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const InputRef = useRef(null);
useEffect(() => {
console.log(InputRef, 'start useEffect');
document.title = `${count} times`;
return () => {
console.log(InputRef, 'end useEffect');
document.title = 'remove';
}
});
useLayoutEffect(() => {
console.log(InputRef, 'start useLayoutEffect');
document.title = `${count} times`;
return () => {
console.log(InputRef, 'end useLayoutEffect');
document.title = 'Layout remove';
}
})
return (
You clicked {count} times
setText(e.target.value)}/>
)
}
看执行结果如下:
结论:
useImperativeHandle可以让你在使用ref时将自定义的实例值暴露给父组件,应尽量避免使用ref,useImperativeHandle应当与forwardRef一起使用。
语法:
useImperativeHandle(ref, createHandle, [deps]);
完整demo:
import React, { useImperativeHandle, useRef, forwardRef} from 'react';
function TestuseImper(){
const childRef = useRef(null);
const btnFn = () => {
console.log('childRef=', childRef);
childRef.current.focus();
}
return (
)
}
function Child(props, ref){
const InputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => InputRef.current.focus();
}))
return (
)
}
Child = forwardRef(Child);
export default TestuseImper;
上述demo执行后,我们发现父组件可以通过暴露的ref来获取子组件的ref,并进行修改。
语法:
useDebugValue(value)
完整demo:
import React, { useState, useDebugValue } from 'react';
export default function TestuseLayout(){
function useMyCount(num) {
const [ count, setCount ] = useState(0);
useDebugValue(count > num ? '溢出' : '不足');
const myCount = () => {
setCount(count + 2);
}
return [ count, myCount ];
}
const [count , myCount] = useMyCount(10);
return (
You clicked {count} times
)
}
自定义Hook可以让组件之间复用一些状态逻辑,在class方式下通常采用高阶组件来实现,而自定义Hook可以让我们在不增加组件的情况下达到同样的目的,可以将组件逻辑提取到可重用的函数中;下面提供一个简单的自定义hook完整demo:
import React, { useState } from 'react';
function useChangeCount(init){
// 自定义hook,名字必须以use开头
const [count, setCount] = useState(init);
const plus = () => {
setCount(count + 1);
}
const minus = () => {
setCount(count - 1);
};
const reset = () => {
setCount(init);
};
return [count, {plus, minus, reset}];
}
function SelfHook(){
const [count, setCount] = useChangeCount(0);
return (
当前count: {count}
)
}
export default SelfHook;
更多自定义Hook可阅读 React-use源码
不要在循环,条件或嵌套函数中调用Hook;
调用hook的地方
注意:要启用Hook,所有React相关的package都必须升级到16.8.0或者更高的版本。否则,Hook将无法运行。
官方React-Hook
Hooks FAQ