Repository navigation
[$1500] Expose Onyx.get #101910
Copy link
Copy link
Open
Labels
ImprovementItem broken or needs improvement.Item broken or needs improvement.ReviewingHas a PR in reviewHas a PR in reviewWeeklyKSv2KSv2
Description
Coming from #expert-contributors
Problem
Some components subscribe to Onyx values purely so they can pass them to event handlers and side effect functions. When a component subscribes to an Onyx value it never uses for rendering UI, a change to that value re-renders the component without changing anything on screen, which is unnecessary work.
Solution
Expose an Onyx.get() function that can lazily read values from the store
- Ship Onyx.get() in react-native-onyx. Onyx.get(key: TKey): Promise<ReadonlyDeep<OnyxValue>> on the public object, implemented on tryGetCachedValue wrapped in afterInit. That is the read useOnyx already performs, and it handles whole collections..
- Define where it may be called. Handlers, network callbacks, actions and library helpers whose return value no caller consumes synchronously, useCallback and useEffect bodies among them, since both run on an event. Never a render body, a useOnyx selector, a useMemo body, a useState or useReducer lazy initializer, a useSyncExternalStore snapshot or module scope..
- Ban the Search snapshot keys. Our useOnyx wrapper redirects those keys and Onyx.get does not, so it always returns live data and a converted subscription silently changes what a Search-scoped component sees. Scope membership is a runtime property of the mounted tree, so rather than try to decide it, a new lint rule refuses all seven search-overridden keys outright. The ban follows from the redirect, not from anything about the keys themselves. If a future initiative removes snapshot_ and useOnyx wrapper, the rule can be lifted and these high-traffic keys converted..
- Enforce it. New lint rule no-unsafe-onyx-read takes every misuse that one file's syntax decides, tsc takes the forgotten await and any attempt to mutate what comes back, and ONYX-1 review takes the rest. Broken down in the thread details..
- Prove it on one pilot. useSwitchToDelegator is a great candidate because it has 8 useOnyx subscriptions, none of which are used for rendering. PoC commit.
- Convert the easy cases in small PRs. 248 bindings whose value is only used at event time and needs no other change, across 123 files, batched into 19 independent PRs. Details in the thread..
Issue Owner
Current Issue Owner: @TaduJRReactions are currently unavailable
Metadata
Metadata
Labels
ImprovementItem broken or needs improvement.Item broken or needs improvement.ReviewingHas a PR in reviewHas a PR in reviewWeeklyKSv2KSv2
Type
Projects
- StatusShow more project fieldsHIGH
Activity
chuckdries commentedon Sep 22, 2026
We may also want to do something like https://github.com/Expensify/App/blob/e9cc9d0c696597bf151fa877f7ac74f68c60a18b/.github/workflows/onyxConnectWithoutViewReviewers.yml where we ping a set of reviewers each time a new usage gets added
fabioh8010 commentedon Sep 23, 2026
Hey @chuckdries @tgolen @roryabraham @mountiny based on the lengthy discussion we had in the thread, @LukasMod and I came back with a revised rollout plan so we are all aligned before the next steps.
Phase 0 — API only
ReadonlyDeeptype for now. Why?Onyx.getonly in some test files already required ~50 signature changes inReportUtilsplus six other lib files. AnyReadonlyDeepvalue passed into a lib function typed with our current Onyx types requires changing that function's signature. Every handler in Phase 1 that passes a read value into a lib helper would hit the same thing, so we can't keep readonly and also promise "no lib changes" in the first Phases. I vote for removing it for now and revisiting it per key once we have real usages ofOnyx.getin the codebase.no-unsafe-onyx-read, ONYX-1, the docs and theaddUtilsToWindowcleanup, with three small additions to the lint rule:Onyx.getis allowed only undersrc/components,src/pages,src/hooksandtests. Everywhere else is an error.scripts/checkOnyxConnectBypass.tsalso coversno-unsafe-onyx-read, so an inlineeslint-disablefails CI.OnyxUtils.gettoo since we have a proper API now.Phase 1 — drop-in group, components and hooks
useOnyxline andawait Onyx.get(key)in the handler that used it. We can start with useSwitchToDelegator, which already has tests written before the conversion. We need to regenerate this list first though, since it was made before the proposal.useOnyxbindings were removed, whether any of the converted flows regressed, and how many of the ONYX-1 review comments caught real problems vs false alarms (this tells us how much maintenance the new API actually adds). Only then we decide if we continue to Phase 2.Phase 2 — the "needs a fix" group and lib replacements
On the component side, this is the
useOnyxusages that are not a straightforward change:useEffect. AuseOnyxusage can be replaced byOnyx.getinside an effect only if the Onyx value was not in that effect's dependency array. If it was, the subscription is what re-runs the effect and it stays. This is visible in the diff, so it can be a CI check instead of a review judgment.useOnyxcalls that use aselector.Onyx.gethas no selector option and returns the raw value, so the conversion has to call the selector on the result.useOnyxis used to build the key of the next one.On the lib side, this is where
connectWithoutViewgets reduced. Today ~91 of the ~195connectWithoutViewcalls only exist to keep a module variable up to date so a function can read it later, nothing reacts to the change. Those can become anawait Onyx.get(key)inside that function, but only when the function is already async, or when it returns nothing that a caller uses, so callers can keep calling it without changes. Same rule as above: if callers would have to become async, it goes to Phase 3. To keep this controlled, a lib file only gets added to the allowlist in a PR that also removes aconnectWithoutViewfrom that same file, reviewed by the same group that reviewsconnectWithoutViewtoday. Every approval removes a subscription instead of adding one.Phase 3 — reads moving into actions (follow-up work/proposal)
navigateToConciergeChatfrom 26 call sites to 1, plus every conversion Phases 1 and 2 skipped because it would make a chain of callers async, whether it started as auseOnyxin a component or aconnectWithoutViewin a lib.src/libs/actions, never in utils, with the render-reachability check running in CI for every converted function.tgolen commentedon Sep 23, 2026
I'd like to make a small request on the rollout plan. Could we please put the enforcement into place first, before introducing
.get()into the code?LukasMod commentedon Sep 24, 2026
I prepared 2 PRs based on @tgolen suggestion and the Phase 0 plan. I dropped the readonly changes to avoid having multiple signatures at this stage.
I also added one extra thing:
Onyx.multiGetfor reading multiple keys, which should prevent repetitivePromise.allblocks. Looking back at some previous PoCs, I often migrated multiple keys in a single handler, so it would be nice to have a ready-made method for those cases. WDYT about the idea and the name?rn-onyx: Expensify/react-native-onyx#850
get/multiGetE/App: #102135
OnyxUtils.getswap from testsaddUtilsToWindowchange, to resolve theget()override conflictmountiny commentedon Sep 24, 2026
Thanks for the update! Lets try to get on C+ for this
[-]Expose Onyx.get[/-][+][$1500] Expose Onyx.get[/+]melvin-bot commentedon Sep 24, 2026
mountiny commentedon Sep 24, 2026
@LukasMod @fabioh8010 - @aimane-chnaif will be C+ and reviewer on all the prs for this project. Please make sure they are tagged and added on all the prs as I think onyx prs will still autoassign randomly. Also feel free to start threads in Slack for PRs for easier communication so we can deliver this smoothly and sooner than later.
Thanks!
tgolen commentedon Sep 24, 2026
I like the addition of
multiGet()👍LukasMod commentedon Sep 25, 2026
On the EApp PR, I addressed the feedback and added a script to auto-assign reviewers for all
Onyx.get/multiGetusage, the same way we do forconnectWithoutView, as requested. For now, I assigned @tgolen, @mountiny, @chuckdries.The way I see it now:
End of Phase 0
fabioh8010 commentedon Sep 28, 2026
I'm taking over from @LukasMod here. Applied some review suggestions on Expensify/react-native-onyx#850, will continue working there.
fabioh8010 commentedon Sep 29, 2026
Updates:
main.LukasMod commentedon Oct 2, 2026
Updates:
melvin-bot commentedon Oct 21, 2026
@TaduJR — to hire @TaduJR on Upwork, add the External label so a job is created and the offer is sent automatically.