对 React Hooks 的三层认知,你在哪一层?

2,073 阅读11分钟

React Hooks 是 React 16.8 版本引入的一项特性,它允许我们在不编写类组件的情况下使用 state 和其他 React 的特性。React Hooks 直接掀翻了类组件的统治地位,助力函数式组件成为了主流。今天结合我自己的开发经验,聊聊对它的三层认知,看看你在哪一层?

第一层:会用

目前 React 官方共有 17 个 Hooks:React16有 10个, React18有 5个,React19有 2个。接下来让我们看一下它们都有什么作用~

1. useState、useEffect

这是入门级别的两个 Hooks。

const [state, setState] = useState(initialState)
useEffect(setup, dependencies?)
  • useState:用来声明状态变量。可以在函数组件中使用 state,而不需要编写类组件。
  • useEffect:用来处理副作用(side effects),如数据获取、订阅、或手动更改 React 组件中的 DOM。

有了这两个 Hooks,能完成 80% 的简单需求了。

2. useRef、useMemo、useCallback

当我们的项目进展到一定阶段,我们常常会遇到一些性能问题,比如希望某个变量在组件重新渲染时保持不变,或者优化组件性能以避免不必要的计算。这时候,我们可以使用一些高级的Hooks,如useRefuseMemouseCallback。下面将详细介绍它们的使用场景和实际应用。

const ref = useRef(initialValue)
const cachedValue = useMemo(calculateValue, dependencies)
const cachedFn = useCallback(fn, dependencies)

useRef

useRef是一个较为常用的Hook,它有两个主要的使用场景:

  1. 保持变量不变:如果你希望某个变量在每次重新渲染时都保持不变,可以使用useRef。例如,你需要保存一个计时器的ID,而不希望它在组件重新渲染时被重置:

    const timerId = useRef(null);
    
    useEffect(() => {
      timerId.current = setInterval(() => {
        console.log('计时中...');
      }, 1000);
    
      return () => clearInterval(timerId.current);
    }, []);
    
  2. 操作DOM:当你需要直接操作DOM元素时,useRef也非常有用。例如,你需要在组件加载后自动聚焦一个输入框:

    javascript
    复制代码
    const inputRef = useRef(null);
    
    useEffect(() => {
      inputRef.current.focus();
    }, []);
    
    return <input ref={inputRef} type="text" />;
    

useMemo

useMemo用于优化组件性能,确保只有在依赖项变化时才重新计算某个值。它可以帮助你避免每次渲染时都进行耗时的计算。

例如,你有一个计算密集型的函数,它依赖于某些输入数据,你可以使用useMemo来缓存其计算结果:

const expensiveCalculation = (num) => {
  console.log('计算中...');
  return num * 2;
};

const MyComponent = ({ number }) => {
  const calculatedValue = useMemo(() => expensiveCalculation(number), [number]);

  return <div>计算结果:{calculatedValue}</div>;
};

useCallback

useCallback的作用与useMemo类似,但它是用于缓存函数的。它确保只有在依赖项变化时才重新创建函数,从而避免子组件不必要的重新渲染。

例如,你有一个子组件需要依赖一个回调函数,你可以使用useCallback来优化性能:

const MyComponent = ({ onButtonClick }) => {
  return <button onClick={onButtonClick}>点击我</button>;
};

const ParentComponent = () => {
  const handleClick = useCallback(() => {
    console.log('按钮被点击了');
  }, []);
    /*
    如果在这不使用`useCallback`,
    `handleClick`函数将在每次`ParentComponent`重新渲染时被重新创建,
    导致传递给`MyComponent`的`onButtonClick`属性变化,
    从而使`MyComponent`重新渲染。
    */

  return <MyComponent onButtonClick={handleClick} />;
};

通过使用这些高级Hooks,我们可以更好地管理状态和性能,减少无效的重新渲染。

3. useContext、useReducer 、useImperativeHandel

当我们的项目规模变大,组件层次逐渐加深时,组件状态的维护变得更加复杂。这时候,我们可以利用一些高级的状态管理Hooks,如useContextuseReduceruseImperativeHandle,来更好地管理状态和组件间的交互。

  • useContext:当我们需要在不同层级的组件之间共享数据时,useContext可以用来减少props的传递。
  • useReducer:相当于迷你 redux,用于复杂的状态逻辑管理。
  • useImperativeHandle:用于自定义使用ref时暴露给父组件的实例值。通常与forwardRef一起使用。我的建议:不要用!它会增加组件之间的耦合度,违反了 React 数据流单向的原则,导致代码难以维护和理解。

在复杂的状态管理场景下,使用合适的状态管理工具和方法可以大大提升开发效率和代码可维护性。对于大型项目,通常推荐使用像 Redux、Zustand 等专门的状态管理库来处理复杂的状态逻辑。

4. useLayoutEffect、useDebugValue

