写在前面

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

约定

  1. use开头并紧随一个大写字母的函数,被识别为hooks,注意自定义hooks命名;
  2. hooks只能在React函数组件和自定义hooks中使用,不能在其他地方使用;
  3. hooks只能在顶层使用,不能在分支、循环中使用;
  4. 正确的配置依赖项减少bug的产生,不可完全依赖 eslint-plugin-react-hooks;

    功能

    useState(initialValue)

    功能:定义state状态

    1. const [state, setState] = useState(1);

    注意事项

  5. setState 需要手动加入未改变的值,this.setState 只传入改变的值;

  6. setState 是不变值,不需要放在 useEffect 等 hooks 的依赖中;
  7. useState 初始值尽量不调用函数,而是传入函数以免不必要的性能开销;

案例

  1. function Table(props) {
  2. // ❌ createRows() 每次渲染都会被调用
  3. const [rows, setRows] = useState(createRows(props.count));
  4. }
  5. function Table(props) {
  6. // ✅ createRows() 只会被调用一次
  7. const [rows, setRows] = useState(() => createRows(props.count));
  8. }

useEffect(() => fn?, deps)

功能:执行有副作用函数

  1. useEffect(() => {
  2. const handler = () => {};
  3. dom.addEventListener('click', handler, false);
  4. return () => {
  5. dom.removeEventListener('click', handler);
  6. }
  7. }, []);

注意事项

  1. 请按实际的功能分类useEffect,而不是像写componentDidMount一样按都堆在一起(DEMO);
  2. 不应在函数中执行阻塞浏览器更新屏幕的操作(执行顺序DEMO);
  3. 严格声明函数中的依赖项,防止出现bug或性能消耗;
  4. 注意,该函数返回的是一个函数,所以不能使用 async;
  5. 每次调用 useEffect 获取的值都是当前运行时状态,异步流程中很容易在这一点出错(DEMO);
  6. 返回的函数并不是仅在组件卸载时调用,每次通过diff校验都会调用上一次注册的返回函数;
  7. 首次渲染一定会执行,如果不必要首次执行请使用 ahooks 的 useUpdateEffect;

case1:函数遗漏易遗漏

  1. const Page = (props) => {
  2. const [text, setText] = useState();
  3. const onInputChange = useCallback(({ target }) => {
  4. if(target.value.length < props.maxLength) {
  5. setText(target.value);
  6. }
  7. }, [props.maxLength]);
  8. useEffect(() => {
  9. props.onChange && props.onChange(text);
  10. }, [text]); // ❌ 这里遗漏了 props.onChange
  11. return <Input value={text} onChange={onChange} />;
  12. }

useContext(ReactContext)

功能:获取 React 上下文中的值

  1. const context = React.createContext();
  2. const Page = () => {
  3. const contextValue = useContext(context);
  4. };

useMemo(fn, deps)

功能:类似 mobx 的 @computedState,优化耗时计算

  1. const Square = () => {
  2. const [size, setSize] = useState();
  3. const onSizeChange = useCallback((e) => {
  4. setSize(e.target.value)
  5. }, []);
  6. const area = useMemo(() => size * size, [size]);
  7. return (
  8. <>
  9. <Input onChange={onSizeChange} value={size} />
  10. <div>面积是: {area} </div>
  11. </>
  12. );
  13. };

