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

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

Не делайте так. Это прям глобальный выстрел в ногу и идет против всей идеологии реакта. В итоге вы получите эффект с неочевидным и не прямым жизненным циклом.

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

// плохо, даже если у вас трижды useDeepEffect
useEffect(() => {
   fetchData(data.id)
}, [data]);

// хорошо
useEffect(() => {
   fetchData(data.id);
}, [data.id]); // заметьте прямую зависимость от id 

В случае с useDeepEffect у вас перезагрузка будет происходить даже если поменялись нерелевантные данные. Если она так и задумана -- объявляйте больше зависимостей напрямую. Ну или у вас уже где-то глубоко архитектурная проблема.

PS ваш useDeepEffect, в общем случае, будет приводить к двойной перерисовки компонента, что может плохо сказаться на производительности. Ну и большой шанс, что три эффекта с useRef и setState будет гораздо медленнее, чем логика, которая в эффекте.


PPS

React: разрабатываем кастомный useEffect

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

Не перегибайте палку.


useEffect(() => {
   fetchData(data.id)
}, [data]);

Это нормальный код (если человек знает что делает). Это же не useMemo или useCallback. Хук useEffect он куда более, как бы это сказать, ориентированный на бизнес-логику. Его deps это совершенно не обязательно те поля, которые задействованы внутри. Он ведь — side effect. Side effect-ы это муть мутная и зависят от конкретной бизнес-задачи. В большинстве случаев всё просто и deps по бизнес-логике совпадают с deps задействованными. Но не стоит обманываться. В deps могут быть вообще незадействованные поля, так же как могут не быть задействованные. Это "ручная" работа. Линтеры на useEffect имхо просто мусор.


В итоге вы получите эффект с неочевидным и не прямым жизненным циклом.

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


Т.е. в общем и целом я согласен что deps надо указывать как можно более прицельно. Но не стоит забывать что эффекты это эффекты, и это триггер эффекта. Штука к которой надо относиться очень серьёзно и исходить из реалий бизнес-логики, моделей данных, окружения у компонента и т.д… Просто подход — что заюзал, то и занёс в deps это самообман.

Вы, конечно же, правы. Но я считаю, что опустив случаи микрооптимизаций (там много чего идет в ход, что не react-way, например, когда нужно делать onscroll), с загрузкой / перезагрузкой данных такое возникает обычно когда данные обновились, а их айди - нет. Я бы сказал что, начиная с какой-то сложности, лучше всё таки переходить на что-то более подходящее, чем эффекты: redux, mobx, rxjs, react query, в конце концов. Эффекты с непрямыми зависимостями рано или поздно начнут вызываться не тогда, когда вы этого хотите, и победить это может быть проблематично.

Обратите внимание: все операции можно выполнять в одном useEffect, но мы будем придерживаться принципа единственной ответственности (single responsibility).

Плюсы от SRP тут совсем неочевидны, зато будет лишний ререндер.

Поправил:

function useDeepEffect(cb: VoidFunction, deps: unknown[]) {
    const prevDeps = usePrevious(deps);
    const firstRender = useRef(true);

    useEffect(() => {
        if (firstRender.current || !prevDeps) {
            firstRender.current = false;
        } else {
            if (areEqual(deps, prevDeps)) {
                return;
            }
        }

        cb();
    }, [deps, cb, prevDeps]);
}

теперь линтер не ругается.

Кстати, можно ли прикрутить react-hooks/exhaustive-deps к этому самописному хуку?

можно ли прикрутить react-hooks/exhaustive-deps к этому самописному хуку?

сам спросил, сам ответил: additionalHooks в конфиге

useDeepEffect пишется гораздо проще путём глубокой мемоизации его зависимостей.

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

function useDeepCompareDeps(deps: DependencyList | undefined): DependencyList | undefined {
  const ref = useRef(deps)
  if (!isEqual(ref.current, deps)) ref.current = deps
  return ref.current
}

const useDeepEffect: typeof useEffect = (fn, deps) => useEffect(fn, useDeepCompareDeps(deps))

Пишем кастомный useEffect:

function useCustomEffect(a, b) {
    return useEffect(a, b);
}

Profit.

Интересная тема. Столкнулся  с необходимостью подобного хука когда пробовал использовать mobx. Так там нужно было вызывать toJS для "глупых" компонентов. И в итоге пришел к выводу что легче всем этим не заниматься, а ориентироваться на равенство по ссылке из-за неочевидности использования дальнейших решений(кастомные хуки, мемоизации, юзколбеки). Показалось легче уж из того же редакса с помощью реселекта получать один и тот же объект, но оставаться в одной идеологии на всем проекте.
Ещё вопрос, написано что stringify медленно работает, а вот насколько? Если такие тяжёлые объекты на вход идут, то как же их рендерить?

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