Summary:
`VirtualizedList`/`FlatList`/`SectionList` re-run a small amount of
bookkeeping on every render and on every scroll event (60–120 Hz on
ProMotion displays). This PR removes three allocations from those hot
paths without changing any observable behavior:
1. **`VirtualizedList.render` no longer builds a `Set` for
`stickyHeaderIndices` on every render** when the prop is not provided
(the common case). The Set is now only created when the prop is
present; the two `.has()` lookups use optional access.
2. **`ChildListCollection.forEach` returns early when there are no nested
child lists** (the common case) instead of allocating a
`Map.values()` iterator. This is called from `_onScroll` and the four
other scroll callbacks on every scroll event.
3. **`_orientation()` caches its result** and only rebuilds the object
when the `horizontal` prop changes. `I18nManager.isRTL` is a
module-load constant (only changes on app reload), so the cache is
invalidated solely by the `horizontal` prop. The object is replaced,
never mutated, which keeps `ListMetricsAggregator`'s field-based
invalidation correct.
## Changelog:
[GENERAL][CHANGED] - Reduce allocations in the `VirtualizedList` render and scroll path (avoid per-render `Set` allocation for `stickyHeaderIndices`, per-scroll-event `Map` iterator for the empty nested-list collection, and per-call `orientation` object allocation)
Pull Request resolved: https://github.com/react/react-native/pull/58593
Test Plan:
- `yarn test packages/virtualized-lists` → 9 suites, 186 passed, 69
snapshots:
- `ChildListCollection-test.js` (new): forEach over populated/empty
collection, removal, `forEachInCell`/`anyInCell`
- `VirtualizedList-test.js`: `stickyHeaderIndices` not forwarded when
the prop is absent (with `ListHeaderComponent`), forwarded when
provided; orientation cache identity + invalidation on
`horizontal` change
- `yarn flow-check` → 0 errors
- `yarn lint` → 0 errors, 0 warnings
- `yarn format-check` (changed files)
Micro-benchmark (Node v24, V8, 2M iterations, before vs after, same
machine; the real-world benefit is dominated by reduced GC pressure,
which is largest on low-end Android):
Reviewed By: Abbondanzo
Differential Revision: D121177373
Pulled By: javache
fbshipit-source-id: 1befd7e6d90fbd1fcea9c4cc8cf6d575c6c26111
Summary:
I added `ListItemComponent` to these types in https://github.com/react/react-native/issues/57754 and got the comment wrong. It says the component receives `index` and `separators` "in addition to the data provided to `renderItem`", which reads as if `renderItem` doesn't get them. It does. `_renderElement` in `VirtualizedListCellRenderer.js` passes the same `item`, `index` and `separators` to both.
This fixes that sentence and mentions the one thing that actually is different, which is precedence. When both are set, `ListItemComponent` wins and `_renderElement` logs a warning saying so.
## Changelog:
[INTERNAL] [FIXED] - Fix JSDoc for ListItemComponent in VirtualizedList types
Pull Request resolved: https://github.com/react/react-native/pull/58500
Test Plan:
Comment-only change in a `.d.ts`, no type changes. Checked the wording against `_renderElement`:
```js
if (ListItemComponent) {
return (
<ListItemComponent item={item} index={index} separators={this._separators} />
);
}
if (renderItem) {
return renderItem({item, index, separators: this._separators});
}
```
Reviewed By: cortinico
Differential Revision: D120077066
Pulled By: Abbondanzo
fbshipit-source-id: 489321ae950b169c9a985d18220d1ec84114e5fc
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/58329
`VirtualizedSectionList.scrollToLocation`, and `SectionList` through its wrapper, mapped `itemIndex` to the underlying flat list without skipping the section header. As a result, `itemIndex: 0` targeted the header and every other item was one row early. Sticky-header compensation was also skipped for the first item, allowing the header to obscure it.
This change adds the header row to the flattened target index and always applies the sticky-header offset using the current section header metrics. The public API remains zero-based, but callers that compensated for the old behavior must remove that compensation.
Migration:
- Replace `itemIndex: n + 1` workarounds with `itemIndex: n`.
- Replace `itemIndex: data.length` last-item workarounds with `itemIndex: data.length - 1`.
Thanks to Marc Rousavy (mrousavy) for the diagnosis: https://github.com/react/react-native/issues/50143
Changelog:
[General][Breaking] - Fix `SectionList` and `VirtualizedSectionList` `scrollToLocation` to account for section headers and sticky headers correctly.
Reviewed By: javache, ryanfrawley
Differential Revision: D118547738
fbshipit-source-id: 4025d4bea38fcf69aa769f6f9260715b521ec7df
Summary:
Fixes https://github.com/react/react-native/issues/57933.
`react-native/virtualized-lists` is published separately and imported `ReactNativeFeatureFlags` through `react-native/src/private/featureflags/ReactNativeFeatureFlags`, which is not listed in `react-native`'s `"exports"`. Metro therefore warned and fell back to file-based resolution whenever an app rendered a virtualized list.
Thanks huntie for the patch and the direction — this PR now applies it instead of the original approach:
- `ReactNativeFeatureFlags` is exposed on the existing private package boundary, `react-native/react-private-interface` (both the runtime getter and the `.js.flow` re-export);
- `VirtualizedList.js` and `VirtualizeUtils.js` import it from there.
No new `src/private/*` subpath is exported, and the feature-flag singleton is unchanged.
Per your review, the `scripts/monorepo-tests/__tests__/check-packages-test.js` and `scripts/shared/monorepoUtils.js` changes have been dropped — the PR is now just the patch above. Happy to look at enabling `react-native/no-deep-imports` on `virtualized-lists` as a follow-up if that's wanted.
`VirtualizeUtils.js` is included alongside `VirtualizedList.js` because it carried the same runtime deep import. The remaining occurrences are out of scope: the four in `react-native/jest-preset` are all `import type` and are erased before resolution, and the one in `VirtualizeUtils-test.js` is not shipped (`virtualized-lists` excludes `**/__tests__/**` from `files`).
## Changelog:
[GENERAL] [FIXED] - Fix the Metro package-exports warning caused by `react-native/virtualized-lists` importing an unexported React Native subpath.
Pull Request resolved: https://github.com/react/react-native/pull/57940
Test Plan:
No new test is added. The existing `virtualized-lists` suites already cover this route, because `VirtualizeUtils`/`VirtualizedList` read the flags at runtime through the new boundary.
Counterfactual — dropping only the `ReactNativeFeatureFlags` getter and its `import typeof` from `react-private-interface.js`, keeping the two `virtualized-lists` imports:
```text
TypeError: Cannot read properties of undefined (reading 'fixVirtualizeListCollapseWindowSize')
182 | let lastWillAddMore;
183 |
> 184 | if (ReactNativeFeatureFlags.fixVirtualizeListCollapseWindowSize()) {
| ^
at computeWindowedRenderLimits (packages/virtualized-lists/Lists/VirtualizeUtils.js:184:32)
at Object.<anonymous> (packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js:261:47)
Test Suites: 2 failed, 6 passed, 8 total
Tests: 20 failed, 1 skipped, 151 passed, 172 total
```
Restoring the getter makes it green again.
```text
$ yarn jest packages/virtualized-lists scripts/monorepo-tests packages/react-native/Libraries/ReactPrivate --runInBand
Test Suites: 9 passed, 9 total
Tests: 1 skipped, 176 passed, 177 total
Snapshots: 69 passed, 69 total
$ yarn flow-check
Found 0 errors
$ yarn lint
$ eslint --max-warnings 0 .
Done in 10.23s.
```
Reviewed By: javache
Differential Revision: D119164284
Pulled By: cortinico
fbshipit-source-id: 1e8ae3b85ae7fb28c55f207f887a97aad85b563c
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/58074
Pull Request resolved: https://github.com/react/react-native/pull/58000
VirtualizedList viewability only considered the scroll-axis viewport length. A horizontal list with zero height could therefore report items as viewable. Record the cross-axis length in the same scroll metrics snapshot and require both viewport dimensions to be non-zero for viewability.
Changelog:
[General][Fixed] - Prevent zero-sized lists from reporting viewable items
Reviewed By: zeyap
Differential Revision: D116699497
fbshipit-source-id: 29f8ceaf0e5ffe0b79c807aa749240558cfe115a
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/58000
VirtualizedList viewability only considered the scroll-axis viewport length. A horizontal list with zero height could therefore report items as viewable. Record the cross-axis length in the same scroll metrics snapshot and require both viewport dimensions to be non-zero for viewability.
Changelog:
[General][Fixed] - Prevent zero-sized lists from reporting viewable items
Reviewed By: zeyap, christophpurrer
Differential Revision: D116356139
fbshipit-source-id: 77c1b61bddfde1dcac9e670ddab80915ef42149a
Summary:
Fixes https://github.com/react/react-native/issues/53542
**Problem:** `maintainVisibleContentPosition` fails when FlatList data is updated rapidly with prepends in quick succession (e.g., chat receiving messages). After 2 prepends before native scroll drains, render window stays frozen, `onViewableItemsChanged` suppressed, `onEndReached` never fires.
**Root cause:** `pendingScrollUpdateCount` assumption is structurally unsound – it increments by 1 per prepend in `getDerivedStateFromProps` (0→1→2) but native scroll events are dispatched as unique events that coalesce. Verified in C++:
- `ScrollViewEventEmitter.cpp:13` – `onScroll` dispatched with `dispatchUniqueEvent("scroll", ...)`
- `EventQueue.cpp:29-50` – unique event whose target+type matches existing replaces in place (line 49) instead of appending
So N scroll events emitted before queue flush deliver exactly 1 to JS. Single coalesced event drains 2→1 leaving blocked. Also leak when JS predicate fires (old key found at new index) but native declines to adjust (tag recycled, view deleted, delta ≤0.5, clamped).
**Fix (1 file, 2 lines core):** Clamp pending to at most 1 and drain to 0 on any scroll:
- `pendingScrollUpdateCount: 1` instead of `prev+1` – prevents accumulation during rapid prepends
- `setState({pendingScrollUpdateCount: 0})` instead of `-1` – single coalesced scroll unblocks
This turns "stuck at N" into unblocked after next scroll, fixing frozen window / viewability / onEndReached for rapid updates. For leak path where native declines (no scroll ever), flag would still be stranded at 1 – addressed by boolean rename + escape hatch in follow-up, but this PR already strictly improves and matches existing tests that drain 0→1→0.
## Changelog:
[GENERAL] [FIXED] - Fix maintainVisibleContentPosition with rapid data updates (https://github.com/react/react-native/issues/53542)
Pull Request resolved: https://github.com/react/react-native/pull/57955
Test Plan:
**Jest (VirtualizedList):**
```bash
yarn jest --watchman=false packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js --no-coverage --ci
# Before: new coalesced test fails with pending 1
# After: 83 passed (82 existing + 1 new rapid prepends with coalesced scroll), 1 skipped, 59 snapshots
```
**New test ( regression for https://github.com/react/react-native/issues/53542 ):**
- Simulates 2 rapid prepends WITHOUT intermediate scroll (5 + 3 items)
- Only 1 coalesced scroll event (delta 8*ITEM_HEIGHT)
- Asserts `pendingScrollUpdateCount === 0` (fails on main with 1) and `firstVisibleItemKey` not null
**Existing MVCP tests still pass:**
- `handles maintainVisibleContentPosition`
- `handles multiple rapid prepends` (separate scroll per prepend)
- `delta stays bounded`
- `minIndexForVisible >0` and `inverted`
**Lint:**
```bash
yarn lint
# Done (max-warnings 0)
```
Closes https://github.com/react/react-native/issues/53542
Reviewed By: javache
Differential Revision: D115907610
Pulled By: fabriziocucci
fbshipit-source-id: 0cd59b35d7c43803a000b93779cd84c9c4202053
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57923
Changelog: [General][Fixed] - Ignore stale viewability updates while enforcing minimum view time
Minimum view-time callbacks can outlive a newer viewport update. During initial layout, callbacks for intermediate viewport snapshots can fire after the current snapshot and replace the correct visible-item set. Discard callbacks whose captured visible-index set is no longer current, and cover the transition with a unit test.
Reviewed By: Abbondanzo
Differential Revision: D115740467
fbshipit-source-id: 0a308dbc57c1d1fc5b2b669d998d1d9f8f1495f1
Summary:
When a list changes between vertical and horizontal layouts, `ListMetricsAggregator` resets its cell measurements but keeps `_contentLength`. That value belongs to the old scrolling axis. In horizontal RTL mode, it can then be used to calculate a cell offset before the new content layout is observed.
This clears `_contentLength` during horizontal orientation changes so RTL calculations wait for the new content length. The reset was present in the original orientation invalidation logic and was removed when bottom-up layout support was removed.
## Changelog:
[GENERAL] [FIXED] - Invalidate list content length when orientation changes.
Pull Request resolved: https://github.com/react/react-native/pull/57871
Test Plan:
- Added regression coverage for vertical to horizontal content length invalidation.
- Verified horizontal RTL metrics throw until new content layout is observed.
- Ran `yarn jest packages/virtualized-lists/Lists/__tests__/ListMetricsAggregator-test.js --runInBand`.
- Ran Prettier and targeted ESLint checks.
Reviewed By: christophpurrer
Differential Revision: D115586644
Pulled By: fabriziocucci
fbshipit-source-id: 6c0f1b435575c080444b72381f7ce56ae3f69a15
Summary:
`ListItemComponent` is a valid prop on `FlatList`, `SectionList` and `VirtualizedList` (it's in the Flow types in `VirtualizedListProps.js`), but it's missing from the `.d.ts`, so TypeScript errors on it even though it works at runtime.
Added it to `VirtualizedListWithoutRenderItemProps` next to the other `List*Component` props, so all three inherit it.
## Changelog:
[GENERAL] [ADDED] - Add missing ListItemComponent type to VirtualizedList props
Pull Request resolved: https://github.com/react/react-native/pull/57754
Test Plan:
Errors on `main`, type-checks with the change:
```tsx
<FlatList data={[]} ListItemComponent={MyItem} />
```
Type-only, addition only, no runtime changes.
Reviewed By: Abbondanzo
Differential Revision: D114061127
Pulled By: fabriziocucci
fbshipit-source-id: e843645a93b2320370829e8fd86deda078d6852a
Summary:
This PR fixes three bugs in the VirtualizedLists maintainVisibleContentPosition (MVCP) feature and adds comprehensive documentation for the architecture and testing approach.
It was intended to address the reports in https://github.com/react/react-native/issues/25239 but may also address others like https://github.com/react/react-native/issues/42915 and https://github.com/react/react-native/issues/53542.
**Bug Fixes:**
1. **Android: MVCP scroll position stale due to event throttle** — Scroll events during scroll animations and MVCP adjustments were being throttled by `scrollEventThrottle` (default 16ms, tests use 500ms), preventing the JS-side scroll offset state from updating to reflect the actual native scroll position. This caused MVCP delta calculations to use stale JS offset values, resulting in incorrect scroll corrections (e.g., delta of 143 instead of expected 44). Fixed by adding `emitScrollEventNoThrottle()` that bypasses the throttle after scroll animations end and after MVCP adjustments.
2. **iOS: MVCP correction fails when anchor view is deleted or recycled** — `_adjustForMaintainVisibleContentPosition` could apply a delta from a stale view's frame after `scrollToOffset(0)` during reset, resulting in incorrect offset (e.g., ~3876 instead of 0). Fixed by adding three abort conditions: nil check, tag check (detects view recycling), and superview check (detects view deletion).
3. **VirtualizedLists: Cell metrics not cleared on orientation change** — `ListMetricsAggregator` didn't clear `_cellMetrics` on orientation change, causing new cells to find stale entries and `_averageCellLength` to become NaN or Infinity. Also added a guard against divide-by-zero when `_measuredCellsCount` is 0. Both bugs broke scroll position calculations, content length estimates, and index-to-offset conversions.
**Documentation:**
Added documentation file under `packages/virtualized-lists/__docs__/`:
- `DESIGN.md` — MVCP architecture and implementation details
## Changelog:
[ANDROID] [FIXED] - Fix MVCP scroll position not updating due to scrollEventThrottle during animations and adjustments
[IOS] [FIXED] - Fix MVCP correction failing when anchor view is deleted or recycled during mount operations
[GENERAL] [FIXED] - Fix ListMetricsAggregator cell metrics not clearing on orientation change and guard against divide-by-zero
[INTERNAL] [ADDED] - Document MVCP architecture, history, and test coverage in `__docs__/`
Pull Request resolved: https://github.com/react/react-native/pull/57294
Test Plan:
- Maestro test `flatlist-throttle-maintainvisible.yml` passes — delta is within expected range on Android with 500ms throttle
- iOS: Verified MVCP correction works correctly after reset/clear, item deletion, and view recycling scenarios
- Verified cell metrics are correctly reset on orientation change (vertical ↔ horizontal)
- Verified `_averageCellLength` is guarded against divide-by-zero when no cells have been measured yet
Reviewed By: cipolleschi, javache
Differential Revision: D112200548
Pulled By: Abbondanzo
fbshipit-source-id: 27d55965707a2a24d08c80921012e6c359f2e9f9
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57611
Directly follows D112733139 (Metro).
**Motivation**
- New Node builtins are *only* available under the prefix (e.g. `node:sqlite`), as this allows Node to introduce them without ecosystem-breaking changes, so this is the direction of travel and the only choice that'll allow consistency.
- Encouraging them and grouping them separately makes it easier to reason about a module's 3rd party dependencies.
Changelog: [Internal]
Reviewed By: robhogan
Differential Revision: D112803684
fbshipit-source-id: 40668d746a7151b3aa4800ff8af997902a18d198
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57417
`SceneTracker` was a small helper in `Libraries/Utilities` that tracked the active navigation scene. It was only ever consumed through a deep import and was never part of the public API. This removes it from the React Native package and moves scene tracking to the application layer that actually depends on it.
As part of this, `AppRegistry.runApplication` no longer sets the active scene to the app key. To let the application layer preserve that behavior for surfaces launched by app key, `WrapperComponentProvider` now receives the app key as an optional second argument.
Changelog: [General][Breaking] - Remove the `SceneTracker` module from `Libraries/Utilities`, stop setting the active scene from `AppRegistry.runApplication`, and pass the app key as an optional second argument to `WrapperComponentProvider`
Reviewed By: huntie, javache
Differential Revision: D110471317
fbshipit-source-id: 28d44c74d2cff7926094959f26aa1ed0bf4be9ad
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57490
See [**RFC0894: Removing deep imports from react-native**](https://github.com/react-native-community/discussions-and-proposals/pull/894)
This is the **big switch** to enable the Strict TypeScript API (generated types + single index entry point) by default in React Native.
**Opt-in → opt-out**
After this change, the main `react-native` package resolves its `"types"` entry points only to `types_generated/index.d.ts` — with no other subpaths available.
The new `"react-native-legacy-deep-imports"` condition maps to legacy `types/` and `Libraries/*.d.ts` sources.
**Impact limitation**: For this stage of rollout, the `"default"` condition continues to resolve to source files. Only TypeScript is affected.
**How to opt out**
Opposite of today's opt-in, which we will update in [the docs](https://reactnative.dev/docs/strict-typescript-api). Again, the only impact area today is **TypeScript**.
```json5
// tsconfig.json
{
"extends": "react-native/typescript-config",
"compilerOptions": {
...
"customConditions": ["react-native-legacy-deep-imports"]
}
}
```
**Other changes**
- Drop `react-native/typescript-config/strict` entry point, update README.
- Update `__typetests__`.
**Rollout plan**
**Target release: 0.87**. This and the contributing stack will be cherry picked for RC1.
- We've conducted testing against 100+ real Expo codebases, giving us the confidence that we've reduced breaking changes enough that the vast majority of RN codebases can migrate.
- The Strict API includes a number of **intentional breaking changes**, and docs have been kept up to date.
- We're shipping a `/migrate-to-strict-api` skill to migrate via agents, see https://github.com/react-native-community/skills/pull/3.
**What's improved since 0.80?**
Since the initial opt-in launch of the Strict API in 0.80, we've been making continuous improvements over the last year to get our generated types into a widely launchable state.
Most notably:
- 21+ new/updated root APIs and fixes due to community feedback ([discussion](https://github.com/react-native-community/discussions-and-proposals/discussions/893), [PRs](https://github.com/react/react-native/pulls?q=is%3Apr%20label%3A%22JS%20API%20stabilization%20(1.0)%22%20is%3Aclosed)).
- Upstream encapsulation blocker in TypeScript, fixed in 6.0 (https://github.com/react/react-native/issues/53565).
- Tailwind/Uniwind compatibility (`interface` types for props).
- `*Instance` ref type exports for all built-in components (https://github.com/react-native-community/discussions-and-proposals/pull/1003).
- Fixes to previously mistyped, high impact APIs, such as `Appearance`.
- New subpath entry points for `asset-registry`, `setup-env`, and others.
- Refinements to doc comments/type translation build.
**Rollback plan**
Revert this diff.
IMPORTANT: We'll adopt a policy of **super-eager rollback**, if there are any unsolvable issues during the RC phase.
Changelog:
[General][Breaking] - React Native's default JavaScript API is now the [Strict TypeScript API](https://reactnative.dev/docs/strict-typescript-api). Use `customConditions: ["react-native-legacy-deep-imports"]` to opt out.
Reviewed By: cortinico
Differential Revision: D110458670
fbshipit-source-id: 4b0e0b458a5f895f783d6d936e7b11ccff2df076
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57273
**Motivation**
A README is the first thing an npm visitor sees on each package; internal-repo boilerplate like the "Testing" sections and the "We're using yarn…" note doesn't belong on a published package page.
**Changes**
- Standardize every README: a `# react-native/<name>` heading, a user-facing description, and a blue version badge plus a green monthly-downloads badge.
- Keep the "internal dependency" prelude only on `community-cli-plugin`, `virtualized-lists`, and `js-polyfills`.
- Drop the monorepo-template "Testing" sections and yarn note.
- Rework `metro-config` around the Configuring Metro guide; add missing READMEs for `metro-babel-transformer` and `popup-menu-android`.
Changelog: [Internal]
Reviewed By: emily8rown
Differential Revision: D109017270
fbshipit-source-id: 6208294c4ad2e6235a6605ae54f22d730f0476e7
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57258
Reflecting the move to https://github.com/react/react-native , update references in first-party `package.json`s
The `repository.url` field in particular is load-bearing for Trusted Publish.
Changelog: [Internal]
___
Reviewed By: fabriziocucci
Differential Revision: D108986410
fbshipit-source-id: 78513ffefc32ddf417dc1479a4834ebc44240080
Summary:
`VirtualizedList._createRenderMask` always did the sticky-header lookup, even when there were no sticky headers. For a large list scrolled far from the top, that meant walking backward from the first visible item toward index 0 on every render-mask update.
This changes that path to:
- skip the lookup when `stickyHeaderIndices` is missing or empty
- when sticky headers exist, scan the sticky header indices and pick the closest one above the viewport
- keep `ListHeaderComponent` offset handling and integer-index behavior
## Changelog:
[GENERAL][CHANGED] - Speed up VirtualizedList render-mask creation for large lists by avoiding the old backward sticky-header scan when sticky headers are missing or sparse.
## Affected components
This is inside `VirtualizedList`, so the affected callers are:
- `VirtualizedList`
- `FlatList`, because it renders through `VirtualizedList`
- `VirtualizedSectionList` / `SectionList`, because section lists also render through this path
`stickyHeaderIndices` does not need to be set to get the no-sticky win. A normal `FlatList` with no sticky headers still used to pay the backward scan. With this change, that case exits before the sticky-header helper runs.
When `stickyHeaderIndices` is set, the lookup changes from scanning item indices back toward 0 to scanning only the sticky header index array. The size of the win then depends on how many sticky headers are configured.
## Benchmark
Benchmark command:
```sh
yarn fantom --benchmarks packages/react-native/Libraries/Lists/__tests__/VirtualizedList-stickyHeaders-benchmark-itest.js --runInBand
```
Fantom Hermes benchmark, 100 samples per case. Values below are median latency for `VirtualizedList._createRenderMask` when the viewport is near the end of the list.
Per-call values are under 1 second, so they stay in milliseconds. For 1,000 render-mask updates, values over 1 second are shown in seconds.
| Case | One update before | One update after | Speedup | 1,000 updates before | 1,000 updates after |
| --- | ---: | ---: | ---: | ---: | ---: |
| 100k rows, no sticky headers | 2.664 ms | 0.0034 ms | 780x | 2.66 s | 3.42 ms |
| 100k rows, empty sticky headers | 2.632 ms | 0.0033 ms | 800x | 2.63 s | 3.29 ms |
| 100k rows, one top sticky header | 2.705 ms | 0.0054 ms | 499x | 2.71 s | 5.42 ms |
| 250k rows, no sticky headers | 6.543 ms | 0.0035 ms | 1,892x | 6.54 s | 3.46 ms |
| 250k rows, empty sticky headers | 6.579 ms | 0.0033 ms | 2,024x | 6.58 s | 3.25 ms |
| 250k rows, one top sticky header | 6.796 ms | 0.0053 ms | 1,294x | 6.80 s | 5.25 ms |
| 500k rows, no sticky headers | 13.173 ms | 0.0033 ms | 4,002x | 13.17 s | 3.29 ms |
| 500k rows, empty sticky headers | 13.073 ms | 0.0033 ms | 3,997x | 13.07 s | 3.27 ms |
| 500k rows, one top sticky header | 13.444 ms | 0.0054 ms | 2,511x | 13.44 s | 5.35 ms |
| 750k rows, no sticky headers | 19.524 ms | 0.0033 ms | 6,008x | 19.52 s | 3.25 ms |
| 750k rows, empty sticky headers | 19.572 ms | 0.0033 ms | 6,022x | 19.57 s | 3.25 ms |
| 750k rows, one top sticky header | 20.304 ms | 0.0054 ms | 3,778x | 20.30 s | 5.37 ms |
| 1m rows, no sticky headers | 26.190 ms | 0.0033 ms | 8,058x | 26.19 s | 3.25 ms |
| 1m rows, empty sticky headers | 26.039 ms | 0.0033 ms | 8,012x | 26.04 s | 3.25 ms |
| 1m rows, one top sticky header | 26.855 ms | 0.0053 ms | 5,075x | 26.86 s | 5.29 ms |
This benchmark is intentionally focused on this helper. It does not claim the whole app or whole list render becomes thousands of times faster. It shows that this hot helper is no longer proportional to the scroll distance from the top of the list.
Pull Request resolved: https://github.com/react/react-native/pull/57210
Test Plan:
```sh
yarn jest packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js packages/react-native/Libraries/Lists/__tests__/FlatList-test.js --runInBand
```
Passed: 3 suites, 101 tests, 1 skipped, 76 snapshots.
```sh
yarn fantom packages/react-native/Libraries/Lists/__tests__/FlatList-itest.js packages/react-native/Libraries/Lists/__tests__/SectionList-itest.js --runInBand
```
Passed: 2 suites, 64 tests.
```sh
yarn flow check
```
Passed: no errors.
```sh
./node_modules/.bin/eslint packages/virtualized-lists/Lists/VirtualizedList.js packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js packages/react-native/Libraries/Lists/__tests__/VirtualizedList-stickyHeaders-benchmark-itest.js
```
Passed.
```sh
git diff --check
```
Passed.
Reviewed By: javache
Differential Revision: D108890210
Pulled By: Abbondanzo
fbshipit-source-id: 7548ba29d77ac14a609665356ab7d09662820abe
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/56887
Drop support for Node.js 20. The minimum supported version is now `^22.13.0 || ^24.3.0 || >= 26.0.0`.
Node.js 20 reached end-of-life in April 2026 and is no longer actively maintained. This aligns React Native with the upcoming Metro requirement.
Changelog:
[General][Breaking] - Require Node.js >= 22.13.0
Reviewed By: christophpurrer, cortinico
Differential Revision: D105685641
fbshipit-source-id: f490e5a13e4289daba98c4d56dd8fdd341aef0db
Summary:
This PR is a follow-up of https://github.com/facebook/react-native/issues/56358, in order to improve and VirtualizedList tests.
I re-enabled the skipped test `retains batch render region when an item is appended`.
With React 19, the previous approach that was using `jest.runAllTimersAsync` in this test path was unstable because the pre-update render region could still be processing updates.
I adopted the same approach used in https://github.com/facebook/react-native/issues/56358 by introducing a small helper function, `advanceUntilLastCellIndexRendered`, which advances timers one step at a time and stops when the expected state is reached: `cellsAroundViewport.last === items.length - 1`.
As part of this follow-up, I also aligned `advanceUntilRenderAreaChanged` to use performNextBatch for consistent stepwise timer advancement.
## Changelog:
[GENERAL][FIXED] - Re-enabled VirtualizedList "retains batch render region when an item is appended" tes
Pull Request resolved: https://github.com/facebook/react-native/pull/56653
Test Plan:
- Ran the VirtualizedList-test.js and verified all test cases passed.
- Verified test modification correctness by intentionally breaking the related snapshot and check that it is breaking/it is failing.
Reviewed By: cortinico
Differential Revision: D104291284
Pulled By: Abbondanzo
fbshipit-source-id: e32f8707a837a1805ce130d526768228f684e3e7
Summary:
In `VirtualizedList-test.js`, the test `adjusts render area with non-zero initialScrollIndex` relied on a hardcoded timer `jest.advanceTimersToNextTimer(3)` to wait for `VirtualizedList` to expand its render window when `initialScrollIndex` is passed as prop.
When `initialScrollIndex` > 0, `VirtualizedList` blocks the render area recalculation until a valid scroll event is received (`pendingScrollUpdateCount`). Once unblocked, a series of batched `setTimeout` callbacks is scheduled through `_scheduleCellsToRenderUpdate` (triggered from `_onLayout`, `_onContentSizeChange`, and then from `componentDidUpdate`). Those callbacks eventually lead to updates of `cellsAroundViewport.first/last` in `_updateCellsToRender`.
The `cellsAroundViewport` property of `VirtualizedList` describes the currently rendered range of item indices, including overscan items outside the visible viewport. The hardcoded magic number 3 was effectively tied to how many internal timer callbacks fired from the chain of calls above.
So I replaced the fixed count (`3`) with a small helper function, `advanceUntilRenderAreaChanged`, that advances timers one tick at a time (every time in its own act to let the `VirtualizedList` component update) and stops as soon as `cellsAroundViewport` changes.
If this doesn't happened, I trigger a specific error to report that the render area did not been change as expected.
## Changelog:
[GENERAL][FIXED] - Improve render area change with initialScrollIndex non zero test in VirtualizedList to avoid magic numbers timers
Pull Request resolved: https://github.com/facebook/react-native/pull/56358
Test Plan:
- Ran the `VirtualizedList-test.js` and verified all test cases passed.
- Verified test modification correctness by intentionally breaking the related snapshot and check that it is breaking/it is failing.
Reviewed By: zeyap
Differential Revision: D100649294
Pulled By: Abbondanzo
fbshipit-source-id: fc04c6c44fa311f9ef7263ceeea3bac3f44a8e13
Summary:
I was upgrading bare react native project to expo sdk 54 and stumbled upon
```
"TurboModule system assumes returnType == void iff the method is synchronous."
```
error from `turbomodule/core/TurboModuleInteropUtils.kt`
I thought that `iff` with double `f` was a typo and wanted to submit a pr with fix - I learned that it means `if and only if` - decided to scan the repo for any other typos anywany - submitting the ones that I've found.
## Changelog:
[Internal]
<!-- Help reviewers and the release process by writing your own changelog entry.
Pick one each for the category and type tags:
[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message
Pull Request resolved: https://github.com/facebook/react-native/pull/55651
Reviewed By: cipolleschi
Differential Revision: D93876470
Pulled By: cortinico
fbshipit-source-id: 43fc905bda14e77b27cdeda568bde1e2299d9d0f
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/55114
Our official [docs](https://reactnative.dev/docs/set-up-your-environment#node--watchman) require:
> Node 20.19.4 or newer
Currently, our `package.json#engines` fields express this as `"node": ">= 20.19.4"`
This is a bit imprecise because of Node's overlapping release lines - e.g. v24.0.0 is actually older than v20.19.4 (2025-07-15), and the whole v21 line was already EOL before v20.19.4.
The release *date* is relevant because Node.js frequently backports the most important updates - notably, `require(esm)` is already stable in Node 20.19 but is not unflagged in v22 until v22.12.
This makes the `package.json#engines` requirement truer to the documented requirement, dropping some old minors and EOL majors that prevent us using features included in v20.19.
Notably, `require(esm)` is stable and unflagged on all versions supported from this diff.
- v21 and v23 are EOL, so are dropped.
- v22.13 pre-dates 20.19.4 and is the first to have unflagged silent `require(esm)`
- v24.3.0 pre-dates 20.19.4 and is the first to unflag TS-stripping
- Support v25 and newer.
Changelog:
[General][Breaking] Drop support for EOL Node.js lines and old minors.
Reviewed By: cortinico, huntie
Differential Revision: D90467358
fbshipit-source-id: a5fdfb1b93a9d6cffe78eb6811ff7420ab906f44
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/54009
Packages haven't been bumped on main ahead of 0.83. This takes care of it.
Changelog:
[Internal] [Changed] -
Reviewed By: cipolleschi, huntie
Differential Revision: D83580515
fbshipit-source-id: 7471e77f74e3fb3b4ee6538a49369b5df1393098