Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
3223df7
Use newest Onyx changes
fabioh8010 Apr 16, 2026
7c53b58
Merge branch '@chrispader/bump-onyx-to-3.0.46' into feature/onyxutils…
fabioh8010 Apr 21, 2026
d4052e2
Use newest Onyx changes
fabioh8010 Apr 21, 2026
450c868
Fix usage of OnyxUtils.get
fabioh8010 Apr 21, 2026
4e40a58
Merge branch 'main' into feature/onyxutils-get-synchronous-2
fabioh8010 Jul 22, 2026
eae5a28
Merge branch 'main' of github.com:Expensify/App into feature/onyxutil…
LukasMod Jul 29, 2026
8c7876f
Merge branch 'main' of github.com:Expensify/App into feature/onyxutil…
LukasMod Jul 29, 2026
511a0f1
Merge branch 'main' of github.com:Expensify/App into feature/onyxutil…
LukasMod Aug 3, 2026
f556ef8
Merge branch 'main' into feature/onyxutils-get-synchronous-2
fabioh8010 Aug 4, 2026
c9d9e27
Merge remote-tracking branch 'origin/main' into feature/onyxutils-get…
fabioh8010 Aug 4, 2026
e39dfac
Update patch file
fabioh8010 Aug 4, 2026
4b90db1
Merge branch 'main' of github.com:Expensify/App into feature/onyxutil…
LukasMod Aug 5, 2026
e7d723f
update patch
LukasMod Aug 5, 2026
8636510
Merge branch 'main' into feature/onyxutils-get-synchronous-2
fabioh8010 Aug 6, 2026
b56b537
Update patch file
fabioh8010 Aug 6, 2026
212dea3
Merge remote-tracking branch 'refs/remotes/fork-ck/feature/onyxutils-…
fabioh8010 Aug 6, 2026
fe03665
Merge branch 'main' into feature/onyxutils-get-synchronous-2
fabioh8010 Aug 7, 2026
2ad249f
Merge branch 'main' of github.com:Expensify/App into feature/onyxutil…
LukasMod Aug 12, 2026
6b07fdb
Add render-position lint rule and caller-graph checker for synchronou…
LukasMod Aug 13, 2026
1da41bb
Add Onyx synchronous read semantics test suites
LukasMod Aug 13, 2026
26b4238
Expose get on the public Onyx export in the react-native-onyx patch
LukasMod Aug 13, 2026
cacab04
Read Onyx synchronously through the public Onyx.get instead of the On…
LukasMod Aug 13, 2026
5bd92ce
Move useSwitchToDelegator off eight Onyx subscriptions onto event-tim…
LukasMod Aug 13, 2026
61adf97
Drop the no-op await on the now synchronous OnyxUtils.get in tests
LukasMod Aug 13, 2026
318a7cc
Move bulkDuplicateReports off eleven Onyx subscriptions onto its own …
LukasMod Aug 13, 2026
7aa5422
Shadow the library get on window.Onyx instead of intersecting with it
LukasMod Aug 13, 2026
83edad4
Move PayActionCell off four Onyx subscriptions onto press-time reads
LukasMod Aug 13, 2026
193b529
Drop the two module-level collection caches in replaceOptimisticRepor…
LukasMod Aug 13, 2026
aa29dee
Correct the A4 test comment that credited the patch for derived-key f…
LukasMod Aug 17, 2026
2d59353
Merge branch 'main' of github.com:Expensify/App into feature/onyxutil…
LukasMod Aug 17, 2026
35951b0
replace patch with commit link
LukasMod Aug 17, 2026
bc23444
update gh commit link
LukasMod Aug 17, 2026
475ad7e
fix knip, spellcheck, unit tests
LukasMod Aug 17, 2026
a3d8540
update addUtilsToWindow
LukasMod Aug 17, 2026
c9d24c1
Read Onyx through the public get instead of the OnyxUtils deep import…
LukasMod Aug 17, 2026
7a6d0fe
Merge branch 'feature/onyxutils-get-synchronous-2' into feature/onyx-…
LukasMod Aug 17, 2026
b1db155
Read tsconfig paths through TypeScript so the reachability checker su…
LukasMod Aug 17, 2026
6947ff2
Merge branch 'feature/onyxutils-get-synchronous-2' into feature/onyx-…
LukasMod Aug 17, 2026
6e1c752
Test that a synchronous read misses storage-backed data before Onyx h…
LukasMod Aug 17, 2026
569e00f
fix onyx derived init
LukasMod Aug 17, 2026
4444df0
Merge branch 'feature/onyxutils-get-synchronous-2' into feature/onyx-…
LukasMod Aug 17, 2026
d3bcd8c
add 2 more onyx get eslint rules
LukasMod Aug 17, 2026
eea5442
scripts for analysis
fabioh8010 Aug 18, 2026
e7f0876
Merge remote-tracking branch 'upstream/main' into feature/onyxutils-g…
LukasMod Aug 18, 2026
23e7ca2
Merge branch 'feature/onyxutils-get-synchronous-2' into feature/onyx-…
LukasMod Aug 18, 2026
140e4f0
Restore hydration-safe GPS draft read dropped by the main merge
LukasMod Aug 18, 2026
53fb2ca
Merge branch 'feature/onyxutils-get-synchronous-2' into feature/onyx-…
LukasMod Aug 18, 2026
8cf53c1
Match the onyx counters anchor to both keyChanged signatures
LukasMod Aug 18, 2026
5295689
Merge branch 'main' of github.com:Expensify/App into feature/onyxutil…
LukasMod Aug 18, 2026
cc894bd
Merge branch 'feature/onyxutils-get-synchronous-2' into feature/onyx-…
LukasMod Aug 18, 2026
9d07fea
add 2 more onyx get eslint rules
LukasMod Aug 17, 2026
3348e05
Merge branch 'feature/onyxutils-get-synchronous-2' into feature/onyx-…
LukasMod Aug 18, 2026
3a07db5
Merge the three Onyx read rules into no-unsafe-onyx-read
LukasMod Aug 19, 2026
8ec1d11
Point the Onyx bypass runner and graph scripts at the merged rule
LukasMod Aug 19, 2026
dad3090
Merge branch 'main' of github.com:Expensify/App into feature/onyxutil…
LukasMod Aug 19, 2026
77b1ca0
Merge branch 'feature/onyxutils-get-synchronous-2' into feature/onyx-…
LukasMod Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/eslint/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ const config = defineConfig([
'rulesdir/require-live-region-for-status-updates': 'error',
'rulesdir/require-a11y-disable-justification': 'error',
'rulesdir/no-direct-pre-insert-fullscreen-under-rhp': 'error',
// Position and order for the synchronous Onyx read: not during render, where the read does not subscribe; not at module
// scope, where it runs at import time, before Onyx.init() has hydrated the cache; and not after an un-awaited write in
// the same body, where Onyx.merge() and Onyx.update() apply in a later microtask so the read returns the pre-write value.
// Ships as a warning while the synchronous read is still being introduced; promoted to an error once the first wave of conversions lands.
'rulesdir/no-unsafe-onyx-read': 'warn',
'rulesdir/require-locale-for-localized-date-format': 'error',
'rulesdir/prefer-narrow-hook-dependencies': [
'error',
Expand Down
590 changes: 590 additions & 0 deletions eslint-plugin-local-rules/no-unsafe-onyx-read.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@
"react-native-nitro-fetch": "1.5.4",
"react-native-nitro-modules": "0.36.3",
"react-native-nitro-sqlite": "9.6.0",
"react-native-onyx": "3.0.95",
"react-native-onyx": "git+https://github.com/Expensify/react-native-onyx#073821fb322df72f43a6510575f8b1abd93af2e9",
"react-native-pager-view": "8.0.0",
"react-native-pdf": "7.0.2",
"react-native-permissions": "^5.4.0",
Expand Down
88 changes: 88 additions & 0 deletions scripts/README-onyx-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Onyx subscription analysis tooling

Three scripts behind the figures in the `Onyx.get()` proposal. They answer three different
questions and are independent of each other.

| Script | Question it answers |
| --- | --- |
| `analyzeOnyxSubscriptions.ts` | Which `useOnyx` bindings are never read during render, and which `Onyx.connect`/`connectWithoutView` callbacks are nothing but module-level caches |
| `instrumentOnyxCounters.mjs` | What Onyx actually notified during a recorded interaction, and when |
| `renderCensus.mjs` | How many renders in a session are components re-rendering for Onyx data they never display |

Nothing here is wired into CI. They are analysis tools, run on demand.

## analyzeOnyxSubscriptions.ts

```bash
bun scripts/analyzeOnyxSubscriptions.ts # summary + candidate tiers
bun scripts/analyzeOnyxSubscriptions.ts --connect # Onyx.connect vs connectWithoutView
bun scripts/analyzeOnyxSubscriptions.ts --provenance <sha> # migrated legacy vs net-new
bun scripts/analyzeOnyxSubscriptions.ts --json # per-binding data, feeds renderCensus
bun scripts/analyzeOnyxSubscriptions.ts --file <path> # one file, per-binding verdict
bun scripts/analyzeOnyxSubscriptions.ts --src /other/App/src # analyse a different checkout
```

Purely syntactic — each file is parsed alone, with no type-checker. References resolve by name
within the declaring function rather than through the symbol table, so a shadowed name is
over-counted as a reference. That biases a binding toward `render`, which is the safe direction:
**the non-render set is a lower bound.**

**Run `--provenance` from the repo root, without `--src`.** It compares a `git ls-tree` of the
baseline against the current working tree, and git returns repo-relative paths. An absolute
`--src` silently reports 100% net-new because nothing matches.

A binding counts as *non-render* when no reference to it is reachable during render: every
reference sits behind a function boundary that is not a `useMemo` callback, and none appears in
JSX. IIFEs and synchronous array callbacks (`map`, `filter`, `reduce`, …) are treated as
transparent, because they run where they are written — an earlier version got this wrong and
mis-classified 99 bindings as non-render.

## instrumentOnyxCounters.mjs

Installs notification counters and a timeline into the installed copy of `react-native-onyx`.

```bash
node scripts/instrumentOnyxCounters.mjs . # install
node scripts/instrumentOnyxCounters.mjs --status . # check
node scripts/instrumentOnyxCounters.mjs --uninstall . # remove, byte-for-byte
```

Then restart the dev server — the bundler caches `node_modules`, so a running server serves the
uninstrumented build. On load the console prints `[onyxStats] installed`. In the app:

```js
__onyxStats.reset() // immediately after clicking Record in the profiler
__onyxStats.timeline() // every notification in fire order, ms since reset
__onyxStats.gaps() // pauses >= 100ms, where a separate React commit becomes likely
__onyxStats.json() // blob for offline comparison
```

**Edits `node_modules` in place.** Uninstall before regenerating any Onyx patch, or the counters
get baked into it. Hooks attach only at the top of `keyChanged` and `keysChanged`, whose
signatures are identical in patched and unpatched builds, so the same instrumentation applies to
both sides of an A/B and cannot introduce a difference of its own.

**Reset immediately after starting the profiler.** React's timestamps start at profiling start and
`__onyxStats` starts at `reset()`; if they are seconds apart, matching a notification to the commit
it caused becomes guesswork.

## renderCensus.mjs

```bash
bun scripts/analyzeOnyxSubscriptions.ts --json > /tmp/analysis.json
node scripts/renderCensus.mjs --classifier /tmp/analysis.json <profile.json>
```

Takes a React DevTools profile export and counts renders that are provably wasted: a hooks-only
render of a file where *every* `useOnyx` binding is non-render. Requires **"Record why each
component rendered"** enabled in the profiler, otherwise there is nothing to attribute.

Deliberately a lower bound. Mixed files are excluded rather than guessed at, because
`changeDescriptions` reports hook indices across all hooks and custom-hook flattening makes
index-to-binding mapping unreliable. Hook files are excluded too, since a hook's subscriptions
surface under whichever component called it.

Two things it has to do that are easy to miss: `snapshots` holds only the tree as it stood when
recording began, so components mounted later are read out of the `operations` log instead
(skipping this drops name resolution to ~26%), and React Compiler wraps display names as
`Forget(X)`, which are unwrapped before matching.
Loading
Loading