这俩不常用

  • useLayoutEffect:类似 useEffect,适用于需要在 DOM 更新后但在浏览器绘制之前执行的场景,可以同步读取布局或强制同步重新渲染。
import React, { useLayoutEffect, useRef, useState } from 'react';

const LayoutEffectExample = () => {
  const divRef = useRef(null);
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    if (divRef.current) {
      setWidth(divRef.current.offsetWidth);
    }
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ width: '50%' }}>
        This div is 50% of the parent width.
      </div>
      <p>The width of the div is: {width}px</p>
    </div>
  );
};

export default LayoutEffectExample;

在这个示例中,我们使用 useLayoutEffect 来获取一个 div 的宽度,并在组件渲染之前同步更新状态。

  • useDebugValue:用于在 React DevTools 中显示自定义 hook 的调试信息,帮助开发者更方便地调试自定义 hooks。(我写 React Native,用不上)

5. 新 Hooks (5+2)

由于公司的项目一直React 17,下面的 7 个 Hooks 我还没用到过,似乎也没影响到我的工作,你懂的。我们简单看一下:

React 18 的 5 个新 Hooks:

  • useTransition:用于管理用户界面上的异步状态过渡。
  • useDeferredValue:用于延迟状态值,防止过多渲染。
  • useId:生成稳定的唯一 ID。(这个蛮实用的
  • useSyncExternalStore:用于读取外部可变存储的 Hook,保证同步性。
  • useInsertionEffect:与 useLayoutEffect 类似,但在所有 DOM 变化之前同步运行,适用于 CSS-in-JS 库。

React 19 引入的 2 个 Hooks:

  • useOptimistic:用于管理乐观更新。当执行某个操作时,可以先假设操作成功,并立即更新 UI,然后在操作完成后根据实际结果调整状态。比如点赞、评论、加入购物车等功能,我们都可以先假设成功,再根据接口返回来调整。
  • useActionState:用于管理与用户操作相关的状态。它能够记录和回放用户操作,帮助实现更复杂的交互和调试功能。

第二层:懂原理

动手实现官方 Hooks,对其有更深的理解。

1. 实现useState

目标

const [state, setState] = useState(initialState)

我们先来观察一下 useState 是什么?useState 是一个以 use开头的函数,输入一个初始值,输出一个 state 和改变这个 state 的函数。我们再来看平时是怎么使用 useState 的? 我们会在不同函数组件中调用多个 useState,每个 useState 都有自己的 state 和 setState 函数,这些 state 和 setState 函数互不干扰。我们每次调用 setState 函数,都希望页面重新渲染。

有了这些信息,我们就可以确定我们的目标:

  • 实现一个函数 useState,输入一个初始值,输出一个 state 和改变这个 state 的函数。
  • 多次调用 useState 时,每个 useState 都有自己的 state 和 setState 函数,互不干扰。这就需要维护一个全局的 state 数组,和一个全局的 index 变量,用来记录当前是第几个 useState。
  • 每次调用 setState 函数,都希望页面重新渲染。这就需要在 setState 函数中调用 React 的更新函数,来触发页面重新渲染。

动手实现

const state = [];
let stateIndex = 0;
function useState(initialState) {
  const currentIndex = stateIndex; 
  stateIndex++;
  state[currentIndex] = state[currentIndex] || initialState; 
  function setState(newState) {
    state[currentIndex] = newState; //这里用到了闭包
    render();
  }
  return [state[currentIndex], setState]; 
}

我第一次学的时候有两个小疑问,和大家分享一下:

  • 为什么 state[currentIndex] = state[currentIndex] || initialState;,而不是直接写成 state[currentIndex] = initialState?
  • 为什么返回值是第一个参数是state[currentIndex],这样返回的 state 永远是定义时的那个值呀。

其实本质问题是React 如何管理和更新状态,我梳理了下这个流程:

image.png render 函数的基本思路如下:

function render() {
  stateIndex = 0;
  // 重新渲染组件
  // ...
}

2. 实现useEffect

目标

   useEffect(()=>{
      effectFunction();
      return ()=>{cleanupFunction()}
   }, dependences)

还是先来回顾一下 useEffect 是什么?它是一个函数,接受两个入参,分别是一个函数,和一个数组,这个数组里内容变化时执行前面的函数。如果数组为空,那么仅在初始化的时候执行函数。函数可以有一个返回值,用来清除副作用。 useEffect 多用在初始化的场景以及监听一些变量变化时执行一些函数。

实现的思路如下:

  1. 定义一个useEffect函数,接受一个副作用函数和一个依赖数组
  2. 管理依赖变化。和上一次依赖项比较,决定是否执行副作用函数。
  3. 处理清理函数

动手实现

let effectIndex = 0; // 用于跟踪每个 effect 的索引
const effectDependencies = []; // 用于存储每个 effect 的依赖项
const effectCleanups = []; // 用于存储每个 effect 的清理函数

function useCustomEffect(callback, dependencies) {
    const currentIndex = effectIndex; // 当前 useEffect 的索引
    const previousDependencies = effectDependencies[currentIndex];

    const hasChanged = !previousDependencies || dependencies.some((dep, i) => dep !== previousDependencies[i]);

    if (hasChanged) {
        if (effectCleanups[currentIndex]) {
            effectCleanups[currentIndex](); // 执行上一次的清理函数
        }
        const cleanup = callback();
        effectCleanups[currentIndex] = typeof cleanup === 'function' ? cleanup : null;
        effectDependencies[currentIndex] = dependencies;
    }

    effectIndex++;
}

// render的时候重置effectIndex
function render() {
  effectIndex = 0;
  // 重新渲染组件
  // ...
}

// 在组件卸载时执行所有清理函数
function cleanupAllEffects() {
    effectCleanups.forEach(cleanup => {
        if (cleanup) cleanup();
    });
}

3. 实现 useRef

目标

useRef 在 React 中用于创建一个可变的对象,该对象在组件的整个生命周期内保持一致。它通常用于存储对 DOM 元素或其他任意值的引用,而不会触发组件重新渲染。

思路如下:

  • 创建一个 useRef 函数
    • 接受一个初始值作为参数。
    • 返回一个包含 current 属性的对象,该属性持有传入的初始值。
  • 保持引用的一致性
    • 确保在每次渲染时,返回的引用对象都是同一个。

动手实现

let refStore = []; // 存储所有 useRef 创建的引用
let refIndex = 0;  // 跟踪当前 useRef 的索引

function useRef(initialValue) {
    const currentIndex = refIndex; // 当前 useRef 的索引

    if (!refStore[currentIndex]) { //只有不存在的时候才需要初始化
        refStore[currentIndex] = { current: initialValue }; // 初始化引用对象
    }

    refIndex++; // 增加索引以支持多个 useRef

    return refStore[currentIndex]; // 返回引用对象
}

function render() {
  refIndex = 0;
  // 重新渲染组件
  // ...
}

4. 实现 useMemo

目标

const memoDate = useMemo(()=>{return result}), dependences)

useEffect、useMemo、useCallback 这三个其实很像,它们都依赖于依赖数组来决定何时执行。区别在于:

  • useEffect:依赖变化时执行副作用函数。
  • useMemo:依赖项变化时,重新一个缓存结果。
  • useCallback:依赖变化是,重新创建一个函数。

useMemo的实现思路和 useEffect 就很像,只不过把执行结果返回回去。

动手实现

let memoStore = []; // 存储所有 useMemo 的值
let memoIndex = 0;  // 跟踪当前 useMemo 的索引

function useMemo(factory, deps) {
    const currentIndex = memoIndex; // 当前 useMemo 的索引

    // 如果没有存储过这个索引的值或者依赖项发生变化
    if (
        memoStore[currentIndex] === undefined ||
        !areDepsEqual(memoStore[currentIndex].deps, deps)
    ) {
        // 计算新的值并存储
        memoStore[currentIndex] = {
            value: factory(),
            deps
        };
    }

    memoIndex++; // 增加索引以支持多个 useMemo

    return memoStore[currentIndex].value; // 返回存储的值
}

更多 Hooks 的实现可以参考: 手写 React Hooks,助你掌握实现原理(全程无废话)

第三层:会设计

Hooks 本身并没有很难,它就是一个以 use 开头的函数,什么功能都可以封装进去。我们来一起看几个小例子:

1. useFetch

这是一个用于数据获取的 Hook,代码实现:

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

假设我们要从一个公开的 API 获取用户数据,并在我们的组件中展示这些数据。我们就可以这么用:

import React from 'react';
import useFetch from './useFetch'; // 确保路径正确

function UserList() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {data.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

2. useLocalStorage

一个用于管理 localStorage 的 Hook。

import { useState } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };

  return [storedValue, setValue];
}

3. usePrevious

一个用于获取上一个状态值的 Hook。

import { useEffect, useRef } from 'react';

function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

4. useOnClickOutside

一个用于检测点击元素外部的 Hook。

import { useEffect } from 'react';

function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

上面的这几个小例子都可以跑,测试代码太长了就不放在这里,大家可以把它丢给 GPT,让 GPT 帮忙写测试用例。我写的这几个都是很简单的自定义 Hooks,看了大佬的文章发现设计好 Hooks 还是蛮有挑战的,感兴趣的朋友可以看看这个:搞懂这12个Hooks,保证让你玩转React

总结

React Hooks 的理解可以分为三个层次:

  1. 会用:掌握基本和高级 Hooks 的使用,处理简单和复杂的状态管理与性能优化。
  2. 懂原理:了解 Hooks 的内部工作原理,能够手动实现常见的 Hooks。
  3. 会设计:具备设计自定义 Hooks 的能力,提升代码复用性和可维护性。

希望本文对您有帮助,如有任何问题,欢迎评论~

参考: