Skip to content

feat:migrate to standard navigation shared adapter - #581

Open
oleksandrzavarzin-callstack wants to merge 15 commits into
callstack:mainfrom
oleksandrzavarzin-callstack:feat/migrate-to-standard-navigation-shared-adapter
Open

oleksandrzavarzin-callstack wants to merge 15 commits into
callstack:mainfrom
oleksandrzavarzin-callstack:feat/migrate-to-standard-navigation-shared-adapter

Conversation

@oleksandrzavarzin-callstack

@oleksandrzavarzin-callstack oleksandrzavarzin-callstack commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

What

Expo SDK 56 dropped Expo Router's dependency on @react-navigation/* in favour of a vendored fork. @bottom-tabs/react-navigation imports the real thing, so on SDK 56+ two copies of React Navigation load and their contexts stop lining up.

This moves the tab view into a new framework-agnostic package, @bottom-tabs/standard-navigation, built on the standard-navigation contract. One implementation, two adapters:

Expo SDK Package Integration
56+ @bottom-tabs/standard-navigation unstable_createStandardRouterNavigator
52 to 55 @bottom-tabs/react-navigation withLayoutContext

@bottom-tabs/react-navigation's public API is unchanged - same exports, same types, same theme-derived tint defaults. It becomes a thin adapter over the shared view.

Notable

  • Dropped the color dependency for React Native's processColor. color is ESM-only, so every consumer who writes tests had to patch transformIgnorePatterns before Jest would run at all.
  • The peer on @react-navigation/native stays at >=7. An earlier revision raised it to >=7.3.0, which broke npm install outright for SDK 52-55 apps locked below that - and nothing on that path needs 7.3.
  • NativeBottomTabsContent carries its event map and navigator props on phantom type-only properties. Without them an integrator loses every navigator prop at the call site: tabBarActiveTintColor and friends become type errors.
  • unstable_createStandardRouterNavigator is marked unstable by Expo and may change between minors. We deliberately don't call it ourselves, so a break stays in app code.
  • On the Expo Router path a custom tabBar receives { state, descriptors, actions, emitter } rather than a navigation object, so @react-navigation/bottom-tabs' BottomTabBar can't be used there. The React Navigation path is unaffected.

How to test

yarn && yarn build
yarn test        # 96 tests across 5 workspaces
yarn lint && yarn typecheck

apps/expo-router-test runs one assertion suite against both adapters under a real Expo Router tree, with a console spy that fails on any warning or error.

Verified on an app scaffolded from create-expo-app --template default@sdk-57 (Expo 57, React Native 0.86.3, React 19.2.3):

  • iOS 26 and Android 17 - builds and bundles, Liquid Glass and Material 3 tab bars render with tint and badge, tapping routes between screens. No JS warnings or errors.
  • @react-navigation/* is absent from node_modules entirely, so there's no second copy to line up - everything resolves through Expo Router's vendored fork.

The SDK 52-55 path was checked separately: the documented withLayoutContext recipe typechecks clean against a real expo-router@55 install, at both 7.1.33 and 7.3.0.

Screenshots

iOS 26 Android 17
image image

@oleksandrzavarzin-callstack
oleksandrzavarzin-callstack marked this pull request as ready for review September 11, 2026 14:20
@@ -0,0 +1,12 @@
---
'@bottom-tabs/navigation': minor

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this new package @bottom-tabs/standard-navigation to keep consistency with React Navigation

Comment on lines +8 to +12
`NativeBottomTabsContent` carries its event map and navigator props on phantom properties, so an integrator can type `tabBarActiveTintColor`, `tabBar` and the rest. Neither is inferable from the contract on its own - the event map appears only as an argument to `emitter.emit`, and the navigator props only inside an `Omit<…>`.

`createNativeBottomTabNavigator` renders through the shared navigator, and keeps building its own state with `useNavigationBuilder`, so `state.key` and the per-route `navigation` and `route` objects a custom `tabBar` reads stay intact.

`@bottom-tabs/react-navigation` now requires `@react-navigation/native` 7.3.0 or newer, which is the version that introduced `createStandardNavigationFactories` used by the documented shared-navigator recipe

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this is unnecessary for the changeset

Match the naming of @bottom-tabs/react-navigation, and trim the
changeset down to the release-note facts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
<PackageManagerTabs command="install react-native-bottom-tabs" />

If you are going to use [React Navigation / Expo Router Integration](/docs/guides/usage-with-react-navigation) make sure to install `@bottom-tabs/react-navigation`.
If you are going to use the [React Navigation integration](/docs/guides/usage-with-react-navigation), also install `@bottom-tabs/react-navigation`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be here React Navigation or Expo SDK < 56? Or I'm missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's true.
I've added "Expo SDK 55+" note to it.

@@ -0,0 +1,116 @@
import { NavigationContainer } from '@react-navigation/native';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this file specifically, but the example app should be named example-expo-router to follow the convention of the other example apps

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a test harness rather than the example Expo router app (for that option, we will have expo-template, after it's updated). It just checks whether our two adapters, driven by a real Expo Router and a real React Navigation, pass the same correct props to the tab view.

| 55 and older | `@bottom-tabs/react-navigation` | `withLayoutContext` |

Next, create a custom layout adapter for the native bottom tabs using `withLayoutContext` from Expo Router. Put it in `components/bottom-tabs.tsx` and export the navigator, so your layouts can import it:
## Expo SDK 56 and newer

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to see in the diff, but did we loose the section for integrating with SDK < 56?

It's important to have it since we still support it with the old integration

@oleksandrzavarzin-callstack oleksandrzavarzin-callstack Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is that section:

## Expo SDK 52 to 55
Install `@bottom-tabs/react-navigation`, which provides a native bottom tab navigator for React Navigation.
<PackageManagerTabs command="install @bottom-tabs/react-navigation" />
Create a custom layout adapter using `withLayoutContext` from Expo Router. Put it in `components/bottom-tabs.tsx` and export the navigator, so your layouts can import it:
```tsx
import { withLayoutContext } from 'expo-router';
import {
createNativeBottomTabNavigator,
NativeBottomTabNavigationOptions,
NativeBottomTabNavigationEventMap,
} from '@bottom-tabs/react-navigation';
import { ParamListBase, TabNavigationState } from '@react-navigation/native';
const BottomTabNavigator = createNativeBottomTabNavigator().Navigator;
export const Tabs = withLayoutContext<
NativeBottomTabNavigationOptions,
typeof BottomTabNavigator,
TabNavigationState<ParamListBase>,
NativeBottomTabNavigationEventMap
>(BottomTabNavigator);
```
> [!NOTE] Make sure to provide the correct types for the `withLayoutContext` function. Without it screen options won't have proper types.
The layout itself is the same as above.
### Upgrading to SDK 56 or newer
On SDK 56+ this setup loads a second copy of React Navigation, whose contexts do not line up with the router's.
To upgrade:
1. Replace `@bottom-tabs/react-navigation` with `@bottom-tabs/standard-navigation`.
2. Swap `withLayoutContext` for `unstable_createStandardRouterNavigator` as shown above. The generic type arguments are no longer needed.
3. Uninstall the `@react-navigation/*` packages, and import from `expo-router/react-navigation` instead. That entry point re-exports Expo Router's own copy of React Navigation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants