Fix deeperEqual throwing when a selection becomes null - #1223
Open
eastagiletracker wants to merge 1 commit into
Open
eastagiletracker wants to merge 1 commit into
eastagiletracker wants to merge 1 commit into
Conversation
deeperEqual is the isEqual predicate behind useStore and the exported useManuscriptsState hook. Its first branch returned false for any falsy value, so an unchanged undefined, null, false, 0 or '' selection was reported as changed, and neither branch guarded the other argument: an object or map selection turning into null made the Object and Map comparisons dereference null and throw a TypeError while rendering. Compare identity first, and bail out when only one side is nullish, before the structural comparisons.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR proposes a fix for
deeperEqualthrowing aTypeErrorwhen a selected object or map becomesnull. We include this PR work along with a full history of your repo at https://eastagiletracker.com/projects/297. You can sign in with your GitHub ID to claim ownership of the project.What is wrong
src/lib/deeper-equal.tsis theisEqualpredicate behind two consumers:useStore(src/store/useStore.tspasses it touseSyncExternalStoreWithSelector, and about 45 call sites acrosssrc/componentsandsrc/hooksgo through it), and the exporteduseManuscriptsStatehook (src/hooks/external/use-manuscripts-state.ts, re-exported fromsrc/index.tsx), which a parent app drives with a selector of its own.Neither of the two opening branches guards the second argument.
if (!next) return falseonly covers the first one, and thetypeof prev == 'undefined'check letsnullthrough, so as soon as a selection that is a plain object or aMapturns intonull, theObjectandMapbranches dereference it —prev[i]andprev.size— and throw while React is rendering. A selector as ordinary as(s) => s.files?.find((f) => f.id === id) ?? nullis enough to hit it.The same missing guard also makes the predicate answer "not equal" for a value that did not change:
deeperEqual(undefined, undefined),(null, null),(false, false),(0, 0)and('', '')all returnfalsetoday.Reproducing it on master
On
masterat29d3cd5, adding the three test files from this PR and runningTZ=UTC npx vitest rungives:Nine tests fail on that tree: the five unchanged-falsy cases, the predicate's own nullish case, and the three crashes above — one through
useStore, two through the publicuseManuscriptsStatehook.The change
Six lines in
deeperEqual: compare identity first and returntrue(which covers an unchangedundefined,null,false,0or''), then returnfalsewhen only one side is nullish, before the structural comparisons that would dereference it. The one-level-deep semantics of theObject,Map,Arrayand default branches are untouched.One behavioural note, since this predicate decides re-renders: a component whose selection stays falsy previously reported "changed" on every store update. React's own
Object.isbail-out already absorbed that for identical primitives, so no component re-renders that used to —src/store/__tests__/useStore.test.tsxpins that down, including a selection that legitimately moves betweenundefined,trueandfalse.How it was verified
TZ=UTC pnpm test,pnpm typecheckandpnpm lintwere all run onmasterbefore the change (13 tests, all green) and again after it: 34 tests green,tsc --noEmitclean,eslint --max-warnings 0clean, no new failures either way. The three new test files exercise the predicate directly and both of its consumers — mounting the realuseStoreand the realuseManuscriptsStatethroughreact-dom/client— and every one of the nine assertions above fails without the one-file change.How this was managed
This work was tracked as a story on a board imported from this repository's own issues and pull requests (1222 stories): Fix deeperEqual throwing when a store selection becomes null, on the board at eastagiletracker.com/projects/297.
If you'd rather not receive contributions like this, reply
no-more-prson this pull request and we won't open any further ones on your repositories.Lawrence W. Sinclair
CEO / East Agile
linkedin.com/in/lwsinclair/
eastagile.com