Skip to content

🧊 feat(hooks): add useCustomCompareEffect - #508

Open
ashenoooone wants to merge 2 commits into
siberiacancode:mainfrom
ashenoooone:feature/use-custom-compare-effect
Open

🧊 feat(hooks): add useCustomCompareEffect#508
ashenoooone wants to merge 2 commits into
siberiacancode:mainfrom
ashenoooone:feature/use-custom-compare-effect

Conversation

@ashenoooone

@ashenoooone ashenoooone commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #507

useCompareEffect(effect, deps, compare?) runs the effect when the compare function says a dependency changed, instead of relying on React's reference check. compare defaults to deepEqual.

useCompareEffect(() => subscribe(user), [user]);
useCompareEffect(() => subscribe(user), [user], shallowEqual);
useCompareEffect(() => subscribe(user), [user], (user, prev) => user.id === prev.id);

The compare function returns true when the values are equal and the effect is skipped. It is applied to each dependency separately, not to the list as a whole — applied to the whole list, shallowEqual would compare each dependency by reference and behave exactly like a plain useEffect.

It is read fresh on every render, so it does not belong in deps. On mount the effect always runs and the compare function is not called at all, so it never has to handle an undefined previous value. Without dependencies the hook keeps the useEffect(fn) meaning and runs on every render.

Changes are tracked with a counter, and useEffect receives [signalRef.current], never the caller's array. So a dependency list that changes length never triggers React's changed size between renders warning, and React's own Object.is cannot overrule the compare function:

// re-runs on every render, exactly as asked, even though the dependency is the same object
useCompareEffect(fn, [stableUser], () => false);

deepEqual moved out of useShallowEffect.ts into packages/core/src/helpers/deepEqual, next to a new shallowEqual. The move fixes what the old implementation got wrong: NaN was not equal to itself, any two Dates compared equal, Set, Map and symbol keys were unhandled, a cyclic dependency overflowed the stack, and the key loop was quadratic on wide objects. Two rules are deliberate and documented in the JSDoc: Set members and Map keys compare by reference while Map values compare deeply; and an object with no own enumerable string keys that is not a plain object compares by reference, so Error, URL, ArrayBuffer and DOM nodes do not all come out equal.

Breaking changes

useShallowEffect is removed — with the comparison as an argument, a hook per comparison rule is redundant. The migration is useCompareEffect(fn, deps, shallowEqual), and it is not behavior-preserving: the old hook compared recursively despite its name, so callers who relied on that drop the third argument instead.

The exported deepEqual also keeps its name with different semantics. Both need a line in the release notes, otherwise they look like a regression appearing with no change on the caller's side.

52 tests cover this, including the case where shallow and deep disagree.

@ashenoooone ashenoooone changed the title feature/use-custom-compare-effect 🧊 feat(hooks): добавь useCustomComp… 🧊 feat(hooks): add useCustomCompareEffect Sep 5, 2026
…равнением на useCompareEffect

useCompareEffect(effect, deps, compare?) применяет compare к каждой
зависимости отдельно, по умолчанию deepEqual. На уровне всего списка
shallowEqual сравнивал бы зависимости по ссылке, то есть вёл бы себя
как обычный useEffect.

BREAKING CHANGE: useShallowEffect удалён. Миграция —
useCompareEffect(fn, deps, shallowEqual), но поведение меняется:
старый хук вопреки названию сравнивал рекурсивно. Для прежней
семантики третий аргумент не передавать.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[hook]: Create useCustomCompareEffect hook

1 participant