Conversation
Adds Onyx.get(key), a public one-shot async read that resolves a key's current value without opening a subscription. The body runs inside OnyxUtils.afterInit, so a call made before init() finishes resolves with the hydrated value instead of undefined. A single key delegates to OnyxUtils.get and normalises null to undefined. A collection key returns the cached collection, or assembles one from getAllKeys and multiGet over the member keys, which also warms the cache. This closes the gap where the internal OnyxUtils.get cannot return a whole collection. Returns Promise<OnyxValue<TKey>>, typed mutable like useOnyx, so a value read with get() goes straight back into set, merge, mergeCollection, and setCollection without a read-only wrapper. No new storage machinery; these are the same reads useOnyx already performs.
ec1fe5b to
6edd3fc
Compare
|
@MelvinBot Assign reviewer @aimane-chnaif as requested in Expensify/App#101910 (comment) cc @mollfpr |
|
Assigned @aimane-chnaif |
|
I'm going to review later today as well |
|
@chuckdries @aimane-chnaif @tgolen FYI I made a change here in both
Apart from that the API will stay the same so it won't affect the refactor plan. If in the future we decide to bring back storage fallback to Onyx, it's something we only need to do in its internals and won't require changes in E/App. |
|
OK, thanks for the heads up! Just in case, are there any logs for when it
would have fallen back to storage? It would at least be nice to know if we
ever hit that log, or how often.
…On Tue, Sep 29, 2026 at 8:09 AM Fábio Henriques ***@***.***> wrote:
*fabioh8010* left a comment (Expensify/react-native-onyx#850)
<#850 (comment)>
@chuckdries <https://github.com/chuckdries> @aimane-chnaif
<https://github.com/aimane-chnaif> @tgolen <https://github.com/tgolen>
FYI I made a change here in both Onyx.get and Onyx.multiGet APIs to make
them read only for cache instead of falling back to Storage. The reason is
that:
1. All storage data is loaded into cache during startup and Onyx.get/
Onyx.multiGet are wrapped with OnyxUtils.afterInit, which guarantee
they only execute once Onyx is fully initialised. In reality, the storage's
fallback today is essentially dead code as it's never reached.
2. The store-based solution
<#834> is already
following the same approach for useOnyx/connect, so it makes sense
that Onyx.get would do the same for the sake of correctness.
Apart from that the API will stay the same so it won't affect the refactor
plan. If in the future we decide to bring back storage fallback to Onyx,
it's something we only need to do in its internals and won't require
changes in E/App.
—
Reply to this email directly, view it on GitHub
<#850?email_source=notifications&email_token=AAJMABZ6EGLEIZHRTP6SUNT5RO7BHA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKOBZGE4TEMZVGYZ2M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5891923563>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAJMAB5V3RMQ4TAWUCEJU7T5RO7BHAVCNFSNUABFKJSXA33TNF2G64TZHMZTAOJYGM2TOOJWHNEXG43VMU5TKNJWGQZTSOBYHA2KC5QC>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
@tgolen I will have a look into that 👍 Also please hold merge here as #834 just got merged today and I have the check if we have conflicts |
aimane-chnaif
left a comment
There was a problem hiding this comment.
PR description is out of date with the code
The description says get() falls back to storage through OnyxUtils.get / OnyxUtils.multiGet and cites old line numbers and test counts, but the current head reads only from the cache via onyxSubscriptionManager.getState.
Please update the description to match the current implementation.
The type test wrote through the cached value while the JSDoc says read-only. A draft copy keeps the mutability check via the typeof annotation, since a spread alone drops readonly modifiers. The exports were never needed, tsconfig.test.json checks the file without them.
Details
Adds
Onyx.get(key), a public one-shot asynchronous read on theOnyxobject, so callers can read a key without opening a subscription.What it does
Onyx.get(key)andOnyx.multiGet(keys)join theOnyxobject (lib/Onyx.ts:772andlib/Onyx.ts:793, registered atlib/Onyx.ts:812-813) and come out of the default export. Both read once and open no subscription, so a caller can read a key without registering a listener.onyxSubscriptionManager.getState(key). No storage read and no wait for an in-flightGET.init()hydrates the cache from storage before it releases the deferred init task, and both methods run their body insideOnyxUtils.afterInit, so a call made beforeinit()finishes still resolves with the hydrated value. Same value a subscription to that key would deliver.OnyxUtils.getcould not do. A single key or collection member returns its value. Passingreport_gives all reports, passingreport_123gives one.set,merge,update, andmultiSet. The JSDoc andAPI.mdsay to treat them as read-only: a member is the live cached object, so an in-place edit skips notification, and a collection is a frozen snapshot, so adding a member needs a copy first.multiGetmaps over the keys and returns a positional tuple through a cast. Each slot isundefinedwhere its key has no value, without failing the other slots.multiGet([])resolves to[].Related Issues
Expensify/App#101910
Linked E/App PR
Expensify/App#102135
Automated Tests
This is a library read API with no UI surface, and the suite already asserts the behavior, so most of it needs no manual re-check. New cases added per method:
Onyx.get: 12 unit cases (10 indescribe('get'), 2 in the after-init block), 4 type checks intests/types/get.ts, and 2 perf benchmarks intests/perf-test/Onyx.perf-test.ts.Onyx.multiGet: 5 unit cases indescribe('multiGet')and 4 type checks intests/types/multiGet.ts.Manual Tests
This is a library API addition with no UI surface, so it is exercised through the linked E/App PR rather than in isolation:
react-native-onyxto this PR's HEAD.Author Checklist
### Related Issuessection above### Linked E/App PRsection above, and verified this change against it (E/App CI passed and manual testing completed)TestssectiontoggleReportand 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
Screen.Recording.2026-09-24.at.11.08.10.mov