Audience: React Navigation maintainers.
Purpose: a complete, cited catalog of every place the One framework stack
(~/one — the One router + the vxrn libraries) forks, patches, vendors, aliases,
or otherwise works around React Navigation and its directly-related navigation
packages, so the divergences can be evaluated for upstreaming.
Author: air research worker (track #18). Draft for review, not committed.
One does not carry a patches/ entry for any @react-navigation/* package.
Instead it works around React Navigation in four ways:
- Two install-time dependency patches to
@react-navigation/core(declared inpackages/vxrn/src/patches/builtInDepPatches.ts, applied by vxrn's own patch engine, notpatch-package). - A vendored fork of ~18 React Navigation source files under
packages/one/src/fork/, copied from tag@react-navigation/core@7.1.2and modified in place (every divergence marked// @modified). - Forks of individual navigators / components outside
fork/(StackRouter,Link,useFocusEffect, the Tabs router, a web reimplementation of the native stack view). - A web alias replacing
react-native-safe-area-contextwith an in-house reimplementation, plus native/web file-split navigator factories.
The three headline drivers, in order of how much code they force One to fork:
- SSR + hydration determinism. React Navigation generates route
keys withnanoid()(non-deterministic), and itsBaseNavigationContaineris built for a live client. One forksgetStateFromPathto emit deterministic keys, forksNavigationContainerto add a server-only fast path, and reaches into three un-exported internal context modules to build a lean SSR container. This single theme cascades into a second fork (see the native deep-link bug below). - File-based routing semantics. Groups
(group), dynamic[param], catch-all[...param], and per-groupinitialRouteNameare not concepts React Navigation's linking layer has, sogetStateFromPath/getPathFromStateare heavily rewritten (59 and 36 marked edits respectively). - The native/web split.
@react-navigation/native-stackandreact-native-safe-area-contextare native-first; One provides web replacements or aliases behind the same import.
Version context (read every "v8 fixes it" claim against this):
| stack | @react-navigation/native |
@react-navigation/core |
other |
|---|---|---|---|
~/one (installed) |
7.2.2 |
7.17.2 |
native-stack 7.14.10, routers 7.5.3, elements 2.9.14, bottom-tabs 7.15.9, drawer 7.9.8; screens 4.23.0, safe-area-context 5.6.2 |
~/soot |
7.2.2 |
(via one) | screens 4.23.0, safe-area 5.6.2 — no soot-specific nav patch |
~/takeout |
7.1.28 |
(via one) | drawer 7.7.13 — no takeout patches dir at all |
upstream main (clone) |
8.0.0-alpha.34 |
8.0.0-alpha.25 |
v8 is alpha only, not stable-released (latest alpha dated 2026-07-08) |
The fork files were copied from @react-navigation/core@7.1.2; the installed
packages are 7.2.2 / 7.17.2. So One is already a few minors behind its own
fork base, and two majors behind upstream main. v8 has not shipped
stable, which matters: nothing here is "fixed by simply upgrading" today.
~/soot and ~/takeout add no navigation forks/patches of their own — they
are pure consumers of One's versions. Everything below lives in ~/one.
Both are declared in packages/one/../vxrn/src/patches/builtInDepPatches.ts and
applied to node_modules at install by vxrn's patch runner
(packages/vxrn/src/utils/patches.ts). They rewrite files in the installed
package, patch-package style.
What it is. Patches @react-navigation/core's package.json exports to add
subpath entries for three internal modules
(builtInDepPatches.ts:8-32):
// expose internal contexts for SSR-optimized NavigationContainer
// validated against 7.x. when bumping past 7.x, add a new entry pinned to the
// new major and re-verify the lib/module/* paths still exist.
for (const path of [
'./lib/module/NavigationBuilderContext',
'./lib/module/NavigationStateContext',
'./lib/module/EnsureSingleNavigator',
]) {
if (!exports[path]) { exports[path] = path + '.js'; changed = true }
}Why it exists. One's SSR container
(packages/one/src/fork/SSRNavigationContainer.tsx:10-16) imports those three
contexts directly to provide static/no-op values during server render:
// @ts-ignore internal module (exports patched at install time)
import { NavigationBuilderContext } from '@react-navigation/core/lib/module/NavigationBuilderContext'
import { NavigationStateContext } from '@react-navigation/core/lib/module/NavigationStateContext'
import { SingleNavigatorContext } from '@react-navigation/core/lib/module/EnsureSingleNavigator'@react-navigation/core's exports map only exposes . and ./package.json,
so these modules are unreachable through the public API and Node's exports
resolution would throw without the patch.
Ideal upstream fix. Either (a) officially export the low-level context
objects (NavigationBuilderContext, NavigationStateContext,
SingleNavigatorContext) under a stable ./contexts or unstable_ subpath, or
(b) ship a first-class lightweight SSR container (see §2.5) so consumers never
need the internals.
v8 status — NOT fixed (verified). In the clone, @react-navigation/core@8.0.0-alpha.25
package.json still exports only "." and "./package.json"
(~/github/react-navigation/packages/core/package.json exports block). The
internal context modules remain un-exported. The patch (or an equivalent) would
still be required on v8.
What it is. For @react-navigation/core >=7.17.0 <8.0.0, re-adds an export
alias in lib/module/index.js and lib/commonjs/index.js
(builtInDepPatches.ts:34-62):
// @react-navigation/core 7.17 renamed createComponentForStaticNavigation to
// createComponentForStaticConfigDeprecated, breaking @react-navigation/native
// which still imports the old name. re-export the old name as an alias.Why it exists. A version-skew regression inside React Navigation's own
monorepo: core@7.17 renamed the export, but a co-installed older
@react-navigation/native still imported the previous name, producing an
undefined import at runtime.
Nuance (honest). On the currently installed core@7.17.2 this patch is a
defensive no-op: node_modules/@react-navigation/core/lib/module/index.js:20
already re-exports the old name
(createComponentForStaticNavigationDeprecated as createComponentForStaticNavigation),
and the patch's own guard (if (contents.includes('createComponentForStaticNavigation')) return)
bails. It only bites on the specific 7.17.x sub-version that shipped the rename
without a back-compat alias.
Ideal upstream fix. Keep core and native export names in lockstep within
a minor line; never rename a public export that a same-generation sibling package
imports without shipping the alias in the same release.
v8 status — resolved by design (verified). In v8 the symbol is uniformly
createComponentForStaticConfig in both packages
(packages/core/src/StaticNavigation.tsx:700,
packages/core/src/createNavigatorFactory.tsx:7); the deprecated/renamed name
churn is gone. The patch is unnecessary once the whole stack is on v8 (and is
already inert on 7.17.2).
react-native-gesture-handlergetShadowNodeFromRefnull-guard (builtInDepPatches.ts:269-322). "fix RNGH crash on RN 0.81+ new architecture — getShadowNodeFromRef crashes whenfindHostInstance_DEPRECATEDreturns null." RNGH underpins Drawer and the gesture-driven stack, so it's in the navigation blast radius, but the bug and fix belong toreact-native-gesture-handler, not React Navigation. Upstream ask goes to RNGH.react-native-reanimated['jsx']transform (builtInDepPatches.ts:260-265) and@react-native-masked-view/masked-view(324-329) are build-time source transforms (Flow/JSX), not behavioral nav patches. Masked-view is a header/transition dependency; noted only so the list is exhaustive.
Every file here carries the banner "This file is copied from the react-navigation
repo … @react-navigation/core@7.1.2 … Please refrain from making changes … All
modifications except formatting should be marked with // @modified." The count
of marked divergences per file (a proxy for how far each has drifted):
| file | @modified markers |
drift |
|---|---|---|
getStateFromPath.ts (+ getStateFromPath-mods.ts) |
59 | heavy rewrite |
useLinking.ts |
53 | heavy rewrite |
getPathFromState.ts (+ getPathFromState-mods.ts) |
36 | heavy rewrite |
createMemoryHistory.tsx |
15 | moderate |
NavigationContainer.tsx |
7 | SSR fast-path graft |
useLinking.native.ts |
~4 | targeted deep-link fix |
validatePathConfig.ts |
3 | import-path only |
findFocusedRoute.tsx |
1 | import type only |
useThenable.tsx, useDocumentTitle(.native).tsx, useBackButton(.native).tsx |
0 | verbatim copies |
Two structural reasons stack up before any single bug:
- Run the linking layer in Node without importing React Native. The verbatim
copies state it plainly —
findFocusedRoute.tsx:8: "No modifications currently, copied so we can access without importing any React Native code in Node.js environments." One executesgetStateFromPath/ path resolution during SSR and at build time in a Node/RSC context; pulling these through@react-navigation/nativedrags inreact-native. - File-based routing. One layers expo-router-style semantics (groups,
dynamic, catch-all, per-group
initialRouteName) that the stock linking functions don't model.
Ideal upstream fix for (1). Guarantee that the pure path<->state helpers
(getStateFromPath, getPathFromState, getActionFromState, findFocusedRoute,
validatePathConfig) are importable from @react-navigation/core with zero
transitive react-native import, and are documented as Node-safe. They mostly
already live in core; the guarantee (and a subpath that a bundler can prove is
RN-free) is what's missing.
What it is. Two distinct classes of change:
-
Deterministic route keys.
getStateFromPath.ts:665-682:// HYDRATION FIX: Add deterministic keys to routes so react-navigation preserves them // instead of generating new ones with nanoid(). This prevents SSR/hydration key mismatch. let routeKeyCounter = 0 function getRouteWithKey<T extends { name: string }>(route: T): T & { key: string } { const key = `${route.name}-${routeKeyCounter++}` return { ...route, key } }
The counter is reset at the top of every call (
getStateFromPath.ts:102-103,resetRouteKeyCounter()), and keys are attached increateStateObject(:695-717). -
File-based routing rewrite. Group collapsing, catch-all/dynamic matching, per-group initial-route ranking, "params available at all levels," and Hermes-safe URL parsing all live in the sibling
getStateFromPath-mods.ts(e.g.getUrlWithReactNavigationConcessionsat:29, with// parse pathname and hash without URL constructor (Hermes doesn't support file: base URLs)at:38; the ranking comparatorgetRouteConfigSorterat:125-295).
Why it exists. React Navigation assigns route keys with nanoid()
(non-deterministic). Server-rendered state and client-hydrated state then get
different keys for the same route, so React Navigation treats them as different
routes and hydration mismatches. One makes keys a deterministic function of
name + ordinal so the server and client agree.
Ideal upstream fix. Make key generation injectable/deterministic — e.g. an
optional getRouteKey/seeded generator on the linking options, or content-based
keys derived from route name + position, used consistently in the routers and
in getStateFromPath. That single change removes One's need to fork
getStateFromPath for keys and removes the cascading native fix in §2.4.
v8 status — NOT fixed (verified). v8 routers still mint keys with nanoid():
packages/routers/src/BaseRouter.tsx:96
({ ...route, key: \${route.name}-${nanoid()}` }) and multiple sites in packages/routers/src/StackRouter.tsx (:301,307,326,349,367,411,660). v8's getStateFromPath` still emits keyless routes. The hydration determinism problem —
and this workaround — persist on v8. (The file-based-routing rewrite is inherent;
see §5.)
What it is. 36 marked edits (getPathFromState.ts + getPathFromState-mods.ts)
that extract a getPathDataFromState so One can read both path and params
(:79), swap in One's dynamic-segment/param-name helpers
(:227-230, using sharedModUtils.getParamName), and union extra config
(ConfigItemMods).
Why it exists. Same file-based-routing model as §2.1, in the reverse direction, plus the need to surface params separately from the path string.
Ideal upstream fix. Same as §2.1's file-based-routing half — this is inherent
to One layering a different route-tree model on top of React Navigation. v8 is
actively improving automatic path generation (CHANGELOG 8.0.0-alpha.19
"fix various issues with path generation with automatic paths"), which may
shrink the diff but won't remove the fork.
v8 status — inherent (not a bug v8 "fixes").
What it is. The most heavily forked file (53 edits). Distinct workarounds:
-
Hash navigation.
useLinking.ts:429-434:// @modified: workaround to make react-navigation handle hash changes if (index > previousIndex || (index === previousIndex && (!record || `${record?.path}${location.hash}` === path))) { const action = getActionFromStateRef.current(state, configRef.current) ...
Upstream's web
useLinkingwon't dispatch when only the#hashchanges on the same path, so hash navigation is inert; One forces an action. -
SSR fast path.
:169// @modified - SSR fast path: skip all client-only linking logic. -
Masked URLs /
__tempLocation.:237-243,:303-305— One tracks amaskedDisplayPathRefand restores a masked display path acrossonStateChange(a One feature: navigate to route X while showing URL Y). -
React Strict Mode double-mount guard.
:239// @modified - Track if initial history setup is complete (handles React Strict Mode double-mount). -
ServerContextreplaced.:275-281— upstreamServerContainersets location viaServerContext; One uses aninitialLocationprop instead and comments out the@react-navigation/webServerContextimport. -
Deterministic-key import.
:13,20— imports One's forkedgetStateFromPathfor the keys described in §2.1.
Why it exists. Web history integration, hash routing, URL masking, and SSR
location injection that One needs and stock useLinking doesn't provide (or
provides differently).
Ideal upstream fix.
- Hash: dispatch a navigation (or a dedicated hash-change event) when the path is unchanged but the fragment changed, or expose a documented hook for it.
- URL masking: first-class "navigate to state A, display URL B" support (expo-router and One both want this).
- SSR location: allow the initial location to be passed as a prop to the container rather than only through context.
v8 status — PARTIAL / UNVERIFIED. v8's web useLinking
(packages/native/src/useLinking.tsx:526-534) now preserves the hash when
writing the outgoing path if the focused route key is unchanged
(// Preserve the hash if the route didn't change → path = path + location.hash).
That addresses the path-generation/write side. On the incoming side, v8's
popstate handler (useLinking.tsx:281 history.listen(...), dispatch decision at
:436 "We should only dispatch an action when going forward") keys off the
history index delta, not a hash-only same-path change — the same shape One
forked — so One's incoming-hash concern most likely still applies. But I could not
fully trace it to a definitive verdict from static reading; mark unverified /
likely-not-fixed. v8 also "rework[ed]
server rendering API" (CHANGELOG 8.0.0-alpha.18, PR #13118), so the
ServerContext/initialLocation divergence should be re-evaluated against the
new API before assuming it's still needed. The masking and StrictMode guards
appear to have no upstream equivalent — treat as not fixed.
What it is. useLinking.native.ts:27-46 + :221-222:
// @modified: our fork of getStateFromPath assigns deterministic route keys
// (SSR/hydration stability), but upstream getActionFromState only produces a
// nested NAVIGATE for keyless routes — keyed routes degrade to a whole-tree
// RESET that the One router store immediately reverts, so incoming Linking
// 'url' events never navigated on native. Strip the keys before deriving the
// action; hydration keys only matter for web SSR, never for a live native
// deep link.
function stripRouteKeys(state) { /* recursively delete route.key */ }
...
// @modified: keys stripped so getActionFromState can produce NAVIGATE
const action = getActionFromStateRef.current(stripRouteKeys(state), configRef.current)Plus a smaller edit at :100-115 suppressing the "linking configured in multiple
places" console.error on Android, "where activity recreation causes remounts,"
and only warning when there is truly more than one handler.
Why it exists. Directly caused by §2.1. getActionFromState only emits a
NAVIGATE action when routes are keyless; with keys present it falls back to
a full-tree RESET, which One's router store then reverts — so native Linking
url events silently did nothing. One strips the keys it added, just for action
derivation.
Ideal upstream fix. Two independent options, either removes this:
- Make
getActionFromStateproduce aNAVIGATEfor keyed routes too (use the key when present rather than bailing toRESET); and/or - Solve keys deterministically upstream (§2.1) so One never adds keys that break action derivation.
v8 status — NOT fixed (verified). v8's getActionFromState
(packages/core/src/getActionFromState.tsx:73-81, and again :153-160) still
gates the NAVIGATE/RESET decision on firstRoute.key == null /
secondRoute?.key == null, emitting type: 'RESET' (:81) when keys are
present. The exact limitation One's comment describes is intact on v8.
Android warning suppression: upstream still emits the multi-handler
console.error; the false-positive-on-activity-recreation case is a real DX
paper-cut. Ideal fix: don't warn (or dedupe handlers) across an Android
activity recreation remount. v8 status: not fixed / unverified.
(Note: the getInitialURL 150ms Promise.race timeout at
useLinking.native.ts:55-63 with // Workaround for https://github.com/facebook/react-native/issues/25675
is copied verbatim from upstream — it is React Navigation's own workaround for
a React Native bug, not a One-added divergence. Listed so it isn't mistaken for
One's.)
What it is. NavigationContainer.tsx:86-99 grafts a server-only branch onto
the container:
// @modified - SSR fast path: bypass BaseNavigationContainer entirely
// BaseNavigationContainer has 32+ hooks and 7 providers that are all
// unnecessary on SSR (event emitters, child listeners, state sync, etc.)
if (typeof window === 'undefined') {
return <SSRNavigationContainer initialState={initialState} theme={theme} linking={linking}>{children}</SSRNavigationContainer>
}SSRNavigationContainer.tsx then hand-rolls the five contexts child
navigators actually read during render, with static/no-op values — a no-op
navigation ref (:41-67), a builder context whose scheduleUpdate runs the
callback immediately (:29-31, "useNavigationBuilder calls this during render"),
and a WeakMap cache of partial state keyed by state identity to "avoid deep
clone thrashing under concurrent SSR requests with different URLs" (:81-93).
Why it exists. BaseNavigationContainer is designed for a live client
(event emitters, useSyncExternalStore, child listener registries, back-button
handling). On the server all of that is dead weight; One measured it as "32+
hooks and 8 providers" and replaced it with a 5-provider static shell. This is a
performance workaround, not a correctness one — and it's the reason patch §1.1
exists (it needs the un-exported contexts).
Ideal upstream fix. Ship a first-class SSR/static render mode for the
container — a BaseNavigationContainer variant (or a mode="static" prop) that
renders the tree once with inert event/state machinery and no client-only hooks.
That removes both this fork and the §1.1 export patch.
v8 status — NOT fixed for the perf path (verified). v8 does have a
ServerContainer (packages/native/src/server/index.tsx) and reworked its
server-rendering API (CHANGELOG 8.0.0-alpha.18, PR #13118) — but that
ServerContainer is only a request-scoped location provider
({ location } on a context); it still renders the full
BaseNavigationContainer underneath. It does not eliminate the hook/provider
overhead One is avoiding. The lean static-render path One built has no upstream
equivalent yet. (Re-verify against the reworked API before shipping, but the
overhead concern stands.)
What it is. 15 marked edits over React Navigation's web memory-history
abstraction (the history.pushState/popstate bridge). Mostly retained upstream
logic (the file still carries upstream's own comments like the "hacky timeout"
at :262); One's edits adapt it to its history-record model.
Why it exists. One drives web history itself (masked URLs, history records
consulted in useLinking.ts:400-407), so it needs a memory-history it controls.
Ideal upstream fix. Low priority. If the web history integration were more composable (pluggable history adapter), One could inject rather than fork. Largely inherent to One owning web history.
v8 status — inherent, not a targeted bug.
What it is. Native URL→path extraction for Expo Go / dev-client / universal
links, including parsePathFromExpoGoLink and the "Major hack to support the
makeshift expo-development-client system" (extractPathFromURL.ts:68).
Why it exists. Feeds cleaned paths into the forked getStateFromPath. This is
expo-linking territory, not React Navigation — included because it lives in
fork/ and is part of the native linking path, but the upstream owner is Expo,
not React Navigation. No RN ask here.
findFocusedRoute.tsx, useThenable.tsx, useDocumentTitle.tsx /
.native.tsx, useBackButton.tsx / .native.tsx, and validatePathConfig.ts
carry 0–3 edits (imports only). They exist purely to be importable without the
@react-navigation/native → react-native chain (see §2.0-1). ui/useComponent.tsx
("Copied from @react-navigation/core", :4) is the same story for the
useComponent/NavigationContent helper.
Ideal upstream fix: the Node-safe subpath guarantee from §2.0-1 makes all of these unnecessary — One could import them instead of copying.
What it is. views/OneStackRouter.tsx:1-2 "forked from
@react-navigation/routers/src/StackRouter.tsx — the only changes have @nate before
them." It wraps StackRouter and overrides getStateForAction to special-case
catch-all routes (:14-26):
// @nate
// fix for [...spread] routes not having a stable useId() causing issues
// this fixes it to be stable because we actually don't want to stack when its a ...spread route
if (action.type === 'NAVIGATE') {
if (firstRoute.name.includes('[...') && outRoutes.every(x => x.name === firstRoute.name)) { ... }
}Web-only (process.env.TAMAGUI_TARGET !== 'native').
Why it exists. Navigating between two instances of the same catch-all
([...spread]) route pushed a new stack entry with a fresh key, so useId() and
mounted state weren't stable across what should be the same screen.
Caveats (honest). The override still contains several commented-out
abandoned attempts (:30-45, "this isn't working, we need a deeper solution") —
so this is an acknowledged partial fix, and it's partly entangled with One's own
key/stacking semantics (§2.1), not a clean upstream bug.
Ideal upstream fix. Give the stack router a way to treat a navigation to the
same route name with different params as a replace-or-update rather than a push
(configurable "singleton"/getId-style de-dup at the router level for catch-all
segments). v8's getId and retained/preloaded-route rework may already provide a
cleaner hook.
v8 status — UNVERIFIED. v8 reworked stack preload/retain semantics
(CHANGELOG 8.0.0-alpha.12 "add support for retaining screens", 8.0.0-alpha.13
"rework preload handling in stack" — a breaking change to state.routes)
and added <Activity mode="hidden"> support (8.0.0-alpha.5). Whether any of
that makes the spread-route id stable enough to drop this override needs a runtime
test on v8; do not claim it's fixed.
What it is. A file-split navigator factory:
layouts/stack-navigator.ts→ native:createNativeStackNavigator(stock).layouts/stack-navigator.web.ts→ web: One'screateWebStackNavigator.
The web view (router/web/WebStackView.tsx) reuses NativeStackView from
@react-navigation/native-stack but adds a render prop and keepMounted
option for overlay presentations (modal / formSheet / pageSheet), driven by
ScreenRenderContext.tsx (an open/dismiss protocol the render component uses
to animate overlays and then call StackActions.pop, :10-13).
Why it exists. @react-navigation/native-stack is native-first (it drives
UINavigationController via react-native-screens). On web, One wants
declaratively-controlled overlay presentations with custom animation and the
ability to keep a route's subtree mounted while navigated away. Stock native-stack
on web doesn't offer the render/keepMounted control One needs.
Ideal upstream fix. A documented, web-friendly overlay/presentation API on
native-stack (or a @react-navigation/web-stack) with mount-retention and a
custom-render hook. Partly overlaps v8's retained-screens work (§3.1).
v8 status — inherent to the native/web split; UNVERIFIED whether v8's retained-screens API narrows it.
What it is. link/Link.tsx:1-2 "Fork of @react-navigation/native Link.tsx
with href and replace support added and to / action support removed."
Why it exists. React Navigation's Link navigates via to/action props;
One wants a web-anchor-shaped API (href, replace, push, asChild via Radix
Slot) that matches file-based routing and renders a real <a> on web.
Ideal upstream fix. Support an href-style target and a replace prop on the
upstream Link (both are common asks). Note this is as much an API-design
preference as a gap — may stay a fork regardless.
v8 status — not a bug; API divergence. UNVERIFIED that v8's Link adds href/replace.
What it is. useFocusEffect.ts:1-2 "A fork of useFocusEffect that waits for
the navigation state to load before running the effect. This is especially useful
for native redirects." Also defaults deps to [] so a one-arg
useFocusEffect(useCallback(fn, [])) doesn't crash (:30).
Why it exists. One's routing resolves navigation state asynchronously
(loaders, native redirects); the stock useFocusEffect can run before the state
is ready. The deps default is a DX guard against a common crash.
Ideal upstream fix. (a) Default useFocusEffect's deps to []; (b) make it
wait for navigation.isReady() (or expose a ready-gated variant). Both are small,
self-contained upstream changes.
v8 status — UNVERIFIED.
What it is. ui/TabRouter.tsx extends React Navigation's TabRouter with a
triggerMap and a custom JUMP_TO action (ExpoTabRouterOptions,
ExpoTabActionType, :12-28) — the expo-router "headless Tabs" API.
Why it exists. A slot-based, fully custom tab-bar API on top of React Navigation's tab state machine. Derived from expo-router; inherent to offering that API surface.
Ideal upstream fix. None strictly required — React Navigation's TabRouter is
already the extension point being used here. Listed for completeness.
What it is. router/useScreens.tsx:33-34,75:
// `@react-navigation/core` does not expose the Screen or Group components directly, so we have to
// do this hack.
export const { Screen, Group } = createNavigatorFactory({} as any)()One instantiates a throwaway navigator factory with {} as any solely to pull
the Screen and Group components off the result.
Why it exists. @react-navigation/core returns Screen/Group only from a
constructed navigator, never as standalone exports.
Ideal upstream fix. Export Screen and Group as standalone components from
@react-navigation/core (they already exist as ./Screen and ./Group modules
internally).
v8 status — NOT fixed (verified). In v8, createNavigatorFactory.tsx:75-76
and :110-111 still return Screen/Group only from the factory result; the
core index (packages/core/src/index.tsx) exports createScreenFactory and the
Screen/Group types, but not the bare runtime components. v8's new
createScreenFactory does not help here — it's a static-config identity
helper (packages/core/src/StaticNavigation.tsx:427, body ((config) => config))
that returns a screen config object, not the runtime Screen/Group
components. The {} as any extraction hack still applies on v8.
What it is. packages/vxrn/src/config/getBaseViteConfigOnly.ts:111-114
aliases react-native-safe-area-context to the in-house @vxrn/safe-area
(packages/safe-area/), a from-scratch reimplementation of the safe-area-context
API surface (SafeAreaProvider, SafeAreaInsetsContext, SafeAreaFrameContext,
SafeAreaView, useSafeAreaInsets).
Why it exists. safe-area-context is native-first (reads insets from a native
module). On web there are no device insets; One provides a web implementation
(zero/env(safe-area-inset-*)-style) and controls bundle size. React Navigation's
elements/headers depend on safe-area-context, so this sits directly under the
nav stack.
Ideal upstream fix. None on React Navigation's side — this is a safe-area-context native/web concern. safe-area-context does ship a react-native-web build; One's alias is a deliberate replacement for behavior + bundle control. Inherent to the native/web split.
getBaseViteConfigOnly.ts:100-110 aliases react-native → react-native-web
(and empties react-native/Libraries/*). Not nav-specific, but it's why the
native-first nav packages (native-stack, screens) need web handling at all. Listed
as the substrate the native/web split rests on.
packages/one/src/screensFeatureFlags.ts toggles react-native-screens'
synchronous-layout feature flag; ui/TabSlot.tsx uses Screen/ScreenContainer
directly. This is normal API usage, not a fork/patch — included so the audit
is exhaustive.
- Root
~/one/package.json:99-100overrides pin@react-navigation/nativeto7.2.2andreact-native-reanimatedto4.2.1(dedupe pins, not patches). packages/vxrn/src/config/getBaseViteConfigOnly.ts:17-21andgetOptimizeDeps.ts:81-85list the@react-navigation/*packages for Vite pre-bundling/interop (they ship mixed ESM/CJS). Build config, not a fork.
These change which version resolves and how it's bundled, never its behavior.
Every "v8" cell was checked against the actual clone at
~/github/react-navigation (@react-navigation/core@8.0.0-alpha.25,
native@8.0.0-alpha.34). v8 is alpha, not stable — so "fixed in v8" means
"the alpha source no longer has the limitation," not "available in a release you
can ship today."
| # | Workaround | v8 status | Evidence |
|---|---|---|---|
| 1.1 | Export internal contexts patch | NOT fixed | v8 core package.json still exports only . + ./package.json |
| 1.2 | createComponentForStaticNavigation alias |
Resolved by design (and already inert on 7.17.2) | v8 uniformly uses createComponentForStaticConfig (StaticNavigation.tsx:700) |
| 2.1 | Deterministic keys / HYDRATION FIX | NOT fixed | v8 routers still nanoid() keys (BaseRouter.tsx:96, StackRouter.tsx) |
| 2.2 | getPathFromState file-based rewrite |
Inherent | v8 improving auto-paths (CHANGELOG alpha.19) but no file-based model |
| 2.3 | useLinking hash navigation |
PARTIAL / unverified | v8 added hash preservation on write (useLinking.tsx:526-534); incoming-dispatch not confirmed |
| 2.3 | useLinking masked URL / StrictMode / initialLocation |
NOT fixed (re-check vs reworked SSR API) | no upstream equivalent found; SSR API reworked (alpha.18) |
| 2.4 | stripRouteKeys native deep link |
NOT fixed | v8 getActionFromState.tsx:73-81 still gates NAVIGATE on key == null → RESET |
| 2.5 | SSR lean container | NOT fixed (perf path) | v8 ServerContainer only injects location; still renders full BaseNavigationContainer |
| 2.8 | Node-safe verbatim copies | NOT fixed | no documented RN-free subpath guarantee |
| 3.1 | Spread-route stable id (StackRouter fork) | UNVERIFIED | v8 reworked preload/retain (alpha.12/13, breaking) + Activity — needs runtime test |
| 3.2 | Web native-stack reimpl | Inherent / unverified | native/web split; v8 retained-screens may narrow it |
| 3.3 | Link href/replace |
Not a bug / unverified | API-shape divergence |
| 3.4 | useFocusEffect ready-gating + deps default |
UNVERIFIED | small upstream change if adopted |
| 3.6 | Screen/Group extraction hack |
NOT fixed | v8 still returns them only from createNavigatorFactory() |
| 4.1 | safe-area-context web alias | Inherent | native/web concern, not RN |
- §1.2 the
createComponentForStaticNavigationrename patch — v8's consistentcreateComponentForStaticConfignaming makes it unnecessary (and it's already a no-op on core@7.17.2). This is the only workaround an as-is v8 upgrade clearly retires.
- Deterministic route keys (fixes §2.1 and §2.4 together). Make key
generation injectable/deterministic — an optional seeded generator or
content-derived keys used consistently in the routers and
getStateFromPath. This removes One's biggest fork and the native deep-linkstripRouteKeysfork in one change. This is the single most valuable ask. getActionFromStateshould handle keyed routes. Emit aNAVIGATEfor routes that carry keys instead of falling back to a whole-treeRESET(getActionFromState.tsx:73-81). Independently removes §2.4 even if keys stay non-deterministic.- A real static/SSR render mode for the container. A
BaseNavigationContainervariant (ormode="static") that renders once with inert event/state machinery and no client-only hooks. Removes §2.5 and the §1.1 export patch. v8'sServerContaineris location-injection only; the overhead problem is unaddressed. - Export
ScreenandGroupas standalone components from@react-navigation/core. Removes §3.6 (the{} as anyfactory hack). One-line change; the modules already exist. - Guarantee a React-Native-free import path for the pure path/state helpers
(
getStateFromPath,getPathFromState,getActionFromState,findFocusedRoute,validatePathConfig), documented as Node/RSC-safe. Removes the §2.8 verbatim copies and reduces the reason to vendor at all. - Hash navigation on web: dispatch (or emit a documented event) on a same-path fragment-only change. Partially in progress in v8 (write side) — confirm the read/dispatch side. Removes half of §2.3.
useFocusEffectniceties: defaultdepsto[]and gate onnavigation.isReady()(or ship a ready-gated variant). Removes §3.4.- First-class URL masking ("navigate to state A, display URL B") — wanted by both One and expo-router. Removes part of §2.3.
- Web-friendly overlay/presentation + mount-retention API on native-stack (or a dedicated web-stack) with a custom-render hook. Narrows §3.2; overlaps v8's retained-screens work.
- File-based routing rewrites of
getStateFromPath/getPathFromState(§2.1 file-based half, §2.2) — a different route-tree model layered on top. - The web reimplementation of native-stack (§3.2) and the safe-area-context web alias (§4.1) — native-first packages need web bodies.
createMemoryHistory(§2.6) — One owns web history integration.extractPathFromURL(§2.7) and the Tabs router (§3.5) — Expo-lineage, not React Navigation's to fix.- The
Linkhref/replaceshape (§3.3) — an API-design preference, likely a permanent fork even if upstream addshref.
One (all paths under ~/one/):
packages/vxrn/src/patches/builtInDepPatches.ts(§1.1:8-32, §1.2:34-62, RNGH:269-322),packages/vxrn/src/utils/patches.ts(patch engine).packages/one/src/fork/—SSRNavigationContainer.tsx,NavigationContainer.tsx,getStateFromPath.ts(+-mods.ts),getPathFromState.ts(+-mods.ts),useLinking.ts,useLinking.native.ts,createMemoryHistory.tsx,extractPathFromURL.ts,findFocusedRoute.tsx,useThenable.tsx,useDocumentTitle(.native).tsx,useBackButton(.native).tsx,validatePathConfig.ts.packages/one/src/views/OneStackRouter.tsx,link/Link.tsx,useFocusEffect.ts,ui/TabRouter.tsx,ui/useComponent.tsx,router/useScreens.tsx,router/web/WebStackView.tsx,router/web/WebStackNavigator.tsx,router/web/ScreenRenderContext.tsx,layouts/stack-navigator.ts/.web.ts,screensFeatureFlags.ts.packages/vxrn/src/config/getBaseViteConfigOnly.ts(aliases),getOptimizeDeps.ts(prebundle),packages/safe-area/, rootpackage.jsonoverrides,packages/one/package.jsondeps.
Upstream (all under ~/github/react-navigation/, main @ 8.0.0-alpha):
packages/core/package.json(exports),packages/core/src/getActionFromState.tsx(:73-81),packages/routers/src/BaseRouter.tsx(:96) +StackRouter.tsx(key gen),packages/core/src/createNavigatorFactory.tsx(:75-76,110-111),packages/core/src/StaticNavigation.tsx(:700),packages/native/src/useLinking.tsx(:526-534),packages/native/src/server/index.tsx(ServerContainer),packages/core/CHANGELOG.md(alpha.5/12/13/18/19/25 entries).
Consumers checked, no nav forks found: ~/soot/package.json,
~/takeout/package.json (no patches/ dir).
better question here would be: what do you need in the public API of
NavigationContainerfor SSR? exporting internal contexts are problematic as then they become the public API.I'd rather figure out the behavior you need and then find a way to support it without exporting internal files.
this was a typo in a single version and not an intentional change. so I'll ignore this one.
the
@react-navigation/corepackage is pure JS code that should be able to run on any JS runtime. all of the helpers mentioned here don't importreact-native.pulling in through
@react-navigation/nativewill bring inreact-native- but it must as@react-navigation/nativeis for React Native integration.but what we can ensure is that all of the code that need
react-nativein the native package are in*.native.tsxfiles, soreact-nativeisn't imported when importing the package from a non-native environment. this will need some refactors to split the components, but our direction is to not depend onreact-native-webalias for web support, so this needs to be done anyway.do route keys need to be deterministic? if one adds route keys to state returned from
getStateFromPath, then carries it through to the client and provides ininitialStatetoNavigationContainer, iirc the same route keys would be re-used.i'm not completely opposed to deterministic keys, but it seems like a much bigger change and could cause potential problems and collisions if not careful. "hydrating" server state on the client seems like a simpler approach.
but also correct me if i'm wrong, keys don't need to be deterministic for SSR to to hydrate (unless the
keyis actually being rendered in text, which it shouldn't be) so this whole part maybe unnecessary.getStateFromPath. If the built-ingetStateFromPathlacks any capabilities that makes it impossible, then let me know which features.useRoutehook can accept a parent screen name to get its params. it also ensure only screens that need the params need to re-render when params change.is there any client-only logic? afaik there are only some hooks which would be no-op on the server anyway. does it have any measurable performance impact?
currently hash support is limited in React Navigation currently and only focused on preserving existing hash when opening a URL. but I welcome an RFC with a more detailed design proposal.
is this same as shared URLs? react navigation 8 supports 2 screens sharing the same URL. if not, i'll need more details on this feature (what it is, use case etc.)
is there bug in strict mode?
ideally we'd have React Navigation's
ServerContainersupporting what's needed for SSR so there isn't a need for fork.why do actions need deterministic keys?
this is already how
navigateworks by default. same route name with different params update the screen if it's focused (or if in stack when{ pop: true }is provided).also navigators support
UNSTABLE_routerprop in 7.x androuterprop in 8.x to override router logic without forking.we'd like to have more feature complete native stack on web, but we'll need to more planning around it.
useFocusEffectis intended to be used for side-effects (focus awareuseEffect), not for performing navigation. this seems like documentation/design issue if navigation is happening here. we won't gate it onnavigation.isReady()as it doesn't fit the design ofuseFocusEffect.guess the hack is fine. we don't want people to import and use
ScreenandGroupdirectly.iirc
react-native-safe-area-contextdoes have a web implementation.either way, the plan is to replace safe area context library with implementation from
react-native-screens(API and details aren't finalized). in this case we'd also likely own the web implementation.