注意事项

  1. useMemo 是 state 级别从优化,如果你想要类似 PrueComponent 功能,请使用 React.memo

    1. // 继承上面Demo
    2. export default React.memo(Square);
  2. 你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证;

    useCallback(fn, deps)

    功能:确保函数不会被频繁定义,优化函数定义

    1. const Square = () => {
    2. const [size, setSize] = useState();
    3. const onSizeChange = useCallback((e) => {
    4. setSize(e.target.value)
    5. }, []);
    6. return <Input onChange={onSizeChange} value={size} />;
    7. };

    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. **注意事项**
  2. 1. useRef 的初始值尽量不要调用函数,这一点和 useState 一样,会带来额外的性能消耗。但 useRef 并没有像 useState 一样提供函数形式的初始化方式,这一点需要注意;
  3. 1. 监听 Ref 时应监听 ref 本身,而非 ref.current ([issue](https://github.com/facebook/react/issues/14387));
  4. 1. 自定义 hooks 返回 ref 或者在通过 forwardRef传递 ref 时,请不要用 useRef 创建 ref,原因是 ref 是有在其创建组件内的自身独立的生命周期
  5. **案例**<br />**case1:上述注意事项3**
  6. ```javascript
  7. function useCustomHook() {
  8. const ref = useRef(null)
  9. const setRef = useCallback(node => {
  10. ref.current = node
  11. }, [])
  12. return setRef;
  13. }
  14. function Component() {
  15. const ref = useCustomHook();
  16. return <div ref={ref}>Ref element</div>
  17. }
  18. // 更简洁的方式
  19. function useSimpleHook() {
  20. const [node, setNode] = useState(null)
  21. return setNode;
  22. }
  23. function Component_simple() {
  24. const ref = useSimpleHook();
  25. return <div ref={ref}>Ref element</div>
  26. }

useReducer((state, action) => fn, initialState)

功能:类似 Redux 的 reducer

  1. const initialState = {count: 0};
  2. function reducer(state, action) {
  3. switch (action.type) {
  4. case 'increment':
  5. return {count: state.count + 1};
  6. case 'decrement':
  7. return {count: state.count - 1};
  8. default:
  9. throw new Error();
  10. }
  11. }
  12. function Counter() {
  13. const [state, dispatch] = useReducer(reducer, initialState);
  14. return (
  15. <>
  16. Count: {state.count}
  17. <button onClick={() => dispatch({type: 'decrement'})}>-</button>
  18. <button onClick={() => dispatch({type: 'increment'})}>+</button>
  19. </>
  20. );
  21. }

注意事项

  1. dispatch 方法是不变的,不需要在 useEffect 等 hooks 的依赖项中添加

案例
case1:利用dispatch的不变性,做性能优化

  1. const TodosDispatch = React.createContext(null);
  2. function TodosApp() {
  3. // 提示:`dispatch` 不会在重新渲染之间变化
  4. const [todos, dispatch] = useReducer(todosReducer);
  5. return (
  6. <TodosDispatch.Provider value={dispatch}>
  7. <DeepTree todos={todos} />
  8. </TodosDispatch.Provider>
  9. );
  10. }

useLayoutEffect(() => fn?, deps)

功能:类似 useEffect,只是在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行

  1. const Sticky = (props) => {
  2. const [placeholderStyle, setPlaceholderStyle] = useState({
  3. top: 0,
  4. });
  5. useLayoutEffect(() => {
  6. window.addEventListener('scorll', (e) => {
  7. if(/* 判断滚动的位置 */) {
  8. setPlaceholderStyle({ top: e.scrollTop });
  9. } else {
  10. setPlaceholderStyle({ top: 0 });
  11. }
  12. })
  13. }, [])
  14. return (
  15. <>
  16. <div style={placeholderStyle} />
  17. {props.children}
  18. </>
  19. )
  20. }

注意事项

  1. SSR模式下慎用!
  2. 推荐优先使用 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 }

  1. <a name="5yR9A"></a>
  2. ### useDebugValue(value,formatFn)
  3. 功能:可用于在 React 开发者工具中显示自定义 hook 的标签
  4. ```javascript
  5. function useFriendStatus(friendID) {
  6. const [isOnline, setIsOnline] = useState(null);
  7. useDebugValue(isOnline ? 'Online' : 'Offline');
  8. return isOnline;
  9. }

注意事项

  1. 谨慎相关代码发到生产环境

    自定义hooks

    1. const useReducer = (reducer, initialState) => {
    2. const [state, setState] = useState(initialState);
    3. const dispatch = (action) => {
    4. const nextState = reducer(state, action);
    5. setState(nextState);
    6. }
    7. return [state, dispatch];
    8. }

    注意事项

  2. 命名一定要符合规范;

  3. 如 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} }> </> ); }

  1. <a name="usedeferredvalue"></a>
  2. ### useDeferredValue
  3. 功能:用于在具有基于用户输入立即渲染的内容,以及需要等待数据获取的内容时,保持接口的可响应性。
  4. ```javascript
  5. function App() {
  6. const [text, setText] = useState("hello");
  7. const deferredText = useDeferredValue(text, { timeoutMs: 2000 });
  8. return (
  9. <div className="App">
  10. {/* 保持将当前文本传递给 input */}
  11. <input value={text} onChange={handleChange} />
  12. ...
  13. {/* 但在必要时可以将列表“延后” */}
  14. <MySlowList text={deferredText} />
  15. </div>
  16. );
  17. }

经典案例

计时器

  1. // ❌ 反例
  2. function Counter() {
  3. const [count, setCount] = useState(0);
  4. useEffect(() => {
  5. setInterval(() => {
  6. setCount(count + 1); // 这个 effect 依赖于 `count` state
  7. }, 1000);
  8. // 🔴 Bug: 未在卸载时清理计时器
  9. }, []); // 🔴 Bug: `count` 没有被指定为依赖
  10. return <h1>{count}</h1>;
  11. }

解法1:添加正确的依赖

  1. function Counter() {
  2. const [count, setCount] = useState(0);
  3. useEffect(() => {
  4. const id = setInterval(() => {
  5. setCount(count + 1);
  6. }, 1000);
  7. return () => clearInterval(id); // ✅ 正确清理计时器
  8. }, [count]); // ✅ 正确的添加依赖
  9. return <h1>{count}</h1>;
  10. }

解法2:完全移除依赖

  1. function Counter() {
  2. const [count, setCount] = useState(0);
  3. useEffect(() => {
  4. const id = setInterval(() => {
  5. setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
  6. }, 1000);
  7. return () => clearInterval(id);
  8. }, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量
  9. return <h1>{count}</h1>;
  10. }

解法3:提取到函数外层

  1. const reducer = (state, action) => {
  2. switch(action){
  3. case 'add': return { count: state.count + 1 };
  4. case 'minus': return { count: state.count - 1 };
  5. default: return state;
  6. }
  7. }
  8. function Counter() {
  9. const [state, dispatch] = useReducer(reducer, {
  10. count: 0
  11. });
  12. useEffect(() => {
  13. const id = setInterval(() => {
  14. dispatch('add'); // ✅ 在这不依赖于外部的 `count` 变量
  15. }, 1000);
  16. return () => clearInterval(id);
  17. }, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量
  18. return <h1>{state.count}</h1>;
  19. }

发送一个请求

发请求是一个非常常见的场景,除非你的需求就是一个静态页面。我们发一个请求:

  1. // ❌ 反例
  2. function Component() {
  3. const [data, setData] = useState({});
  4. useEffect(async () => {
  5. const result = await axios(`https://hn.algolia.com/api/v1/getProfile`);
  6. setData(result.data);
  7. }, []);
  8. }

