Skip to content

refactor: migrate AppContainer root to React Navigation static config#7416

Open
diegolmello wants to merge 2 commits into
native-1303-inside-stack-static-configfrom
native-1305-appcontainer-static-config
Open

refactor: migrate AppContainer root to React Navigation static config#7416
diegolmello wants to merge 2 commits into
native-1303-inside-stack-static-configfrom
native-1305-appcontainer-static-config

Conversation

@diegolmello

@diegolmello diegolmello commented Jun 19, 2026

Copy link
Copy Markdown
Member

Proposed changes

Converts the app root from a dynamic NavigationContainer + Stack.Navigator to a fully static React Navigation 7 config.

Root navigatorcreateNativeStackNavigator({ groups }) with six conditional groups, each gated by a useSelector hook that reads state.app.root (and state.app.isMasterDetail for the master-detail split). createStaticNavigation converts the config into AppNavigation, a drop-in replacement for NavigationContainer that accepts ref, onReady, onStateChange, and theme.

Leaf stack bridges removed — all four .getComponent() calls are gone. Each leaf stack now exports its navigator config object directly so the root can nest it as a static screen:

  • OutsideStack.tsx
  • InsideStack.tsx
  • ShareExtensionStack.tsx
  • MasterDetailStack/index.tsx

SetUsernameStack — the inline dynamic navigator is folded into the root static config as a single-screen nested navigator.

ThemenavigationBarColor (theme-dependent) moves into a .with() wrapper on the root; headerShown: false and animation: 'none' stay in the static config. navigationTheme(theme) is passed to AppNavigation's theme prop unchanged.

Preserved unchanged: screen tracking (setCurrentScreen / routeNameRef via onReady / onStateChange and a useEffect on root changes), MediaCallHeader placement, emitter.emit('navigationReady'), and the imperative deep-linking path through navigationRef.

This is the final slice of the React Navigation 7 static-config migration. Stacked on #7415; base retargets to develop once #7415 merges.

Issue(s)

https://rocketchat.atlassian.net/browse/NATIVE-1305

