react hook
动机
● 嵌套地狱(the wrapper hell) - 在组件之间复用状态逻辑困难
● 逻辑分离 - 复杂组件变得难以理解
● class - 难以理解的 class
函数组件
定义函数组件时,通过函数的参数来传递属性,一般我们设为props。
// Function component
function HelloComponent(props) {
const { name } = props;
return <p>Hello {name}!</p>
}
<HelloComponent name="World" />
函数组件特性
● 没有状态,组件里的逻辑比较简单 (没有React Hook API之前)
● 当props得到更新时,重新渲染组件,因此计算复杂性比较高的函数,不建议放在函数组件里,(可暂存计算结果)。
● 复用性比较高
hook
利用Hook API,函数组件可以管理组件自身的状态,可以执行副作用函数。在一些特定的场景下,让函数组件更优雅地方式处理逻辑。
● useState
● useEffect
● useContext
● useReducer
● useCallback
● useMemo
● useRef
● useImperativeHandle
● useLayoutEffect
● useDebugValue
注意
● 类组件无法使用Hook API
● 函数组件支持大部分场景,但现阶段无法完全替代类组件
● 只许在函数组件里的top层执行Hooks,即不能在判断,循环,或者函数里执行hooks。
● 只许在函数组件,或者自定义hook里可以执行hooks,不可以在普通的函数里执行hooks。
useState Hook
useState Hook支持函数组件定义状态,并提供更新状态的函数。
const [state, setState] = useState(initialState);
useState的参数initialState是状态state的初始值,useState返回的值是数组,数组的第一个元素状态的初始值,第二个元素是赋值函数setState。一般利用解构赋值来给状态变量和赋值函数定义名称。
● 每当重新渲染时,赋值函数useState返回的状态值(第一个元素)是函数setState最近更新的值
● 状态state的值可以是基本类型
● 可以定义多个状态
● 当useState返回的赋值函数setState的参数,与当前的state相同时,组件不会重新渲染。相反,类组件里通过setState来更新状态时,不管state是否相同,组件都会执行渲染函数(setState的参数不是null或者undefined,没有继承PureComponent,或shouldComponentUpdate中没有添加判断条件)。
// Function Component
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click me</button>
</div>
);
};
初始化函数 Lazy initial state
useState参数可以是一个函数,函数返回的值是状态的初始值。该初始化函数的用处是当组件只有首次渲染时,执行初始化函数,后续不会再执行,因此我们可以通过这个特性来优化组件,使减少不必要的重复计算。
const [state, setState] = useState(() => heavyComputation(props));
setState参数 Functional updates
可以选择函数作为函数setState的参数,这时,函数的参数为当前的状态state,返回的值想要更新的值。
useEffect(() => {
const intervalId = setInterval(() => {
setTime(timeStr => timeStr + 1000);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
useReducer
useReducer类似于useState,useReducer返回的第一个元素是状态state,第二个元素是dispatch。
const reducer = (state, action) => newState;
const [state, dispatch] = useReducer(reducer, initialArg, init);
● 状态state变化依赖于前一个状态
● 状态state为复杂类型
● 把复杂的逻辑放到reducer来单独管理,提高组件的复用性和可扩展性
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
break;
}
};
const Counter = () => {
const [count, dispatch] = useReducer(reducer, 0);
const add = () => {
dispatch({
type: "INCREMENT"
});
};
const substract = () => {
dispatch({
type: "DECREMENT"
});
};
return (
<div>
<h1>Counter {count}</h1>
<button onClick={add}>+1</button>
<button onClick={substract}>-1</button>
</div>
);
};
export default Counter;
初始化函数 Lazy Initialization
与useState一样,useReducer也可以通过初始化函数来获得初始值。
● 只有首次渲染时执行函数,返回的值是state的初始值。
● 第二个参数变为初始化函数的参数
● 第三个参数是初始化函数
function init(initialCount) {
return {count: initialCount};
}
const [state, dispatch] = useReducer(reducer, initialCount, init);
useState Vs useReducer
useState适用场景
- 状态state是基本类型
- 组件的逻辑比较简单
- 组件的各个状态之间相互没有联系,并通过各自的useState来管理
useReducer适用场景
- 状态state是对象(组件的几个状态绑定在一起)或者数组
- 组件逻辑较复杂,想把逻辑封装到reducer里
- 对测试更友好
Effect Hook
useEffect支持函数组件可执行副作用函数或异步任务,比如调取接口,setInterval等。
● 函数组件渲染结束之后,执行useEffect
● 默认情况下,每渲染组件时都会执行useEffect
● 函数组件里可以有多个useEffect
清理工作 Cleanup
类组件中,在componentWillUnmount里做一些清理工作,类似的,在函数组件里可以做清理工作。当函数组件重新渲染或销毁前,会执行useEffect返回的函数。
// function component with cleanup
useEffect(() => {
const intervalId = setInterval(() => {
setTime(timeStr => timeStr + 1000);
}, 1000);
return () => {
clearInterval(intervalId);
};
});
注意:默认情况下,重新渲染导致执行useEffect里的回调函数和返回的函数。
useEffect的第二个参数
useEffect的第二个参数为空时,组件每次重新渲染都会执行useEffect里的回调函数。有时,我们不需要每次重新渲染时,执行useEffect,当useEffect里的某几个变量发生变化时,再执行useEffect,这样就可以减少无用的计算。
const { num } = props;
useEffect(() => {
const value = getFibonacciNum(Number(num));
setComputedValue(value);
}, [num]);
注意
● useEffect是用了Object.is()方法来判断变量是否发生变化。
● useEffect里用到的变量都需要加到第二个参数数组里才能正确执行。
● 当useEffetc的第二个参数是空数组时,只有首次渲染时执行useEffect。
Custom Hooks
有Hooks之前,组件通过高阶组件或者render属性来复用代码,这会容易导致嵌套地狱问题。利用自定义hook也可实现复用代码,而且比高阶组件更优雅。
开发人员可以创建自定义hook,利用已有的hooks,把复用的功能从组件里分离出来,并且把这些功能按需添加到函数组件。
自定义hook的函数名以"use"为开头,并在函数体里用到了react的hooks,linter插件会自动按照hook的标准检查自定义hook。
注意
● 推荐hook名称用"use"来开头
● 两个组件不会共享自定义hook里的状态
● 组件渲染完成后,执行自定义组件里的useEffect
举例 - useDataApi
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
} catch (error) {
dispatch({ type: 'FETCH_FAILURE' });
}
};
fetchData();
}, [url]);
return [state, setUrl];
};
定义Reducer
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
};
default:
throw new Error();
}
};
调用useDataApi
const [{ data, isLoading, isError }, setUrl] = useDataApi(url);
useRef
useRef返回的是简单对象(plain object),且对象的current属性值为useRef的参数。直到函数组件销毁前,该对象一直存在。对象ref有点类似于类组件的this,可以用作容器来使用。当改变对象ref时,不会引起渲染。
// 保存之前状态state
function Demo() {
const [state, setState] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = state;
}, [state]);
//...
}
注意
● 不要在渲染期间,更新ref对象
Hooks 性能优化
组件性能优化的思路,主要通过以下两种方法来实现
● 缓存组件 - 减少不必要的render次数,执行render时,React会触发协调(Reconciliation)。
● 缓存计算结果- 减少不必要的重复计算
useMemo - 减少不必要的重复计算
useMemo可以缓存计算结果,函数的参数不变时,useMemo返回缓存的结果,只有当函数的参数发生变化时,才会重新计算。
第二个参数是函数的依赖项,一般把函数参数加到依赖项当中,显然当依赖项为空数组时,useMemo只返回第一次计算的结果。
const memoizedValue = useMemo(() => heavyComputation(a, b), [a, b]);
也可以缓存组件
const memoizedMyComponent = useMemo(() => <MyComponent value={value} />, [value]);
React.memo - 减少不必要的render
memo是一个高阶组件,当组件的props没有发生变化时,组件不会再执行render方法,返回上一次render的结果。
const MyComponent = props => {}
export default React.memo(Mycomponent);
默认情况下,memo对比props时是浅对比(shallow compare),我们可以自定义对比函数。props相同时返回true, memo返回已缓存的组件;不同时返回false,返回更新的组件。需要注意的是,逻辑与shouldComponentUpdate正好相反。
const isEqual = (prevProps, nextProps) => {
// compare the arguments
if (prevProps相等于nextProps) {
return true;
} else {
return false;
}
};
export default React.memo(Mycomponent, isEqual);
useCallback - 减少不必要的render
把函数以及依赖项作为参数传入 useCallback,将返回函数的 memoized 版本,useCallback只有当依赖项有变化的时候返回新的函数。
const callback = () => {
doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])