Skip to content

refactor(example): align TabsContainer with StackContainer & retire TabsConfigProvider#3764

Merged
kkafar merged 10 commits intomainfrom
@kkafar/tabs-refactor-3-1
Mar 24, 2026
Merged

refactor(example): align TabsContainer with StackContainer & retire TabsConfigProvider#3764
kkafar merged 10 commits intomainfrom
@kkafar/tabs-refactor-3-1

Conversation

@kkafar
Copy link
Copy Markdown
Member

@kkafar kkafar commented Mar 15, 2026

Description

Further aligns TabsContainer with the existing StackContainer abstractions
and retires the TabsConfigProvider / TabsAutoconfig test infrastructure in
favour 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 reducer
to 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 screen
update its own state directly via React context — no param lists, no dispatch,
no shared reducer.

Changes

  • Move safeAreaConfiguration from top-level TabRouteConfig into
    TabRouteOptions so 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 — tab names are
    enforced to be unique, so no separate generated ID is needed.
  • Add TabsHostConfig type and TabsContainerWithHostConfigContext component:
    initialises host config state from props, provides it via
    TabsHostConfigContext, and re-passes the merged config to TabsContainer.
    Keeps TabsContainer itself free of context management concerns.
  • Add useTabsHostConfig hook and DEFAULT_TAB_ROUTE_OPTIONS preset.
  • Adopt React 19 context syntax (<Context value={...}>) in TabsContainer.
  • Migrate all 8 affected test files off TabsConfigProvider/TabsAutoconfig:
    • tabBar dispatches → useTabsHostConfig + updateHostConfig
    • tabScreen dispatches → useTabsNavigationContext + setRouteOptions
    • Orientation integration tests combine both context hooks directly,
      removing name-based lookups (findTabScreenOptions, findStackScreenOptions).
  • Move one-time-init notes into JSDoc on all three 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.tsx
  • apps/src/tests/single-feature-tests/tabs/test-tabs-ime-insets.tsx
  • apps/src/tests/single-feature-tests/tabs/test-tabs-layout-direction.tsx
  • apps/src/tests/single-feature-tests/tabs/tab-bar-hidden.tsx
  • apps/src/tests/single-feature-tests/tabs/tabs-screen-orientation.tsx
  • apps/src/tests/single-feature-tests/tabs/bottom-accessory-layout.tsx
  • apps/src/tests/component-integration-tests/orientation/orientation-stack-in-tabs.tsx
  • apps/src/tests/component-integration-tests/orientation/orientation-tabs-in-stack.tsx

Checklist

  • 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

@kkafar kkafar changed the title WIP WIP refactor(example): align TabsContainer with StackContainer & retire TabsConfigProvider Mar 15, 2026
@kkafar kkafar requested a review from Copilot March 15, 2026 00:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 safeAreaConfiguration from top-level TabRouteConfig into TabRouteOptions, stripping it before spreading onto <Tabs.Screen>. Added TabsHostConfig type and TabsContainerWithHostConfigContext wrapper with useTabsHostConfig hook.
  • Changed tab routeKey to use the route name directly (since uniqueness is enforced), removing the generateID dependency.
  • 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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 safeAreaConfiguration from top-level TabRouteConfig into TabRouteOptions, stripping it before passing to native <Tabs.Screen>.
  • Added TabsContainerWithHostConfigContext, useTabsHostConfig hook, TabsHostConfig type, DEFAULT_TAB_ROUTE_OPTIONS preset, and a deepMerge utility, using name as routeKey for tabs.
  • Migrated 8 test files from TabsConfigProvider/TabsAutoconfig dispatches 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.

Comment thread apps/src/shared/utils/deep-merge.ts Outdated
Base automatically changed from @kkafar/tabs-refactor-2 to main March 24, 2026 09:58
kkafar and others added 7 commits March 24, 2026 10:59
…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>
@kkafar kkafar force-pushed the @kkafar/tabs-refactor-3-1 branch from 1c3373b to b8c7a11 Compare March 24, 2026 10:00
kkafar and others added 2 commits March 24, 2026 11:05
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>
@kkafar kkafar marked this pull request as ready for review March 24, 2026 10:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 safeAreaConfiguration into per-route TabRouteOptions (JS-only), strip it before spreading native options, and apply it via a SafeAreaView wrapper.
  • Make tab routeKey stable by using the (uniquely enforced) route name instead of a generated ID.
  • Introduce host-config context (TabsHostConfigContext), TabsContainerWithHostConfigContext, useTabsHostConfig, and DEFAULT_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.

Comment on lines +20 to +28
const {
routeConfigs,
initialFocusedName,
experimentalControlNavigationStateInJS,
...hostProps
} = props;

const [hostConfig, setHostConfig] = React.useState<TabsHostConfig>(hostProps);

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sounds reasonable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

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

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is already in TabsHostProps I think.

Comment on lines +20 to +28
const {
routeConfigs,
initialFocusedName,
experimentalControlNavigationStateInJS,
...hostProps
} = props;

const [hostConfig, setHostConfig] = React.useState<TabsHostConfig>(hostProps);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sounds reasonable?

Copy link
Copy Markdown
Contributor

@t0maboro t0maboro left a comment

Choose a reason for hiding this comment

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

leaving some comments, non-blocking

Comment thread apps/src/shared/utils/deep-merge.ts
Comment thread apps/src/shared/utils/deep-merge.ts Outdated
@kkafar kkafar merged commit 66a5ac0 into main Mar 24, 2026
3 checks passed
@kkafar kkafar deleted the @kkafar/tabs-refactor-3-1 branch March 24, 2026 14:41
kkafar added a commit that referenced this pull request Mar 24, 2026
…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>
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.

4 participants