[Onyx Audit] Reset internal useOnyx state on key change#756
Conversation
…6-fix-useonyx-state-not-resetting
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8450987afc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
lib/useOnyx.ts
Outdated
| if (key !== previousKey) { | ||
| previousValueRef.current = null; | ||
| newValueRef.current = null; | ||
| isFirstConnectionRef.current = true; | ||
| shouldGetCachedValueRef.current = true; |
There was a problem hiding this comment.
Keep the new key in first-connection mode until its callback arrives
Resetting previousValueRef and isFirstConnectionRef here interacts badly with the existing subscribe() cleanup. On a key change, the old subscription still cleans up and sets isFirstConnectionRef.current = false before the new Onyx.connect() callback fires. If anything rerenders the component in that window (for example a parent/context update while the new key is loading from storage), getSnapshot() takes its previousValueRef.current === null && !isFirstConnectionRef.current path and marks the new key as {status: 'loaded'} even though the initial callback for that key has not arrived yet.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This was valid for an earlier revision that used a boolean isFirstConnectionRef, but the current code replaces it with connectedKeyRef which stores the actual key string. The "is first connection" check is now connectedKeyRef.current !== key, which is structurally immune to this race:
- Cleanup sets
connectedKeyRef.current = null subscribe()doesn't touch it- Only the Onyx callback sets it to the new key
So between cleanup and the callback firing, connectedKeyRef.current is null, and null !== newKey is always true — meaning isFirstConnection stays true throughout that window. If getSnapshot() runs during a mid-window re-render, previousValueRef.current === null && !isFirstConnection evaluates to false, so the hook correctly stays in loading state until the actual callback arrives.
|
Reviewing... |
|
I would like to review it too |
|
Let's hold the PR a bit so it can be reviewed internally as well. CC: @Krishna2323 |
|
@fabioh8010 I updated the PR with your solutions, take a look! |
|
@Krishna2323 I had a discussion with Fabio on Slack, he greenlighted the current solution, so the PR is all yours now! @shubham1206agra some time ago you asked about a weird Onyx issue on Slack, we're most likely going to fix it with this PR. |
fabioh8010
left a comment
There was a problem hiding this comment.
Forgot to push this review 🙈
| // Reset internal state so the hook properly transitions through loading | ||
| // for the new key instead of preserving stale state from the previous one. | ||
| previousValueRef.current = null; | ||
| newValueRef.current = null; | ||
| shouldGetCachedValueRef.current = true; | ||
| sourceValueRef.current = undefined; | ||
| resultRef.current = [undefined, {status: options?.initWithStoredValues === false ? 'loaded' : 'loading'}]; |
There was a problem hiding this comment.
Maybe could we extract this into a function?
There was a problem hiding this comment.
Only if it won't complicate any further @JKobrynski
There was a problem hiding this comment.
Sure, we could but I'm not sure what would be the benefit? It's not reused anywhere and the placement seems self-explanatory to me, is there anything that I'm missing? 😄
tests/unit/useOnyxTest.ts
Outdated
| rerender(`${ONYXKEYS.COLLECTION.TEST_KEY}2`); | ||
| }); | ||
| } catch (e) { | ||
| fail("Expected to don't throw any errors."); |
There was a problem hiding this comment.
| fail("Expected to don't throw any errors."); | |
| fail("Expected to not throw any errors."); |
There was a problem hiding this comment.
Good point, done
|
Will start reviewing this today. |
|
Reviewing... |
There was a problem hiding this comment.
Looks good overall. A few suggestions:
-
lib/usePrevious.tsis no longer imported anywhere -- should we delete it? -
Test "should return cached value immediately..." doesn't actually assert synchronously after
rerender(). Consider adding a check right afterrerender()beforewaitForPromisesToResolve()to match the title, or adjusting the title. -
Would be nice to add tests for:
- Switching between non-collection keys (e.g.,
TEST_KEYtoTEST_KEY_2) since the PR description mentions arbitrary key changes now work initWithStoredValues: falsewith dynamic keys- A → B → A round-trip (returning to a previously used key)
- Switching between non-collection keys (e.g.,
|
@Krishna2323 done! Let me know what you think |
…fix-useonyx-state-not-resetting
…6-fix-useonyx-state-not-resetting
|
@Krishna2323 kindly bump 🙏 |
|
Reviewing... |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppandroid_hybrid.mp4Android: mWeb Chromeandroid_mWeb.mp4iOS: HybridAppios_hybrid.mp4iOS: mWeb Safariios_mWeb.mp4MacOS: Chrome / Safariweb_chrome.mp4 |
Krishna2323
left a comment
There was a problem hiding this comment.
LGTM and works well! 🚀
Details
Related Issues
GH_LINK Expensify/App#85416
Automated Tests
Unit tests were added for the new logic.
Manual Tests
In all of the cases above look out for:
Author Checklist
### Related Issuessection aboveTestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Avataris modified, I verified thatAvataris working as expected in all cases)mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
web-compressed.mov