Использовать обратный вызов с зависимостью против использования ссылки для вызова последней версии функции

#reactjs #use-ref #usecallback

#reactjs #использовать-ref #обратный вызов

Вопрос:

Выполняя обзор кода, я наткнулся на этот пользовательский хук:

 import { useRef, useEffect, useCallback } from 'react'

export default function useLastVersion (func) {
  const ref = useRef()
  useEffect(() => {
    ref.current = func
  }, [func])
  return useCallback((...args) => {
    return ref.current(...args)
  }, [])
}
 

Этот хук используется следующим образом:

 const f = useLastVersion(() => { // do stuff and depends on props })
 

По сути, по сравнению с const f = useCallBack(() => { // do stuff }, [dep1, dep2]) этим избегается объявление списка зависимостей и f никогда не изменяется, даже если изменяется одна из зависимостей.

Я не знаю, что думать об этом коде. Я не понимаю, в чем недостатки использования useLastVersion по сравнению с useCallback .

Комментарии:

1. Вы можете просто вернуть ref.current это настолько избыточно, это useCallback бесполезно, можете ли вы спросить, почему это там?

2. @DennisVash возвращает ref.current другое: здесь мы возвращаем функцию, которая никогда не меняется, тогда ref.current как изменится.

3. Мой вопрос в том, почему при наличии функции сам объект ref (return ref) имеет тот же срок службы, что и функция, зачем иметь другую оболочку?

4. Простое использование useCallback, похоже, делает то же самое, что вы предложили

5. @DennisVash нет, это не так. При непосредственном использовании функции при каждом рендеринге создается новая функция, и если она передается как реквизит, дочерний компонент также будет повторно отображаться. При использовании useCallback ссылка не изменится, если массив зависимостей не изменится, поэтому мы избегаем потенциальных повторных отправлений для дочерних компонентов. Используя этот useLastVersion хук, мы продвигаем его еще дальше, потому что мы мутируем, чтобы избежать любого повторного использования.

Ответ №1:

На этот вопрос на самом деле уже более или менее дан ответ в документации: https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback

Интересная часть:

Также обратите внимание, что этот шаблон может вызвать проблемы в параллельном режиме. В будущем мы планируем предоставить более эргономичные альтернативы, но самое безопасное решение прямо сейчас — всегда аннулировать обратный вызов, если какое-то значение зависит от изменений.

Также интересно прочитать: https://github.com/facebook/react/issues/14099 и https://github.com/reactjs/rfcs/issues/83

Текущая рекомендация — использовать поставщика, чтобы избежать передачи обратных вызовов в props, если мы обеспокоены тем, что это может привести к слишком большому количеству повторных вызовов.

Ответ №2:

Моя точка зрения, как указано в комментариях, заключается в том, что этот хук избыточен с точки зрения «сколько рендеров вы получаете», когда происходят слишком частые изменения зависимостей (в useEffect useCallback массивах / dep), использование обычной функции является лучшим вариантом (без накладных расходов).

Этот хук скрывает рендеринг компонента, использующего его, но рендеринг происходит из useEffect его родительского элемента.

Если мы суммируем количество рендеринга, мы получаем:

  • Ссылка обратный вызов (перехват): рендеринг в Component (из-за value ) Рендеринг в hook ( useEffect ), всего 2.
  • Только обратный вызов: рендеринг в Component (из-за value ) рендеринг в Counter (изменение в дуэте ссылок на функции на value изменение), всего 2.
  • обычная функция: рендеринг в Component рендеринг в Counter : новая функция при каждом рендеринге, всего 2.

Но вы получаете дополнительные накладные расходы для неглубокого сравнения в useEffect or useCallback .

Практический пример:

 function App() {
  const [value, setValue] = useState("");
  return (
    <div>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        type="text"
      />
      <Component value={value} />
    </div>
  );
}

function useLastVersion(func) {
  const ref = useRef();
  useEffect(() => {
    ref.current = func;
    console.log("useEffect called in ref callback");
  }, [func]);
  return useCallback((...args) => {
    return ref.current(...args);
  }, []);
}

function Component({ value }) {
  const f1 = useLastVersion(() => {
    alert(value.length);
  });

  const f2 = useCallback(() => {
    alert(value.length);
  }, [value]);

  const f3 = () => {
    alert(value.length);
  };

  return (
    <div>
      Ref and useCallback:{" "}
      <MemoCounter callBack={f1} msg="ref and useCallback" />
      Callback only: <MemoCounter callBack={f2} msg="callback only" />
      Normal: <MemoCounter callBack={f3} msg="normal" />
    </div>
  );
}

function Counter({ callBack, msg }) {
  console.log(msg);
  return <button onClick={callBack}>Click Me</button>;
}

const MemoCounter = React.memo(Counter);
 

Редактировать Использовать последнюю версию


В качестве примечания, если целью является только нахождение длины input с минимальным отображением, inputRef.current.value решением будет чтение.

Комментарии:

1. Конечно, для этого примера мы, вероятно, могли бы сделать лучше, это было предназначено только для примера. Как вы можете видеть, используя useLastVersion вместо повторного Counter рендеринга (представьте, что это тяжелый компонент для рендеринга), мы выполняем только очень легкий эффект.