写在前面
It’s only after I stopped looking as the useEffect Hook through the prism of the familiar class lifecycle methods that everything came together for me. 当我不再透过熟悉的class生命周期方法去窥视
**useEffect**这个Hook的时候,我才得以融会贯通。 —Dan Abramov
约定
- use开头并紧随一个大写字母的函数,被识别为hooks,注意自定义hooks命名;
- hooks只能在React函数组件和自定义hooks中使用,不能在其他地方使用;
- hooks只能在顶层使用,不能在分支、循环中使用;
正确的配置依赖项减少bug的产生,不可完全依赖 eslint-plugin-react-hooks;
功能
useState(initialValue)
功能:定义state状态
const [state, setState] = useState(1);
注意事项
setState 需要手动加入未改变的值,this.setState 只传入改变的值;
- setState 是不变值,不需要放在 useEffect 等 hooks 的依赖中;
- useState 初始值尽量不调用函数,而是传入函数以免不必要的性能开销;
案例
function Table(props) {// ❌ createRows() 每次渲染都会被调用const [rows, setRows] = useState(createRows(props.count));}function Table(props) {// ✅ createRows() 只会被调用一次const [rows, setRows] = useState(() => createRows(props.count));}
useEffect(() => fn?, deps)
功能:执行有副作用函数
useEffect(() => {const handler = () => {};dom.addEventListener('click', handler, false);return () => {dom.removeEventListener('click', handler);}}, []);
注意事项
- 请按实际的功能分类useEffect,而不是像写componentDidMount一样按都堆在一起(DEMO);
- 不应在函数中执行阻塞浏览器更新屏幕的操作(执行顺序DEMO);
- 严格声明函数中的依赖项,防止出现bug或性能消耗;
- 注意,该函数返回的是一个函数,所以不能使用 async;
- 每次调用 useEffect 获取的值都是当前运行时状态,异步流程中很容易在这一点出错(DEMO);
- 返回的函数并不是仅在组件卸载时调用,每次通过diff校验都会调用上一次注册的返回函数;
- 首次渲染一定会执行,如果不必要首次执行请使用 ahooks 的 useUpdateEffect;
case1:函数遗漏易遗漏
const Page = (props) => {const [text, setText] = useState();const onInputChange = useCallback(({ target }) => {if(target.value.length < props.maxLength) {setText(target.value);}}, [props.maxLength]);useEffect(() => {props.onChange && props.onChange(text);}, [text]); // ❌ 这里遗漏了 props.onChangereturn <Input value={text} onChange={onChange} />;}
useContext(ReactContext)
功能:获取 React 上下文中的值
const context = React.createContext();const Page = () => {const contextValue = useContext(context);};
useMemo(fn, deps)
功能:类似 mobx 的 @computedState,优化耗时计算
const Square = () => {const [size, setSize] = useState();const onSizeChange = useCallback((e) => {setSize(e.target.value)}, []);const area = useMemo(() => size * size, [size]);return (<><Input onChange={onSizeChange} value={size} /><div>面积是: {area} </div></>);};
注意事项
useMemo 是 state 级别从优化,如果你想要类似 PrueComponent 功能,请使用
React.memo;// 继承上面Demoexport default React.memo(Square);
你可以把
useMemo作为性能优化的手段,但不要把它当成语义上的保证;useCallback(fn, deps)
功能:确保函数不会被频繁定义,优化函数定义
const Square = () => {const [size, setSize] = useState();const onSizeChange = useCallback((e) => {setSize(e.target.value)}, []);return <Input onChange={onSizeChange} value={size} />;};
useRef(ReactContext)
功能1:绑定组件的Ref句柄
功能2:持久化保存函数内的值,不受函数重复执行影响 ```javascript // 用法1 const Page = () => { const ref = useRef();return }
// 用法2 const usePrevious = (value) => { const previous = useRef(); const current = useRef(value);
previous.current = current; current.current = value;
return previous.current; };
// 用法2 const useSize = () => { const [node, setNode] = useState(null); const _ref = useCallback((ref) => { setNode(ref) });
return [_ref, size]; };
**注意事项**1. useRef 的初始值尽量不要调用函数,这一点和 useState 一样,会带来额外的性能消耗。但 useRef 并没有像 useState 一样提供函数形式的初始化方式,这一点需要注意;1. 监听 Ref 时应监听 ref 本身,而非 ref.current ([issue](https://github.com/facebook/react/issues/14387));1. 自定义 hooks 返回 ref 或者在通过 forwardRef传递 ref 时,请不要用 useRef 创建 ref,原因是 ref 是有在其创建组件内的自身独立的生命周期**案例**<br />**case1:上述注意事项3**```javascriptfunction useCustomHook() {const ref = useRef(null)const setRef = useCallback(node => {ref.current = node}, [])return setRef;}function Component() {const ref = useCustomHook();return <div ref={ref}>Ref element</div>}// 更简洁的方式function useSimpleHook() {const [node, setNode] = useState(null)return setNode;}function Component_simple() {const ref = useSimpleHook();return <div ref={ref}>Ref element</div>}
useReducer((state, action) => fn, initialState)
功能:类似 Redux 的 reducer
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();}}function Counter() {const [state, dispatch] = useReducer(reducer, initialState);return (<>Count: {state.count}<button onClick={() => dispatch({type: 'decrement'})}>-</button><button onClick={() => dispatch({type: 'increment'})}>+</button></>);}
注意事项
- dispatch 方法是不变的,不需要在 useEffect 等 hooks 的依赖项中添加
案例
case1:利用dispatch的不变性,做性能优化
const TodosDispatch = React.createContext(null);function TodosApp() {// 提示:`dispatch` 不会在重新渲染之间变化const [todos, dispatch] = useReducer(todosReducer);return (<TodosDispatch.Provider value={dispatch}><DeepTree todos={todos} /></TodosDispatch.Provider>);}
useLayoutEffect(() => fn?, deps)
功能:类似 useEffect,只是在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行
const Sticky = (props) => {const [placeholderStyle, setPlaceholderStyle] = useState({top: 0,});useLayoutEffect(() => {window.addEventListener('scorll', (e) => {if(/* 判断滚动的位置 */) {setPlaceholderStyle({ top: e.scrollTop });} else {setPlaceholderStyle({ top: 0 });}})}, [])return (<><div style={placeholderStyle} />{props.children}</>)}
注意事项
- SSR模式下慎用!
推荐优先使用 useEffect,当这个有问题时再考虑 useLayoutEffect;
useImperativeHandle(ref, createHandle, deps)
功能:向组件调用方提供内部方法 ```javascript const Input = (props, ref) => { const inputRef = useRef();
useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } }), []);
return ; }
export default React.forwardRef(Input);
const Page = () => { const inputRef = useRef(); useEffect(() => { inputRef.current.focus(); // 调用内部方法 }, [])
return }
<a name="5yR9A"></a>### useDebugValue(value,formatFn)功能:可用于在 React 开发者工具中显示自定义 hook 的标签```javascriptfunction useFriendStatus(friendID) {const [isOnline, setIsOnline] = useState(null);useDebugValue(isOnline ? 'Online' : 'Offline');return isOnline;}
注意事项
-
自定义hooks
const useReducer = (reducer, initialState) => {const [state, setState] = useState(initialState);const dispatch = (action) => {const nextState = reducer(state, action);setState(nextState);}return [state, dispatch];}
注意事项
命名一定要符合规范;
- 如 useState 提供的 setState 一样,该方法是不变值,可以有效减少 useEffect 依赖以提高性能。封装自定义 Hooks 也应该明确到各个返回值是否是不变值,以减少bug并提高性能;
useTransition
功能:实现延迟转场 ```javascript const SUSPENSE_CONFIG = { timeoutMs: 2000 };
function App() {
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
return (
<>
{isPending ? “ 加载中…” : null}
<a name="usedeferredvalue"></a>### useDeferredValue功能:用于在具有基于用户输入立即渲染的内容,以及需要等待数据获取的内容时,保持接口的可响应性。```javascriptfunction App() {const [text, setText] = useState("hello");const deferredText = useDeferredValue(text, { timeoutMs: 2000 });return (<div className="App">{/* 保持将当前文本传递给 input */}<input value={text} onChange={handleChange} />...{/* 但在必要时可以将列表“延后” */}<MySlowList text={deferredText} /></div>);}
经典案例
计时器
// ❌ 反例function Counter() {const [count, setCount] = useState(0);useEffect(() => {setInterval(() => {setCount(count + 1); // 这个 effect 依赖于 `count` state}, 1000);// 🔴 Bug: 未在卸载时清理计时器}, []); // 🔴 Bug: `count` 没有被指定为依赖return <h1>{count}</h1>;}
解法1:添加正确的依赖
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const id = setInterval(() => {setCount(count + 1);}, 1000);return () => clearInterval(id); // ✅ 正确清理计时器}, [count]); // ✅ 正确的添加依赖return <h1>{count}</h1>;}
解法2:完全移除依赖
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const id = setInterval(() => {setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量}, 1000);return () => clearInterval(id);}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量return <h1>{count}</h1>;}
解法3:提取到函数外层
const reducer = (state, action) => {switch(action){case 'add': return { count: state.count + 1 };case 'minus': return { count: state.count - 1 };default: return state;}}function Counter() {const [state, dispatch] = useReducer(reducer, {count: 0});useEffect(() => {const id = setInterval(() => {dispatch('add'); // ✅ 在这不依赖于外部的 `count` 变量}, 1000);return () => clearInterval(id);}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量return <h1>{state.count}</h1>;}
发送一个请求
发请求是一个非常常见的场景,除非你的需求就是一个静态页面。我们发一个请求:
// ❌ 反例function Component() {const [data, setData] = useState({});useEffect(async () => {const result = await axios(`https://hn.algolia.com/api/v1/getProfile`);setData(result.data);}, []);}
上面这个代码如果你写了会发现是报错的,因为 useEffect 中回调函数的返回值变成了一个Promise对象,这不符合react的要求(返回空或函数),所以我们需要做以下改写:
// ❌ 反例function Component() {const [data, setData] = useState({});const init = async () => {const result = await axios(`https://hn.algolia.com/api/v1/getProfile`);setData(result.data);};useEffect(() => {init();}, []);}
我们定义了一个函数 init 用来发起请求。发送请求是没问题了,但当请求有依赖参数时,为了解决init被反复重复定义的问题,你又得给他补充一个 useCallback。所以通常,react 推荐我们将请求发送函数定成在react内部:
// ❓ 正解useEffect(() => {const fetchData = async () => {const result = await axios(`https://hn.algolia.com/api/v1/search?query=${props.query}`);setData(result.data);};fetchData();}, [props.query]);
这样代码就很清晰了,这个 useEffect 负责发送这一个请求。当每次props.query发生改变,都会自动请求数据并更新值。但这个例子就真的没有问题了么?并不是的,这里还藏着一个竞态问题。试想 props.query 先变成 ‘react’,紧接着又变成 ‘vue’,而 ‘react’ 的查询结果在 ‘vue’ 的结果之后返回,状态依然会更新出错。因此我们需还要做个简单的改造,在每次渲染前取消上次请求:
// ✅ 正解useEffect(() => {const isCancelled = false;const fetchData = async () => {const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);!isCancelled && setData(result.data);};fetchData();return () => { isCancelled = true };}, [query]);
不要过于依赖ESLint
React 官方提供了对于 hooks 语法的 ESLint 插件,正确这个插件可以完成以下校验:
- hooks 不能用于分支、循环以及非组件和自定义Hook的其他函数中;
- useXXX 中的依赖项不完整
这里第二项在大部分情况下是好东西,但它也会引入bug:
useEffect(() => {// 预期,页面加载时请求接口,后续仅在需用户点击刷新再发送axios(`https://hn.algolia.com/api/v1/search?query=${prop.query}`).then(result => {setData(result.data);});}, []); // 如果你开立了自动fix, 插件会在这里自动添加 props.query 参数
用 Hooks 的方式思考
这一节对理解 hooks 的思想很重要,假定我们有一个组件 DatePicker,现在用两种方式使用这个组件:
传统思维
const Page = () => {const [date, setDate] = useState<Moment>(moment('2021-01-01'));cosnt [isValid, setValid] = useState(false);const onDateChange = useCallback((date: Moment) => {setDate(date);checkValid(date) && setValid(true); // isValid 用来校验日期是否符合业务要求,控制提交按钮状态}, []);return (<><DatePicker value={date} onChange={onDateChange} /><Button disabled={!isValid}>提交</Button></>);}
Hooks 思维
const Page = () => {const [date, setDate] = useState<Moment>(moment('2021-01-01'));cosnt [isValid, setValid] = useState(false);useEffect(() => {checkValid(date) && setValid(true);}, [date]);return (<><DatePicker value={date} onChange={setDate} /><Button disabled={isValid}>提交</Button></>);}
问题所在
上述计时器,在class中的实现(DEMO)
class Counter extends React.Component{state = { count: 0 }componentDidMount() {this.id = setInterval(() => {this.setState(this.state.count + 1);}, 1000);}componentWillUnmount() {clearInterval(this.id);}render() {return <h1>{this.state.count}</h1>;}}
用 class 的习惯性思维改造成 hooks,就bug了(DEMO)
// ❌ 反例function Counter() {const [count, setCount] = useState(0); // state 搬过来// 写一个 didMount/willUnmount 类型的 useEffectuseEffect(() => {// componentDidMount 内容搬过来const id = setInterval(() => {setCount(count + 1);}, 1000);// componentWillUnmount 内容搬过来return () => clearInterval(id);}, []);return <h1>{count}</h1>; // render 搬过来}
Q&A
useEffect(fn, []) 和 comonentDidMount/comonentDidUpdate 一样么?
不完全一样,useEffect会捕获 props和state。所以即便在回调函数里,你拿到的还是初始的 props 和state。如果你想得到“最新”的值,你可以使用ref。不过,通常会有更简单的实现方式,所以你并不一定要用ref。(HOOKS | CLASS | see CLASS with HOOKS)
hooks的本质是什么?
闭包,例如setState,其最简实现可以是这样的
// 简化理解,非React实际实现const useState = (initial) => {const initialState = typeof initial === function ? initial() : initial;let state = initialState;const setState = (newState) => {state = typeof newState === function ? newState(state) : newState;rework(); // 每次setState后重新执行组件函数};return [state, setState];}
为啥调用hooks是依赖顺序的?
上个问题的代码可以看出一段很明显的错误,第4行,这里这样赋值在rework时又会被重新设置成初始值,导致第7行的回调拿到的永远是初始值。所以React是这样定义Hooks的(ReactFiberHooks.js) :
export type Hook = {memoizedState: any, // 指向当前渲染节点 Fiber, 上一次完整更新之后的最终状态值baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newStatebaseUpdate: Update<any, any> | null, // 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯queue: UpdateQueue<any, any> | null, // 缓存的更新队列,存储多次更新行为next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks};
哪些功能class Component可以做,hooks做不了
componentDidCatch、getDerivedStateFromError、getSnapshotBeforeUpdate 功能暂时无法实现。
deps监听是深比较还是浅比较?
是遵循 Object.is() 规则的浅比较,因此 immutable data 在这里依然很重要。
为什么hooks方式我拿不到前一次state和props值
因为每一次重复渲染使用的都是独立的,不变的上下文,所以你几乎不需要比较值。
三方Hooks
react-use
react-adaptive-hooks
@umijs/hooks (ahooks)
- useRequest(强制使用)
- useUpdateEffect(优先使用)
- 尽量用useToggle代替useBoolean
- useRouteMatch(不常用)
- useParams(常用)
- useHistory(常用)
- useLocation(常用)
