你不知道的React系列(十四)Effect Hook(掌握)

289 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

Effect Hook

概念

Effect Hook 使其他另外的系统(定时器、订阅事件、第三方)和组件进行同步,需要在渲染后执行某些操作,改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作(componentDidMountcomponentDidUpdatecomponentWillUnmount

不通过事件改变 state 比如网络连接,分析日志

useEffect(setup, dependencies?)

Effects 和 events

  • Rendering code render 函数里面代码
  • Event handlers 事件处理代码

存在既不是在渲染阶段运行也不能通过事件运行

浏览器 API, 第三方提供的组件, 网络请求内容

步骤

  • 声明 Effect

  • 指定 dependencies

  • 添加清除函数

effect 的执行时机

  • 传给 useEffect 的函数 render 之后执行

  • useEffect 第一次 render 渲染之后和每次更新之后都会执行,为了解决没有使用componentDidUpdate引发的问题

  • 特殊情况

    • 用户操作事件和flushSync 包装的更新结果,传递给 useEffect 的函数将在屏幕布局和绘制之前同步执行

      这只影响传递给 useEffect 的函数被调用时 — 在这些 effect 中执行的更新仍会被推迟

effect 的条件执行

effect 所依赖的值数组

  • 空数组,只运行一次的 effect(仅在组件挂载和卸载时执行)

  • 有值数组,只有声明的变量变化时才会执行

    • setEffect 函数使用了并且会被改变(包括 props、state,以及任何由它们衍生而来的东西),才需要放到 deps 数组中,否则你的代码会引用到先前渲染中的旧变量
  • 使用 Object.is 比较

  • 默认忽略 ref 和 set 函数

  • 每次 Render 都有各自的 Effect

如果修改 dependencies 先修改代码逻辑

  • 修改 Effect 函数或者 reactive values 声明方式。

  • 查看 linter 提示进行修改

  • 如果想要修改 dependencies 返回到开始

无需清除的 effect

  • useEffect 放在组件内部可以在 effect 中直接访问 state 变量(或其他 props)

    Hook 使用了 JavaScript 的闭包机制

  • 每次重新渲染,都会生成新的 effect

  • useEffect 不会阻塞浏览器更新屏幕

清除 effect

  • effect 返回一个清除函数

    • 默认返回一个空函数

    • React 会在组件卸载的时候执行清除操作

    • 调用一个新的 effect 之前执行清除函数清理前一个 effect

      useEffect(() => {
        const subscription = props.source.subscribe();
        return () => {
          // 清除订阅
          subscription.unsubscribe();
        };
      });
      
  • 开发环境 useEffect 会额外执行一次

    • 第三方组件 API

    • 订阅事件(需要清除函数)

    • 动画(需要恢复到原始的状态)

      useEffect(() => {
        const node = ref.current;
        node.style.opacity = 1; // Trigger the animation
        return () => {
          node.style.opacity = 0; // Reset to the initial value
        };
      }, []);
      
    • 网络请求

      useEffect(() => {
        let ignore = false;
      
        async function startFetching() {
          const json = await fetchTodos(userId);
          if (!ignore) {
            setTodos(json);
          }
        }
      
        startFetching();
      
        return () => {
          ignore = true;
        };
      }, [userId]);
      
    • 日志记录

    • 程序初始化

    • 事件处理函数不要放在 effect 里面

使用 Effect 的提示

  • 使用多个 Effect 按照代码的用途实现关注点分离

    function FriendStatusWithCounter(props) {
    const [count, setCount] = useState(0);
    useEffect(() => {
      document.title = `You clicked ${count} times`;
    });
    const [isOnline, setIsOnline] = useState(null);
    useEffect(() => {
      function handleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
      ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
      };
    });
    }
    
  • 按照 effect 声明的顺序依次调用组件中的每一个 effect

    function App() {
      useEffect(() => {console.log(1)});
      useEffect(() => {console.log(2)});
      useEffect(() => {console.log(3)});
      return <div />;
    }
    
  • 依赖列表

    • 在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外

    • 万不得已的情况下,如果你想要类似 class 中的 this 的功能,你可以 使用一个 ref 来保存一个可变的变量

        function Example(props) {
          // 把最新的 props 保存在一个 ref 中
          const latestProps = useRef(props);
          useEffect(() => {
            latestProps.current = props;
          });
          useEffect(() => {
            function tick() {
              // 在任何时候读取最新的 props
              console.log(latestProps.current);
            }
            const id = setInterval(tick, 1000);
            return () => clearInterval(id);
          }, []); // 这个 effect 从不会重新执行
        }
      
  • effect 和函数

    • 当函数(以及它所调用的函数) 使用了 props、state,以及任何由它们衍生而来的东西,就把那个函数移动到你的 effect 内部

      function ProductPage({ productId }) {
        const [product, setProduct] = useState(null);
      
        useEffect(() => {
          // 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
          async function fetchProduct() {
            const response = await fetch("http://myapi/product/" + productId);
            const json = await response.json();
            setProduct(json);
          }
          fetchProduct();
        }, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId  // ...
      }
      
  • 你可以尝试把那个函数移动到你的组件之外,不用出现在依赖列表

  • 如果你所调用的方法是一个纯计算,并且可以在渲染时调用,在 effect 之外调用它, 并让 effect 依赖于它的返回值

  • 万不得已的情况下,useCallback Hook 定义一个函数,添加依赖列表传递给组件,然后 effect 里面调用它并且把这个函数作为依赖数组:

        function ProductPage({ productId }) {
          // ✅ 用 useCallback 包裹以避免随渲染发生改变
          const fetchProduct = useCallback(() => {
            // ... Does something with productId ...
          }, [productId]); // ✅ useCallback 的所有依赖都被指定了
          return <ProductDetails fetchProduct={fetchProduct} />;
        }
    
        function ProductDetails({ fetchProduct }) {
          useEffect(() => {
            fetchProduct();
          }, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
          // ...
        }
    }
    
    • effect 里面的函数包含定时器,使用函数式更新 state

      function Counter() {
        const [count, setCount] = useState(0);
      
        useEffect(() => {
          const id = setInterval(() => {
            // ✅ This doesn't depend on `count` variable outside
            setCount((c) => c + 1);
          }, 1000);
          return () => clearInterval(id);
        }, []);
        // ✅ Our effect doesn't use any variables in the component scope
        return <h1>{count}</h1>;
      }
      
  • hook 使用 this

    function Example(props) {
      // Keep latest props in a ref.
      const latestProps = useRef(props);
      useEffect(() => {
        latestProps.current = props;
      });
      useEffect(() => {
        function tick() {
          // Read latest props at any time
          console.log(latestProps.current);
        }
        const id = setInterval(tick, 1000);
        return () => clearInterval(id);
      }, []); // This effect never re-runs
    }
    

How to fetch data with React Hooks

effect 里面进行网络请求

  • Effects 不能在服务端渲染中使用

  • Effects 发送请求会引发网络瀑布

  • Effects 不能缓存数据

  • Effects 会有模板代码

无需 Effects 场景

  • 处理数据逻辑不应放在 effects

  • 可以通过事件实现的逻辑不应放在 effects

  • 基于 state 或者 props 更新 state

    渲染阶段函数内部直接获取

  • 缓存影响性能的逻辑

    使用 useMemo 缓存(纯计算)

  • props 改变时重置所有的 state

    使用 key

  • props 改变时修改部分 state

    function List({ items }) {
      const [isReverse, setIsReverse] = useState(false);
      const [selection, setSelection] = useState(null);
    
      // 🔴 Avoid: Adjusting state on prop change in an Effect
      useEffect(() => {
        setSelection(null);
      }, [items]);
    }
    
    function List({ items }) {
      const [isReverse, setIsReverse] = useState(false);
      const [selection, setSelection] = useState(null);
    
      // Better: Adjust the state while rendering
      const [prevItems, setPrevItems] = useState(items);
      if (items !== prevItems) {
        setPrevItems(items);
        setSelection(null);
      }
    }
    
    function List({ items }) {
      const [isReverse, setIsReverse] = useState(false);
      const [selectedId, setSelectedId] = useState(null);
      // ✅ Best: Calculate everything during rendering
      const selection = items.find(item => item.id === selectedId) ?? null;
      // ...
    }
    
  • 事件处理函数共享逻辑

    使用函数

  • 发送 POST 请求

    使用事件处理函数

  • 链式计算

    • 使用函数和事件处理函数

    • 多个下拉框,依赖前一个下拉框选择的值使用

  • 初始化

    • 组件开始位置使用标志位

    • 项目入口或者 App.js 直接编写逻辑

  • state 改变后通知父组件

    使用函数

  • 子组件传递数据给父组件

    请求数据逻辑提取到父组件

  • 订阅其他 store

    使用 useSyncExternalStore

    function subscribe(callback) {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    }
    
    function useOnlineStatus() {
      // ✅ Good: Subscribing to an external store with a built-in Hook
      return useSyncExternalStore(
        subscribe, // React won't resubscribe for as long as you pass the same function
        () => navigator.onLine, // How to get the value on the client
        () => true // How to get the value on the server
      );
    }
    
    function ChatIndicator() {
      const isOnline = useOnlineStatus();
      // ...
    }
    
  • 请求数据 race condition

    • 使用标志位、
    function SearchResults({ query }) {
      const [results, setResults] = useState([]);
      const [page, setPage] = useState(1);
      useEffect(() => {
        let ignore = false;
        fetchResults(query, page).then(json => {
          if (!ignore) {
            setResults(json);
          }
        });
        return () => {
          ignore = true;
        };
      }, [query, page]);
    
      function handleNextPageClick() {
        setPage(page + 1);
      }
      // ...
    }
    

Effect 生命周期

mounts => render => setup => props 或者 state更新 => render(新) => cleanup(旧) => setup => unmount => cleanup(旧)

  • 为什么 synchronization 要发生多次

    为了解决 effect 逻辑与 render UI 界面保持一致

  • React 怎样验证 effect 可以 re-synchronize

    通过调用 effect 和 cleanup

  • React 怎样知道 effect 需要 re-synchronize

    通过 dependencies

effect 功能保持独立

每个 effect 应该只是一个独立的 synchronization 过程

dependencies 改变 effect 如何跟踪

  • dependencies 为空数组

    mounts => render => effect => unmount => cleanup

  • 所有依赖 props 和 state 的变量都应包含在 dependencies

  • React 会校验你是否正确传递 dependencies

  • 如何不让 re-synchronize

    逻辑移到组件外部或者 effect 函数内部

  • dependencies 可能会出现相关问题,无限循环和经常 re-synchronizing

    解决方案

    • 检查 effect 是否是一个独立的功能

    • 把不需要 synchronizing 的逻辑部分分割出去读取最新的props 和 state

    • 不要把对象和函数作为 dependencies

事件与 Effect

  • 事件函数是特定的交互逻辑

  • Effects 是需要 synchronization 的时候使用

  • Reactive values (可变值)

    props, state, 和函数内部所有变量,重新渲染的时候有可能会有变化

  • reactive logic·

    因为 Reactive values 改变而需要重新运行的逻辑

    • Effect 函数内部 reactive logic

    • 事件处理函数不是 reactive logic

  • 从 Effect 抽离不是 reactive logic 的逻辑

    • 声明 Effect Event

      const onConnected = useEffectEvent(() => {
          showNotification('Connected!', theme);
      });
      
    • 使用 Effect Event 读取最新的 props 和 state

      函数内部读取的是最新的 props 和 state

    • Effect Event 局限性

      • 只能 Effect 内部调用

      • 不能传递给其他组件和Hooks

如何抽离 Effect 内部无关代码

  • 你可能想要在不同状态重新执行Effect的不同部分
  • 你可能想要只是读取最新的 dependency 的值而不是因为 dependency 变化要执行一些逻辑
  • 因为 dependency 是一个对象或者函数导致经常改变

解决方案

  • 逻辑是否可以移到事件里面

    比如提交事件成功以后的提示逻辑

  • Effect 函数内部是否执行了一些不想关的逻辑

下拉菜单级联需要分开不同的 Effect

  • 是否通过 state 计算另一个 state

    可以使用 updater function

  • 是否只是想读取某一个值或者使用父组件传下的函数而不得不把它放在 dependency

    使用 useEffectEvent 分割 reactive and non-reactive 逻辑

  • 是否 reactive value 无意中改变了

    • 对象或者函数移到组件外部

    • 对象或者函数移动 Effect 内部

    • 组件内部提取对象或者执行函数然后再传递给 dependency

客户端和服务端使用不同逻辑

function MyComponent() {
  const [didMount, setDidMount] = useState(false);

  useEffect(() => {
    setDidMount(true);
  }, []);

  if (didMount) {
    // ... return client-only JSX ...
  }  else {
    // ... return initial JSX ...
  }
}

注意点

  • Effect 不会在服务端执行

问题

  • 组件 mount 时候 Effect 运行了两次

    cleanup function 有问题

  • Effect 每次渲染都会运行

    没有指定 dependency 或者 dependency 每次都是不同

  • Effect 循环

    Effect 更新了 state

    state 引发重新渲染改变了 dependency

    • Effect 是否用于同步第三方系统

    • 为什么需要修改 state

    • 是否用于管理数据流向

    • 为什么和在什么情况下 Effect 更新 state

    • 是否一些逻辑改变影响了组件的呈现

  • 组件没有 unmount,cleanup 却执行

    cleanup 没有与 setup 匹配

  • Effect 有一些视图方面的逻辑,页面闪烁

    使用 useLayoutEffect