Комментарии 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;
}
Ну кстати прикольная подборка, еще useFetch добавить, и можно в библиотеку компоновать! Спасибо!
В приведенном коде 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;
}
Пять нужных кастом-хуков для React