How to test or reproduce

  1. Build and launch the app (iOS or Android).
  2. Verify the login flow: loading screen → outside (login/register) → inside (rooms list).
  3. On a tablet or with master-detail enabled, verify the split-view layout loads correctly.
  4. Verify the share extension launches without a crash.
  5. Verify deep links (e.g. rocketchat:// URLs) still navigate to the correct screen.
  6. Verify screen analytics tracking fires on navigation (check logs for setCurrentScreen).

Screenshots

N/A — navigation structure change only; no visual changes.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

No test files exist for AppContainer or app/stacks (confirmed via pnpm test --testPathPattern='AppContainer' and --testPathPattern='app/stacks'). Lint: 0 errors, 175 warnings (matches the pre-change baseline).

https://claude.ai/code/session_01Qh9P8Bbp6V7PFpCY2X5on3

Summary by CodeRabbit

  • Refactor
    • Updated the app’s root navigation to use a static navigation setup with hook-driven state, removing the previous Redux-connected root switching.
    • Simplified stack exports by directly exporting the underlying navigators/modals to streamline how navigation stacks are instantiated.
  • Bug Fixes
    • Improved route change tracking so screen analytics/state updates continue to follow the currently active route reliably.

Replace the dynamic NavigationContainer + Stack.Navigator with a fully
static root navigator built with createNativeStackNavigator + groups.
Each RootEnum branch is a conditional group gated by a useSelector hook
(useIsLoading, useIsOutside, useIsMasterDetail, useIsInside,
useIsSetUsername, useIsShareExtension). createStaticNavigation converts
the config into the AppNavigation component that wraps NavigationContainer
and accepts ref, onReady, onStateChange, and theme.

The four leaf stacks no longer export a .getComponent() bridge component;
they export their navigator config objects directly so they can be nested
as screens inside the root groups. The inline SetUsernameStack is folded
into the static config as a single-screen navigator.

Theme-dependent screenOptions (navigationBarColor) move into a .with()
wrapper on the root navigator; static options (headerShown: false,
animation: 'none') stay in the config object. Screen tracking
(setCurrentScreen / routeNameRef) and MediaCallHeader placement are
preserved unchanged.

Claude-Session: https://claude.ai/code/session_01Qh9P8Bbp6V7PFpCY2X5on3
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a560232-f0f1-4d97-8325-25aac3f86415

📥 Commits

Reviewing files that changed from the base of the PR and between 4717a69 and bdc3289.

📒 Files selected for processing (1)
  • app/AppContainer.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/AppContainer.tsx
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test

Walkthrough

Migrates AppContainer from a Redux-connected NavigationContainer with manual stack switching to a hook-driven static navigation approach using createStaticNavigation. All stack files (InsideStack, MasterDetailStack, OutsideStack, ShareExtensionStack) drop .getComponent() and now export navigator objects directly.

Changes

Static Navigation Migration

Layer / File(s) Summary
Stack navigator objects exported directly
app/stacks/InsideStack.tsx, app/stacks/MasterDetailStack/index.tsx, app/stacks/OutsideStack.tsx, app/stacks/ShareExtensionStack.tsx
Each stack removes the intermediate .getComponent() call and its associated constant, now exporting the navigator object itself as the default export.
AppContainer refactored to static navigation with hook-based groups
app/AppContainer.tsx
Removes connect/mapStateToProps and NavigationContainer. Adds useSelector-based condition hooks for loading, outside, master-detail, inside, set-username, and share-extension states. Defines RootNavigator with conditional groups, creates AppNavigation via createStaticNavigation, and renders it with the navigation theme while always rendering MediaCallHeader. Navigation events remain wired through onReady and onStateChange.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • RocketChat/Rocket.Chat.ReactNative#7410: Directly related — migrates OutsideStack to React Navigation static configuration, which is prerequisite work for the .getComponent() removal in this PR.

Suggested reviewers

  • OtavioStasiak
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'refactor: migrate AppContainer root to React Navigation static config' accurately describes the primary change—migrating from dynamic NavigationContainer setup to static React Navigation 7 configuration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • NATIVE-1305: Request failed with status code 401

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
app/AppContainer.tsx (1)

24-37: Add type annotation to selector hooks using IApplicationState.

All conditional-group hooks use state: any, which bypasses TypeScript's type safety. The codebase defines IApplicationState in app/definitions/redux/index.ts and uses it consistently throughout. Replace any with IApplicationState to gain type safety:

Example for useIsLoading
+import { type IApplicationState } from './definitions';
+
 const useIsLoading = () =>
 	useSelector(
-		(state: any) => state.app.root === RootEnum.ROOT_LOADING || state.app.root === RootEnum.ROOT_LOADING_SHARE_EXTENSION
+		(state: IApplicationState) => state.app.root === RootEnum.ROOT_LOADING || state.app.root === RootEnum.ROOT_LOADING_SHARE_EXTENSION
 	);

Apply the same change to the other five hooks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/AppContainer.tsx` around lines 24 - 37, Replace the `any` type annotation
with `IApplicationState` in all six selector hooks (useIsLoading, useIsOutside,
useIsMasterDetail, useIsInside, useIsSetUsername, and useIsShareExtension).
Update the state parameter in each useSelector call from `state: any` to `state:
IApplicationState` to enable proper TypeScript type safety. Ensure that
`IApplicationState` is imported from `app/definitions/redux/index.ts` at the top
of the file if it is not already imported.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/AppContainer.tsx`:
- Around line 24-37: Replace the `any` type annotation with `IApplicationState`
in all six selector hooks (useIsLoading, useIsOutside, useIsMasterDetail,
useIsInside, useIsSetUsername, and useIsShareExtension). Update the state
parameter in each useSelector call from `state: any` to `state:
IApplicationState` to enable proper TypeScript type safety. Ensure that
`IApplicationState` is imported from `app/definitions/redux/index.ts` at the top
of the file if it is not already imported.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc20e26a-b538-4cf0-bea0-8396e42c5a80

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd80b9 and 4717a69.

📒 Files selected for processing (5)
  • app/AppContainer.tsx
  • app/stacks/InsideStack.tsx
  • app/stacks/MasterDetailStack/index.tsx
  • app/stacks/OutsideStack.tsx
  • app/stacks/ShareExtensionStack.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/stacks/OutsideStack.tsx
  • app/stacks/InsideStack.tsx
  • app/stacks/ShareExtensionStack.tsx
  • app/stacks/MasterDetailStack/index.tsx
  • app/AppContainer.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript with strict mode enabled

Files:

  • app/stacks/OutsideStack.tsx
  • app/stacks/InsideStack.tsx
  • app/stacks/ShareExtensionStack.tsx
  • app/stacks/MasterDetailStack/index.tsx
  • app/AppContainer.tsx
**/*.{js,jsx,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier formatting with tabs, single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses

Files:

  • app/stacks/OutsideStack.tsx
  • app/stacks/InsideStack.tsx
  • app/stacks/ShareExtensionStack.tsx
  • app/stacks/MasterDetailStack/index.tsx
  • app/AppContainer.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce ESLint rules from @rocket.chat/eslint-config with React, React Native, TypeScript, and Jest plugins

Files:

  • app/stacks/OutsideStack.tsx
  • app/stacks/InsideStack.tsx
  • app/stacks/ShareExtensionStack.tsx
  • app/stacks/MasterDetailStack/index.tsx
  • app/AppContainer.tsx
app/stacks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Place navigation stacks in 'app/stacks/' directory (InsideStack for authenticated, OutsideStack for login/register, MasterDetailStack for tablets, ShareExtensionStack)

Files:

  • app/stacks/OutsideStack.tsx
  • app/stacks/InsideStack.tsx
  • app/stacks/ShareExtensionStack.tsx
  • app/stacks/MasterDetailStack/index.tsx
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/stacks/OutsideStack.tsx
  • app/stacks/InsideStack.tsx
  • app/stacks/ShareExtensionStack.tsx
  • app/stacks/MasterDetailStack/index.tsx
  • app/AppContainer.tsx
🔇 Additional comments (7)
app/stacks/InsideStack.tsx (1)

393-393: LGTM!

app/stacks/MasterDetailStack/index.tsx (1)

317-317: LGTM!

app/stacks/OutsideStack.tsx (1)

53-53: LGTM!

app/stacks/ShareExtensionStack.tsx (1)

44-44: LGTM!

app/AppContainer.tsx (3)

58-63: **Verify 'use memo' directive is supported.**The web search didn't show the 'use memo' directive being documented in React Navigation. Let me search for React Compiler directives specifically.The search confirms that 'use memo' is a valid React Compiler directive. "When a function contains 'use memo', the React Compiler will analyze and optimize it during build time. The compiler will automatically memoize values and components to prevent unnecessary re-computations and re-renders."

"To enable it for specific components, just add the 'use memo' directive inside the component body."

This is a valid React Compiler optimization directive. The usage here in the .with() callback is appropriate for opt-in compiler optimization.


41-57: LGTM!


69-101: LGTM!

@diegolmello diegolmello left a comment

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.

Review of the AppContainer static-config migration (diff against native-1303-inside-stack-static-config).

One blocking bug: cold-start crash. Every root group is if-gated, but the initial redux state is root: undefined (app/reducers/app.ts initialState). The migration removed the if (!root) return null; guard, so <AppNavigation> now mounts unconditionally on the very first render — before any saga sets root. With root undefined, all six if hooks return false, so the static config passes all-null children to the native-stack Navigator; React.Children.toArray strips the nulls, useNavigationBuilder ends up with zero routes and throws "Couldn't find any screens for the navigator." (@react-navigation/core useNavigationBuilder L392-396). AppContainer is rendered unconditionally in app/index.tsx, and appInit() (which triggers the restore saga that first sets root) is dispatched only after several awaits in Root.init(), so the undefined window is reachable at every cold start. The old dynamic root never mounted the container while !root. Must fix before merge — e.g. restore an early if (!root) return null; in AppContainer, or add a default (non-if) fallback group so at least one screen is always registered.

Everything else checks out:

  • Default-export type flip is safe: AppContainer is the ONLY importer of the default export of OutsideStack/InsideStack/MasterDetailStack/ShareExtensionStack. Other references are type-only imports (MasterDetailStack/types, ShareInsideStackParamList). No stray component consumer, no leftover .getComponent() anywhere.
  • Branch parity is exact and mutually exclusive: all 6 RootEnum values map 1:1 to the old conditions; the master-detail vs inside split is preserved. The only uncovered state is undefined (the bug above).
  • navigationRef / onReady / onStateChange / routeNameRef / theme={navigationTheme(theme)} all preserved on the createStaticNavigation Navigation component; shared navigationRef from appNavigation unchanged.
  • MediaCallHeader still a sibling outside the navigator.
  • Theme split is correct: static screenOptions ({headerShown, animation}) stay in the base config; theme-dependent navigationBarColor is read at render in the .with() wrapper and merged by the static NavigatorComponent (nothing frozen at module load).
  • No linking config added.
  • connect+memouseSelector is sound; memo removal is immaterial here (the component takes no props and already re-renders with its parent).
  • pnpm lint: 0 errors, 175 warnings — identical to baseline, no new warnings.

Comment thread app/AppContainer.tsx
<MediaCallHeader />
<NavigationContainer
theme={navTheme}
<AppNavigation

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.

Critical (crash on launch): <AppNavigation> now renders unconditionally, but the redux root is undefined on first render (app/reducers/app.ts initialState; appInit() runs after awaits in Root.init()). With root undefined all six group if hooks return false, the static config emits all-null children, and the native-stack navigator throws "Couldn't find any screens for the navigator" (@react-navigation/core useNavigationBuilder L392-396, after React.Children.toArray drops the nulls). The previous dynamic root guarded this exact window with if (!root) return null; (removed at L73-80 area). Restore that early-return, or add a default non-if fallback group so a screen is always registered when root is undefined.

Comment thread app/AppContainer.tsx
Inside: { if: useIsInside, screens: { InsideStack } },
SetUsername: { if: useIsSetUsername, screens: { SetUsernameStack } },
ShareExtension: { if: useIsShareExtension, screens: { ShareExtensionStack } }
}

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.

Root cause context for the cold-start crash: every group is if-gated, so the union of conditions does not cover root === undefined (the initial state). All 6 RootEnum values are covered and mutually exclusive — only the undefined/cold-start state falls through. A default group with no if (e.g. an AuthLoading/splash screen) would close the gap and also make the navigator resilient to any future unhandled root value.

Comment thread app/AppContainer.tsx
const createStackNavigator = createNativeStackNavigator;
// ─── Conditional-group hooks ──────────────────────────────────────────────────

const useIsLoading = () =>

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.

Nit (slop, moderate): (state: any) is repeated across all six selector hooks and the root selector. The old mapStateToProps also used any, so this isn't a regression, but typing these against the app state (e.g. IApplicationState) would restore the type-checking the redux state.app.root/isMasterDetail reads lost in the migration.

When the app cold-starts, redux state.app.root is undefined until the
restore saga completes. With all six conditional group hooks returning
false the static navigator had no visible screens and threw a native-
stack error. Restore the early-return guard that was present in the
dynamic root.

Claude-Session: https://claude.ai/code/session_01Qh9P8Bbp6V7PFpCY2X5on3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant