Skip to content

最新值与函数持久化之 useLatest 与 useMemoizedFn

“新”与“旧”

JS技术栈中,值是不是最新的其实困扰着很多的初学者。

很多时候我们想始终拿到最新的值,但是由于闭包的作用域问题可能拿到的是“旧”的值

比如还是拿我们源码解读 02 的例子来说,

tsx
import { useEffect, useState } from "react";

const useUnmount = (fn) => {
  useEffect(
    () => () => {
      fn?.();
    },
    []
  );
};
const Child = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  useUnmount(() => {
    console.log("count:", count);
  });
  return <div>{count}</div>;
};

export default function Test() {
  const [flag, setFlag] = useState(false);

  return (
    <div>
      <button
        onClick={() => {
          setFlag(!flag);
        }}
      >
        点我
      </button>
      {!!flag ? <Child /> : null}
    </div>
  );
}

这里面要改是存在两处的问题的。

  1. useUnmount的实现中传入的函数不能取到最新的count
  2. interval中每次累加的时候本身count值始终是从 0 开始的,所以页面也始终是 1

如何修改,大家可以链接到原文去看下

因此我们这篇就来介绍下ahooks源码实现和我们使用ahooks时,关于useLatestuseMemoizedFn的一些知识点

“新” - useLatest

源码实现

ts
import { useRef } from "react";

function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;

  return ref;
}

export default useLatest;

解读

useLatest的实现非常简单。它使用了useRef来保存传入的value。每次组件渲染时,ref.current都会被更新为最新的value。这样,无论什么时候访问ref.current,都能获取到最新的value

应用场景:

  1. 避免闭包问题:在某些情况下,我们可能会在回调函数中使用到组件的某些状态值,如果不使用useLatest,这些状态值可能会因为闭包的特性而无法获取到最新的值。例如,在事件处理函数中使用setIntervalsetTimeout时,使用useLatest可以确保访问到最新的状态。
tsx
import React, { useState, useEffect } from "react";
import { useLatest } from "ahooks";

export default () => {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  const latestCountRef = useLatest(count);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(latestCountRef.current + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount2(count2 + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <>
      <p>count(useLatest): {count}</p>
      <p>count(defult): {count2}</p>
    </>
  );
};

有同学肯定要问了,为什么使用useRef就可以了

On the next renders, useRef will return the same object.

来自React官网的描述,翻译过来的意思就是在后续的渲染时,始终给你返回相同的对象,对象的值引用不变,我们在需要的时候去修改或者读取它的current,就始终得到了最新值。

PS:React中,useRefcurrent的修改不会导致React组件的重新渲染

“旧” - useMemoizedFn

提醒大家注意一下 这里的“旧”其实是一个缓存持久化的感念

源码实现

ts
import { useMemo, useRef } from "react";
import { isFunction } from "../utils";
import isDev from "../utils/isDev";

type noop = (this: any, ...args: any[]) => any;

type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>;

function useMemoizedFn<T extends noop>(fn: T) {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(
        `useMemoizedFn expected parameter is a function, got ${typeof fn}`
      );
    }
  }

  const fnRef = useRef<T>(fn);

  // why not write `fnRef.current = fn`?
  // https://github.com/alibaba/hooks/issues/728
  fnRef.current = useMemo<T>(() => fn, [fn]);

  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current as T;
}

export default useMemoizedFn;

解读

useMemoizedFn同样是利用useRef来保存传入的函数fn。每次组件渲染时,fnRef.current都会被更新为最新的fn。通过useCallback返回一个记忆化的函数memoizedFn,这个函数在执行时总是调用最新的fnRef.current

应用场景:

  1. 保持函数引用的一致性:在传递函数给子组件时,保持函数引用的一致性可以避免子组件的额外渲染。
tsx
import React from 'react';
import useMemoizedFn from './useMemoizedFn';

function ParentComponent() {
  const handleAction = useMemoizedFn(() => {
    console.log('Action handled');
  });

  return <ChildComponent onAction={handleAction} />;
}

function ChildComponent({ onAction }) {
  return <button onClick={onAction}>Click me</button>;
}

export default ParentComponent;
}
  1. 依赖稳定的回调函数:在某些情况下,我们需要将一个回调函数传递给第三方库,这些库可能依赖回调函数的引用一致性。使用useMemoizedFn可以确保回调函数的引用在整个组件生命周期内保持稳定。

这里我们看下官网demo2

tsx
import { useMemoizedFn } from "ahooks";
import { message } from "antd";
import React, { useCallback, useRef, useState } from "react";

export default () => {
  const [count, setCount] = useState(0);

  const callbackFn = useCallback(() => {
    message.info(`Current count is ${count}`);
  }, [count]);

  const memoizedFn = useMemoizedFn(() => {
    message.info(`Current count is ${count}`);
  });

  return (
    <>
      <p>count: {count}</p>
      <button
        type="button"
        onClick={() => {
          setCount((c) => c + 1);
        }}
      >
        Add Count
      </button>

      <p>
        You can click the button to see the number of sub-component renderings
      </p>

      <div style={{ marginTop: 32 }}>
        <h3>Component with useCallback function:</h3>
        {/* use callback function, ExpensiveTree component will re-render on state change */}
        <ExpensiveTree showCount={callbackFn} />
      </div>

      <div style={{ marginTop: 32 }}>
        <h3>Component with useMemoizedFn function:</h3>
        {/* use memoized function, ExpensiveTree component will only render once */}
        <ExpensiveTree showCount={memoizedFn} />
      </div>
    </>
  );
};

// some expensive component with React.memo
const ExpensiveTree = React.memo<{ [key: string]: any }>(({ showCount }) => {
  const renderCountRef = useRef(0);
  renderCountRef.current += 1;

  return (
    <div>
      <p>Render Count: {renderCountRef.current}</p>
      <button type="button" onClick={showCount}>
        showParentCount
      </button>
    </div>
  );
});

当我们点击add count的时候,示例中 memoizedFn 是不会变化的,callbackFn 在 count 变化时变化。

总结:在 React 开发中掌握不变性的艺术与引用的智慧

由于这两个 Hook 在 ahooks 内部实现中被反复复用,对业务开发来说也是非常高频的工具 hook,因此我们这期重点拿出来讲了下。

React 开发中,我们常常面对状态管理、渲染优化和闭包陷阱等复杂问题。而利用useRefuseLatestuseMemoizedFn等 hooks,我们可以更好地管理这些问题,从而编写出高效、稳定和易维护的代码。

不变性与引用的智慧

  1. 不变性
    • 不可变数据结构:在 React 中,状态更新的最佳实践是使用不可变数据结构。这不仅符合 React 通过浅比较进行高效重渲染的机制,还能避免因为直接修改对象或数组而引发的潜在问题。
    • 状态更新:每次状态更新时,创建新的对象或数组,而不是直接修改原有的。这确保了 React 能够检测到状态的变化,从而正确触发重新渲染。
  2. 引用的智慧
    • useRefuseRef提供了一种方法,可以在组件的整个生命周期内持久化数据,而不触发组件的重新渲染。它在处理非状态数据的持久化存储方面非常有效,如保持某个值在多次渲染之间的一致性。
    • useLatest:在处理闭包问题时,useLatest确保了在回调函数中总是可以访问到最新的状态或属性值。这在处理定时器、事件监听器等异步操作时尤其重要。
    • useMemoizedFn:通过useMemoizedFn,可以确保函数引用在整个组件生命周期内保持一致,避免因函数引用变化而导致的子组件不必要的重新渲染。

通过理解和利用这些特性,我们可以编写出更加高效和健壮的 React 组件,提升应用的性能和用户体验。