上面这个代码如果你写了会发现是报错的,因为 useEffect 中回调函数的返回值变成了一个Promise对象,这不符合react的要求(返回空或函数),所以我们需要做以下改写:

  1. // ❌ 反例
  2. function Component() {
  3. const [data, setData] = useState({});
  4. const init = async () => {
  5. const result = await axios(`https://hn.algolia.com/api/v1/getProfile`);
  6. setData(result.data);
  7. };
  8. useEffect(() => {
  9. init();
  10. }, []);
  11. }

我们定义了一个函数 init 用来发起请求。发送请求是没问题了,但当请求有依赖参数时,为了解决init被反复重复定义的问题,你又得给他补充一个 useCallback。所以通常,react 推荐我们将请求发送函数定成在react内部:

  1. // ❓ 正解
  2. useEffect(() => {
  3. const fetchData = async () => {
  4. const result = await axios(`https://hn.algolia.com/api/v1/search?query=${props.query}`);
  5. setData(result.data);
  6. };
  7. fetchData();
  8. }, [props.query]);

这样代码就很清晰了,这个 useEffect 负责发送这一个请求。当每次props.query发生改变,都会自动请求数据并更新值。但这个例子就真的没有问题了么?并不是的,这里还藏着一个竞态问题。试想 props.query 先变成 ‘react’,紧接着又变成 ‘vue’,而 ‘react’ 的查询结果在 ‘vue’ 的结果之后返回,状态依然会更新出错。因此我们需还要做个简单的改造,在每次渲染前取消上次请求:

  1. // ✅ 正解
  2. useEffect(() => {
  3. const isCancelled = false;
  4. const fetchData = async () => {
  5. const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);
  6. !isCancelled && setData(result.data);
  7. };
  8. fetchData();
  9. return () => { isCancelled = true };
  10. }, [query]);

不要过于依赖ESLint

React 官方提供了对于 hooks 语法的 ESLint 插件,正确这个插件可以完成以下校验:

  1. hooks 不能用于分支、循环以及非组件和自定义Hook的其他函数中;
  2. useXXX 中的依赖项不完整

这里第二项在大部分情况下是好东西,但它也会引入bug:

  1. useEffect(() => {
  2. // 预期,页面加载时请求接口,后续仅在需用户点击刷新再发送
  3. axios(`https://hn.algolia.com/api/v1/search?query=${prop.query}`).then(result => {
  4. setData(result.data);
  5. });
  6. }, []); // 如果你开立了自动fix, 插件会在这里自动添加 props.query 参数

用 Hooks 的方式思考

这一节对理解 hooks 的思想很重要,假定我们有一个组件 DatePicker,现在用两种方式使用这个组件:

传统思维

  1. const Page = () => {
  2. const [date, setDate] = useState<Moment>(moment('2021-01-01'));
  3. cosnt [isValid, setValid] = useState(false);
  4. const onDateChange = useCallback((date: Moment) => {
  5. setDate(date);
  6. checkValid(date) && setValid(true); // isValid 用来校验日期是否符合业务要求,控制提交按钮状态
  7. }, []);
  8. return (
  9. <>
  10. <DatePicker value={date} onChange={onDateChange} />
  11. <Button disabled={!isValid}>提交</Button>
  12. </>
  13. );
  14. }

Hooks 思维

  1. const Page = () => {
  2. const [date, setDate] = useState<Moment>(moment('2021-01-01'));
  3. cosnt [isValid, setValid] = useState(false);
  4. useEffect(() => {
  5. checkValid(date) && setValid(true);
  6. }, [date]);
  7. return (
  8. <>
  9. <DatePicker value={date} onChange={setDate} />
  10. <Button disabled={isValid}>提交</Button>
  11. </>
  12. );
  13. }

问题所在

上述计时器,在class中的实现(DEMO)

  1. class Counter extends React.Component{
  2. state = { count: 0 }
  3. componentDidMount() {
  4. this.id = setInterval(() => {
  5. this.setState(this.state.count + 1);
  6. }, 1000);
  7. }
  8. componentWillUnmount() {
  9. clearInterval(this.id);
  10. }
  11. render() {
  12. return <h1>{this.state.count}</h1>;
  13. }
  14. }

用 class 的习惯性思维改造成 hooks,就bug了(DEMO)

  1. // ❌ 反例
  2. function Counter() {
  3. const [count, setCount] = useState(0); // state 搬过来
  4. // 写一个 didMount/willUnmount 类型的 useEffect
  5. useEffect(() => {
  6. // componentDidMount 内容搬过来
  7. const id = setInterval(() => {
  8. setCount(count + 1);
  9. }, 1000);
  10. // componentWillUnmount 内容搬过来
  11. return () => clearInterval(id);
  12. }, []);
  13. return <h1>{count}</h1>; // render 搬过来
  14. }

Q&A

useEffect(fn, []) 和 comonentDidMount/comonentDidUpdate 一样么?

不完全一样useEffect捕获 props和state。所以即便在回调函数里,你拿到的还是初始的 props 和state。如果你想得到“最新”的值,你可以使用ref。不过,通常会有更简单的实现方式,所以你并不一定要用ref。(HOOKS | CLASS | see CLASS with HOOKS)

hooks的本质是什么?

闭包,例如setState,其最简实现可以是这样的

  1. // 简化理解,非React实际实现
  2. const useState = (initial) => {
  3. const initialState = typeof initial === function ? initial() : initial;
  4. let state = initialState;
  5. const setState = (newState) => {
  6. state = typeof newState === function ? newState(state) : newState;
  7. rework(); // 每次setState后重新执行组件函数
  8. };
  9. return [state, setState];
  10. }

为啥调用hooks是依赖顺序的?

上个问题的代码可以看出一段很明显的错误,第4行,这里这样赋值在rework时又会被重新设置成初始值,导致第7行的回调拿到的永远是初始值。所以React是这样定义Hooks的(ReactFiberHooks.js) :

  1. export type Hook = {
  2. memoizedState: any, // 指向当前渲染节点 Fiber, 上一次完整更新之后的最终状态值
  3. baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
  4. baseUpdate: Update<any, any> | null, // 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
  5. queue: UpdateQueue<any, any> | null, // 缓存的更新队列,存储多次更新行为
  6. next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
  7. };

哪些功能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

react-router-dom

  • useRouteMatch(不常用)
  • useParams(常用)
  • useHistory(常用)
  • useLocation(常用)