refactor(example): align TabsContainer with StackContainer & retire TabsConfigProvider#3764
refactor(example): align TabsContainer with StackContainer & retire TabsConfigProvider#3764
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the example/test app's tab infrastructure by retiring the TabsConfigProvider/TabsAutoconfig pattern and migrating test files to use direct container APIs (TabsContainer, TabsContainerWithHostConfigContext) with React context hooks (useTabsHostConfig, useTabsNavigationContext). It also moves safeAreaConfiguration into TabRouteOptions and simplifies tab route key generation.
Changes:
- Moved
safeAreaConfigurationfrom top-levelTabRouteConfigintoTabRouteOptions, stripping it before spreading onto<Tabs.Screen>. AddedTabsHostConfigtype andTabsContainerWithHostConfigContextwrapper withuseTabsHostConfighook. - Changed tab
routeKeyto use the routenamedirectly (since uniqueness is enforced), removing thegenerateIDdependency. - Migrated 8 test files from the old
TabsConfigProvider/dispatch pattern to the new direct context APIs, and adopted React 19 context syntax.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
TabsContainer.types.tsx |
Moved safeAreaConfiguration into TabRouteOptions; added TabsHostConfig type |
TabsContainer.tsx |
Strips safeAreaConfiguration before native spread; adopts React 19 context syntax; refactored getContent signature |
TabsContainerWithHostConfigContext.tsx |
New wrapper providing host config context to child screens |
contexts/TabsHostConfigContext.tsx |
New context for host config state |
hooks/useTabsHostConfig.tsx |
New hook to consume host config context |
presets.ts |
New DEFAULT_TAB_ROUTE_OPTIONS preset with default icons |
index.ts |
Re-exports new types, hooks, and components |
reducer.tsx |
Uses name as routeKey instead of generated ID |
TabsContainerWithDynamicRouteConfigs.tsx |
Moved init note to JSDoc |
StackContainerWithDynamicRouteConfigs.tsx |
Moved init note to JSDoc |
test-tabs-layout-direction.tsx |
Migrated to new APIs |
test-tabs-ime-insets.tsx |
Migrated to new APIs |
test-tabs-color-scheme.tsx |
Migrated to new APIs |
tabs-screen-orientation.tsx |
Migrated to new APIs |
tab-bar-hidden.tsx |
Migrated to new APIs |
bottom-accessory-layout.tsx |
Migrated to new APIs |
TestSafeAreaViewIOS/tabs/TabsComponent.tsx |
Moved safeAreaConfiguration inside options |
TestBottomTabs/index.tsx |
Moved safeAreaConfiguration inside options |
orientation-tabs-in-stack.tsx |
Migrated to new APIs |
orientation-stack-in-tabs.tsx |
Migrated to new APIs |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
This PR aligns TabsContainer with the existing StackContainer abstractions by retiring the TabsConfigProvider/TabsAutoconfig test infrastructure in favor of direct container APIs (useTabsNavigationContext, useTabsHostConfig). It also moves safeAreaConfiguration into TabRouteOptions for runtime updateability and simplifies route key generation.
Changes:
- Moved
safeAreaConfigurationfrom top-levelTabRouteConfigintoTabRouteOptions, stripping it before passing to native<Tabs.Screen>. - Added
TabsContainerWithHostConfigContext,useTabsHostConfighook,TabsHostConfigtype,DEFAULT_TAB_ROUTE_OPTIONSpreset, and adeepMergeutility, usingnameasrouteKeyfor tabs. - Migrated 8 test files from
TabsConfigProvider/TabsAutoconfigdispatches to the new direct context APIs.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
apps/src/shared/utils/deep-merge.ts |
New recursive deep-merge utility for host config updates |
apps/src/shared/gamma/containers/tabs/TabsContainer.types.tsx |
Moved safeAreaConfiguration into TabRouteOptions; added TabsHostConfig type |
apps/src/shared/gamma/containers/tabs/TabsContainer.tsx |
Strip safeAreaConfiguration from native props; adopt React 19 context syntax |
apps/src/shared/gamma/containers/tabs/TabsContainerWithHostConfigContext.tsx |
New wrapper component providing host config context |
apps/src/shared/gamma/containers/tabs/contexts/TabsHostConfigContext.tsx |
New context for host config state |
apps/src/shared/gamma/containers/tabs/hooks/useTabsHostConfig.tsx |
New hook to consume host config context |
apps/src/shared/gamma/containers/tabs/presets.ts |
New DEFAULT_TAB_ROUTE_OPTIONS with default icons |
apps/src/shared/gamma/containers/tabs/reducer.tsx |
Use name as routeKey instead of generated ID |
apps/src/shared/gamma/containers/tabs/index.ts |
Export new types, components, and hooks |
apps/src/shared/gamma/containers/tabs/TabsContainerWithDynamicRouteConfigs.tsx |
Moved init note to JSDoc |
apps/src/shared/gamma/containers/stack/StackContainerWithDynamicRouteConfigs.tsx |
Moved init note to JSDoc |
apps/src/tests/single-feature-tests/tabs/test-tabs-layout-direction.tsx |
Migrated to new APIs |
apps/src/tests/single-feature-tests/tabs/test-tabs-ime-insets.tsx |
Migrated to new APIs |
apps/src/tests/single-feature-tests/tabs/test-tabs-color-scheme.tsx |
Migrated to new APIs |
apps/src/tests/single-feature-tests/tabs/tabs-screen-orientation.tsx |
Migrated to new APIs |
apps/src/tests/single-feature-tests/tabs/tab-bar-hidden.tsx |
Migrated to new APIs |
apps/src/tests/single-feature-tests/tabs/bottom-accessory-layout.tsx |
Migrated to new APIs |
apps/src/tests/issue-tests/TestSafeAreaViewIOS/tabs/TabsComponent.tsx |
Moved safeAreaConfiguration into options |
apps/src/tests/issue-tests/TestBottomTabs/index.tsx |
Moved safeAreaConfiguration into options |
apps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsx |
Migrated to new APIs |
apps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsx |
Migrated to new APIs |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…mplify routeKey - Move `safeAreaConfiguration` from top-level `TabRouteConfig` into `TabRouteOptions` so that it is part of the per-route options object and can be updated at runtime via `setRouteOptions`; strip it before spreading onto `<Tabs.Screen>` as it is a JS-only wrapping concern - Use `name` as `routeKey` in `createTabRouteFromConfig` instead of a generated ID — tab names are enforced to be unique, so no separate identifier is needed, unlike Stack where the same route can appear multiple times; remove `generateID` import from tabs reducer - Update all consumer files accordingly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rate 3 tests
Add TabsHostConfig type, TabsHostConfigContext, useTabsHostConfig hook, and
TabsContainerWithHostConfigContext component to give tab screens a clean API
for updating host-level props (colorScheme, direction, android.*, etc.) at
runtime without manually threading state through parent components.
TabsContainerWithHostConfigContext initialises its hostConfig state from props,
exposes it via context, and re-passes the merged config down to TabsContainer —
keeping TabsContainer itself free of context management concerns.
Also introduce DEFAULT_TAB_ROUTE_OPTIONS preset to centralise the shared icon
config that every test tab needs, and adopt React 19 context syntax
(<Context value={...}> instead of <Context.Provider value={...}>).
Migrate test-tabs-color-scheme, test-tabs-ime-insets, and
test-tabs-layout-direction off the TabsConfigProvider/TabsAutoconfig
infrastructure to use TabsContainerWithHostConfigContext + useTabsHostConfig
+ useTabsNavigationContext directly, removing a layer of indirection and
making each test self-contained.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…absAutoconfig Migrate tab-bar-hidden, tabs-screen-orientation, bottom-accessory-layout, orientation-stack-in-tabs, and orientation-tabs-in-stack to use the new direct container APIs, eliminating all remaining usage of the TabsConfigProvider/TabsAutoconfig infrastructure in test files. - tabBar dispatches are replaced by useTabsHostConfig + updateHostConfig (tab-bar-hidden, bottom-accessory-layout) - tabScreen dispatches on the current screen are replaced by useTabsNavigationContext + setRouteOptions (tabs-screen-orientation) - The two orientation integration tests combine useTabsNavigationContext and useStackNavigationContext directly in ConfigScreen, removing the need for config state hooks and name-based lookups (findTabScreenOptions, findStackScreenOptions) entirely — React context propagation through nested containers makes the route keys and options available naturally Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ory-layout Add updateHostConfig to the dependency array of the useEffect in bottom-accessory-layout ConfigScreen and remove the eslint-disable comment that was suppressing the exhaustive-deps warning. updateHostConfig is stable (wrapped in useCallback with an empty dep array), so adding it to deps has no runtime cost and keeps the code correct by the rules of hooks without needing a suppression comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…components Move the inline comments about one-time prop initialisation into JSDoc blocks on TabsContainerWithHostConfigContext, TabsContainerWithDynamicRouteConfigs, and StackContainerWithDynamicRouteConfigs. JSDoc is more discoverable in IDEs and keeps the note at the component boundary where it is most useful, rather than buried inside the function body. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ContainerWithHostConfigContext Move the inline deepMerge helper to apps/src/shared/utils/deep-merge.ts so it can be reused across the example app. Switch updateHostConfig from a shallow spread to deepMerge so nested objects (e.g. android.*) are merged correctly rather than replaced wholesale. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1c3373b to
b8c7a11
Compare
React elements produced by JSX (e.g. <Component />) are plain objects with a $$typeof Symbol and type/props/key fields. The previous guard (non-null, typeof === 'object', !Array.isArray) would enter the recursive merge branch when both base and override held a React element at the same key, producing a corrupt hybrid object instead of replacing the element. Current call-sites pass component references (typeof 'function') for fields like bottomAccessory, which correctly fell into the else/replace branch — but the code was fragile: passing a JSX element instead of a component reference would silently produce broken output. Fix by adding React.isValidElement checks for both overrideVal and baseVal before entering the recursive branch. If either value is a React element the function now falls through to the else branch and replaces the value wholesale, which is the correct behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors the Tabs “gamma” container API to better match the Stack container patterns, removing the older TabsConfigProvider/TabsAutoconfig test infrastructure and enabling direct, context-based runtime updates for host props and per-route options.
Changes:
- Move
safeAreaConfigurationinto per-routeTabRouteOptions(JS-only), strip it before spreading native options, and apply it via a SafeAreaView wrapper. - Make tab
routeKeystable by using the (uniquely enforced) routenameinstead of a generated ID. - Introduce host-config context (
TabsHostConfigContext),TabsContainerWithHostConfigContext,useTabsHostConfig, andDEFAULT_TAB_ROUTE_OPTIONS, and migrate affected tests to the new APIs.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| apps/src/tests/single-feature-tests/tabs/test-tabs-layout-direction.tsx | Migrates test to TabsContainerWithHostConfigContext + useTabsHostConfig and new route config shape. |
| apps/src/tests/single-feature-tests/tabs/test-tabs-ime-insets.tsx | Migrates to useTabsHostConfig for host props and useTabsNavigationContext for per-route options. |
| apps/src/tests/single-feature-tests/tabs/test-tabs-color-scheme.tsx | Migrates host prop updates to useTabsHostConfig + uses shared default route options. |
| apps/src/tests/single-feature-tests/tabs/tabs-screen-orientation.tsx | Replaces name-based lookup/dispatch with useTabsNavigationContext + setRouteOptions. |
| apps/src/tests/single-feature-tests/tabs/tab-bar-hidden.tsx | Switches tab bar hidden toggling to useTabsHostConfig. |
| apps/src/tests/single-feature-tests/tabs/bottom-accessory-layout.tsx | Replaces dispatch-based bottom accessory updates with useTabsHostConfig. |
| apps/src/tests/issue-tests/TestSafeAreaViewIOS/tabs/TabsComponent.tsx | Updates safe-area config placement into options.safeAreaConfiguration. |
| apps/src/tests/issue-tests/TestBottomTabs/index.tsx | Updates safe-area config placement into options.safeAreaConfiguration. |
| apps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsx | Migrates orientation integration test to stack/tabs navigation contexts. |
| apps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsx | Migrates orientation integration test to stack/tabs navigation contexts. |
| apps/src/shared/utils/deep-merge.ts | Adds deep-merge helper used for incremental host-config updates. |
| apps/src/shared/gamma/containers/tabs/reducer.tsx | Uses name as routeKey (stable, unique) instead of generated IDs. |
| apps/src/shared/gamma/containers/tabs/presets.ts | Adds DEFAULT_TAB_ROUTE_OPTIONS preset for consistent test configs. |
| apps/src/shared/gamma/containers/tabs/index.ts | Re-exports new host-config API surface and presets. |
| apps/src/shared/gamma/containers/tabs/hooks/useTabsHostConfig.tsx | Adds hook for consuming/updating host config via context. |
| apps/src/shared/gamma/containers/tabs/contexts/TabsHostConfigContext.tsx | Introduces context type + instance for host-config updates. |
| apps/src/shared/gamma/containers/tabs/TabsContainerWithHostConfigContext.tsx | New wrapper that owns host config state and exposes updateHostConfig. |
| apps/src/shared/gamma/containers/tabs/TabsContainerWithDynamicRouteConfigs.tsx | Moves one-time-init note into JSDoc for consistency with wrappers. |
| apps/src/shared/gamma/containers/tabs/TabsContainer.types.tsx | Moves safeAreaConfiguration into TabRouteOptions and adds TabsHostConfig type. |
| apps/src/shared/gamma/containers/tabs/TabsContainer.tsx | Strips JS-only safe-area config from native props and applies it via wrapper content. |
| apps/src/shared/gamma/containers/stack/StackContainerWithDynamicRouteConfigs.tsx | Moves one-time-init note into JSDoc (parity with tabs wrapper docs). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { | ||
| routeConfigs, | ||
| initialFocusedName, | ||
| experimentalControlNavigationStateInJS, | ||
| ...hostProps | ||
| } = props; | ||
|
|
||
| const [hostConfig, setHostConfig] = React.useState<TabsHostConfig>(hostProps); | ||
|
|
There was a problem hiding this comment.
TabsContainerWithHostConfigContext initializes hostConfig state from ...hostProps, which still includes non-config host props like onNativeFocusChange (TabsHostConfig intentionally omits it). This means callbacks can be frozen in state (won’t reflect prop changes) and also become part of the object that updateHostConfig deep-merges. Consider destructuring onNativeFocusChange out separately (and passing it through directly to TabsContainer) so only the intended host configuration props are owned/merged by this wrapper.
There was a problem hiding this comment.
kligarski
left a comment
There was a problem hiding this comment.
I haven't checked the runtime but it looks good.
| * Whether to control navigation state in JS. | ||
| * Passed to Tabs.Host as experimentalControlNavigationStateInJS. | ||
| */ | ||
| experimentalControlNavigationStateInJS?: boolean; |
There was a problem hiding this comment.
This is already in TabsHostProps I think.
| const { | ||
| routeConfigs, | ||
| initialFocusedName, | ||
| experimentalControlNavigationStateInJS, | ||
| ...hostProps | ||
| } = props; | ||
|
|
||
| const [hostConfig, setHostConfig] = React.useState<TabsHostConfig>(hostProps); | ||
|
|
t0maboro
left a comment
There was a problem hiding this comment.
leaving some comments, non-blocking
…cture (#3765) ## Description Removes the `TabsConfigProvider` / `TabsAutoconfig` test infrastructure now that all test files have been migrated to the direct container APIs introduced in #3764. The old infrastructure wrapped every test in a shared `useReducer`, required a typed param list, and forced callers to dispatch named `tabBar` / `tabScreen` actions to mutate state. Every test file carried this boilerplate even though the actual logic it was testing had nothing to do with the dispatch pattern. Now that `useTabsNavigationContext` and `useTabsHostConfig` cover all the same use-cases directly, the infra has no remaining consumers and can be deleted. ## Changes - Delete `TabsAutoconfig`, `TabsConfigProvider`, `tabs-config` context, hooks, and types (6 files, ~216 lines removed). - Delete `createAutoConfiguredTabs` and `findTabScreenOptions` helpers from `tabs.tsx`. ## Test plan Verified no remaining imports of the deleted files across `apps/src` before deletion (`grep` returned empty). All tests that previously used this infrastructure were migrated in #3764 and continue to work correctly. ## Checklist - [x] Included code example that can be used to test this change. - [ ] Updated / created local changelog entries in relevant test files. - [ ] For visual changes, included screenshots / GIFs / recordings documenting the change. - [ ] For API changes, updated relevant public types. - [ ] Ensured that CI passes Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Description
Further aligns
TabsContainerwith the existingStackContainerabstractionsand retires the
TabsConfigProvider/TabsAutoconfigtest infrastructure infavour of direct container APIs.
The old infrastructure required tests to define a typed param list, wrap their
app in a
<Tabs.Provider>, and dispatch named actions through a shared reducerto change tab bar or per-screen options. This was indirect, required knowing
route names at dispatch time, and leaked the infra shape into every test.
The new APIs (
useTabsNavigationContext,useTabsHostConfig) let each screenupdate its own state directly via React context — no param lists, no dispatch,
no shared reducer.
Changes
safeAreaConfigurationfrom top-levelTabRouteConfigintoTabRouteOptionsso it is part of the per-route options object and can beupdated at runtime via
setRouteOptions; strip it before spreading onto<Tabs.Screen>as it is a JS-only wrapping concern.nameasrouteKeyincreateTabRouteFromConfig— tab names areenforced to be unique, so no separate generated ID is needed.
TabsHostConfigtype andTabsContainerWithHostConfigContextcomponent:initialises host config state from props, provides it via
TabsHostConfigContext, and re-passes the merged config toTabsContainer.Keeps
TabsContaineritself free of context management concerns.useTabsHostConfighook andDEFAULT_TAB_ROUTE_OPTIONSpreset.<Context value={...}>) inTabsContainer.TabsConfigProvider/TabsAutoconfig:tabBardispatches →useTabsHostConfig+updateHostConfigtabScreendispatches →useTabsNavigationContext+setRouteOptionsremoving name-based lookups (
findTabScreenOptions,findStackScreenOptions).With*wrapper components.Test plan
Tested manually with the FabricExample app on Android and iOS:
apps/src/tests/single-feature-tests/tabs/test-tabs-color-scheme.tsxapps/src/tests/single-feature-tests/tabs/test-tabs-ime-insets.tsxapps/src/tests/single-feature-tests/tabs/test-tabs-layout-direction.tsxapps/src/tests/single-feature-tests/tabs/tab-bar-hidden.tsxapps/src/tests/single-feature-tests/tabs/tabs-screen-orientation.tsxapps/src/tests/single-feature-tests/tabs/bottom-accessory-layout.tsxapps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsxapps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsxChecklist