Как стать автором
Обновить

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

В useEventCallback дичь какая-то, зачем там layouteffect? Если хочешь юзколбек которому не нужен массив зависимостей и ссылка всегда стабильная, можно через рефы сделать

Да, вместо использования useLayoutEffect можно записывать новую функцию в ref прям во время вызова хука. Но это сомнительный паттерн использования Реакта.

Вообще, на месте создателей Реакта, я бы сделал так, чтобы useCallback всегда возвращал одну и ту же ссылку на функцию, потому что случаи, когда нужно менять ссылку, намного более редкие.

Это нормальный паттерн использования реакт, когда реф не используется для дом элемента (если у тебя в return компонента какому-то диву реф присваивается, этот код выполнится тупо позже чем любые синхронные действия с рефом до return и всё перетрёт)

В большинстве случаев ок. Но не во всех:

But for a long time the React team has been talking about a "Concurrent Mode" where an update might start (render gets called), but then get interrupted by a higher priority update.

In this sort of situation it's possible for the ref to end up out of sync with the actual state of the rendered component, if it's updated in a render that gets cancelled.

Если useDerivedState нужен для первоначального состояния, то может использовать нормальные инструменты форм? Например Final Form, потому как в вашем примере работа идёт именно с полями ввода данных

Согласен, для этого примера лучше подойдут инструменты форм. Хотел показать хуки без дополнительных зависимостей

Что будет с useHover, если элемент который к нему подключен рендерится по условию и вдруг исчез, а потом появился?

В 4 хуке вы упомянули про производительность, но в хук воткнули useLayoutEffect. Зачем он там? То же самое и в 5

useDerivedState в такой реализации может добавить головной боли. Сначала компонент перерисуется с обновлёнными пропсами, но старым стейтом. Затем, после setState внутри useEffect, перерисуется с новыми пропсами и новым стейтом.

согласен, лучше сделать как-то так:

export function useDerivedState<T>(
  propValue: T,
): [T, Dispatch<SetStateAction<T>>] {
  const [state, setState] = useState(propValue);
  const ref = useRef(propValue);

  useEffect(() => {
    ref.currrent = propValue;
    setState(propValue);
  }, [propValue]);

  return [propValue === ref.current ? state : propValue, setState];
}

Правда, от лишнего рендера тут не уйти, но хотя бы с актуальным состоянием.

Можно заменить useEffect на useLayoutEffect и мутировать стейт там. Минус производительность (одно дополнительное вычисление VDOM из-за мутации стейта), но зато попадаем в одну отрисовку DOM'а с актуальным состоянием

На рефах у меня получилось что-то такое

export function useDerivedState(propValue) {
  const [state, setState] = useState(propValue);
  const stateChanged = useRef(false);
  const lastValue = useRef(propValue);

  if (lastValue.current !== propValue) {
    stateChanged.current = false;
    lastValue.current = propValue;
  }

  return [
    stateChanged.current ? state : propValue,
    useCallback((stateValue) => {
      if (typeof stateValue === "function") {
        setState((prevValue) => {
          if (stateChanged.current) {
            return stateValue(prevValue);
          } else {
            stateChanged.current = true;
            return stateValue(lastValue.current);
          }
        });
      } else {
        stateChanged.current = true;
        setState(stateValue);
      }
    }, []),
  ];
}

Вариант на useSyncExternalStore

function getStore(propValue) {
  let listener = null;
  let val = propValue;
  let propVal = propValue;
  let stateVal = propValue;

  return {
    clean() {
      listener = null;
    },
    subscribe(l) {
      listener = l;
    },
    getValue() {
      return val;
    },
    setPropValue(nextVal) {
      if (propVal !== nextVal) {
        val = propVal = nextVal;
        if (listener) listener();
      }
    },
    setStateValue(nextVal) {
      const nextStateValue =
        typeof nextVal === "function" ? nextVal(val) : nextVal;
      if (nextStateValue !== stateVal) {
        val = stateVal = nextStateValue;
        if (listener) listener();
      }
    },
  };
}

export function useDerivedState(propValue) {
  const store = useMemo(() => getStore(propValue), []);

  store.setPropValue(propValue);

  return [
    useSyncExternalStore(store.subscribe, store.getValue),
    store.setStateValue,
  ];
}

В реализации useHover есть довольно типичная ошибка (упомянутая в комментарии выше), о ней писал здесь. Альтернативный вариант - использовать функциональный реф вместо useRef.

Несколько мелких поинтов о useEventCallback:

1) Если вызвать созданную им функцию внутри layoutEffect дочернего компонента, то реф ещё не будет обновлен, потому что сначала вызываются эффекты в чилдах. Для обновления рефа больше подходит useInsertionEffect.
2) throw там может вызваться только на самом первом рендере, а на последующих в рефе будет сидеть устаревшая функция и проверка не сработает. По нормальному это не проверить, потому предлагаю просто не заморачиваться.
3) Типизация не поддерживает перегрузки функций
4) Функция, передаваемая в хук, может быть необязательной, например это опциональный проп. Это хорошо бы поддержать, чтобы потом не городить костыли снаружи.

Итого, со всеми поправками
function useEventCallback<T extends undefined | null | ((...args: never[]) => unknown)>(
    func: T,
): T {
    const refCallee = useRef(func);

    useInsertionEffect(() => {
        refCallee.current = func;
    });

    const callback = useCallback((...args: never[]) => refCallee.current?.(...args), []) as T;

    return func && callback;
}

В приведенном коде useDerivedState использование useEffect нежелательно, так как может привести к лишним перерисовкам и другим неприятным багам. Заменить useEffect можно так:

function useDerivedState<T>(propValue: T) {
  const [state, setState] = useState(propValue);
  const [prevState, setPrevState] = useState(propValue);

  if (prevState !== propValue) {
    setState(propValue);
    setPrevState(propValue);
  }

  return [state, setState] as const;
}

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории