diff --git a/.agents/skills/migrate-to-static-config/SKILL.md b/.agents/skills/migrate-to-static-config/SKILL.md new file mode 100644 index 000000000000..19a950fe24ae --- /dev/null +++ b/.agents/skills/migrate-to-static-config/SKILL.md @@ -0,0 +1,13 @@ +--- +name: migrate-to-static-config +description: Migrate React Navigation navigators from dynamic component based config to static object based config. +--- + +# Migrating to Static Config + +Check `@react-navigation/native` in `package.json` first. + +- If `7.x`, read `references/react-navigation-7.md` +- If `8.x`, read `references/react-navigation-8.md` + +Do not load both unless explicitly comparing versions. diff --git a/.agents/skills/migrate-to-static-config/agents/openai.yaml b/.agents/skills/migrate-to-static-config/agents/openai.yaml new file mode 100644 index 000000000000..523c537cc2d0 --- /dev/null +++ b/.agents/skills/migrate-to-static-config/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Migrate React Navigation to Static Config" + short_description: "Convert JSX navigators to static config" + default_prompt: "Use $migrate-to-static-config to migrate React Navigation navigators from dynamic JSX to static config." diff --git a/.agents/skills/migrate-to-static-config/references/react-navigation-7.md b/.agents/skills/migrate-to-static-config/references/react-navigation-7.md new file mode 100644 index 000000000000..60c90e14020d --- /dev/null +++ b/.agents/skills/migrate-to-static-config/references/react-navigation-7.md @@ -0,0 +1,809 @@ +# React Navigation 7.x Static Config Migration + +Use this file only when `@react-navigation/native` is on `7.x`. + +## Goal + +Convert React Navigation navigators from JSX-based dynamic setup to static configuration while preserving behavior, typing, and deep links. + +## When + +1. You are migrating screens to the static API in React Navigation 7.x. +2. The navigator's screen list is static and not built at runtime. +3. The navigator doesn't use dynamic variables or props that are not available in static config. + +## Prerequisites + +- The project is using React Navigation 7.x. +- The versions of `@react-navigation` packages are up-to-date with the published versions. + +## Structure + +1. Create a static navigator with `createXNavigator({ screens, groups, ... })`. +2. Each `screens` entry can be a component, a nested static navigator, or a screen config object. +3. `groups` define shared options and conditional rendering using `if`, and contain their own `screens`. +4. Screen config objects accept the same options as the dynamic `Screen` API, plus static-only additions such as `linking` and `if`. +5. When a screen needs a config object, use a plain screen config object. +6. A screen config `linking` can be a string path or an object with `path`, `parse`, `stringify`, and `exact`. + +## Workflow + +### 1. Identify static candidates + +A navigator is a static candidate if all its screens are known at build time. Look for: + +- **Convertible**: fixed `` elements, conditional rendering based on auth or feature flags (use `if` hooks), render callbacks passing extra props (use React context), navigators wrapped in providers or components using hooks for navigator-level props (use `.with()`) +- **Not convertible**: screen list built from runtime data such as mapping over an API response, screens added or removed based on values that can't be expressed as a hook returning a boolean. + +### 2. Convert navigator JSX to static config + +Convert the existing navigator first, then introduce screen config objects only where a screen needs options, listeners, params, IDs, linking, or `if`. + +Before: + +```tsx +const Stack = createNativeStackNavigator(); + +function MyStack() { + return ( + + + + + ); +} +``` + +After: + +```tsx +const MyStack = createNativeStackNavigator({ + screenOptions: { headerShown: false }, + screens: { + Home: HomeScreen, + Profile: { + screen: ProfileScreen, + options: { title: 'My Profile' }, + }, + }, +}); +``` + +Full screen config shape: + +```tsx +const MyStack = createNativeStackNavigator({ + screens: { + Example: { + screen: ScreenComponent, + options: ({ route, navigation, theme }) => ({ + title: route.name, + }), + listeners: ({ route, navigation }) => ({ + focus: () => {}, + }), + initialParams: {}, + getId: ({ params }) => params.id, + linking: { + path: 'pattern/:id', + parse: { id: Number }, + stringify: { id: (value) => String(value) }, + exact: true, + }, + if: useConditionHook, + layout: ({ children }) => children, + }, + }, +}); +``` + +Shorthand (component only, no config): `ScreenName: ScreenComponent` + +Nested static navigator: `ScreenName: AnotherStaticNavigator` + +### 3. Convert nested navigators + +Nested dynamic navigators rendered as components become nested config objects. + +Before: + +```tsx +const Tab = createBottomTabNavigator(); + +function HomeTabs() { + return ( + + + + + ); +} + +function RootStack() { + return ( + + + + ); +} +``` + +After: + +```tsx +const HomeTabs = createBottomTabNavigator({ + screens: { + Groups: GroupsScreen, + Chats: ChatsScreen, + }, +}); + +const RootStack = createNativeStackNavigator({ + screens: { + Home: HomeTabs, + }, +}); +``` + +### 4. Convert groups + +Before: + +```tsx +function RootStack() { + return ( + + + + + + + + + + ); +} +``` + +After: + +```tsx +const RootStack = createNativeStackNavigator({ + groups: { + Card: { + screenOptions: { headerStyle: { backgroundColor: 'red' } }, + screens: { + Home: HomeScreen, + Profile: ProfileScreen, + }, + }, + Modal: { + screenOptions: { presentation: 'modal' }, + screens: { + Settings: SettingsScreen, + }, + }, + }, +}); +``` + +Top-level `screens` and `screenOptions` handle the default group. + +Use `groups` when you need different shared options, conditional groups, grouped linking, or to logically group screens if the dynamic config already had such groups. + +### 5. Convert auth flows + +To migrate conditional screens from dynamic config, use static `if` hooks. The `if` property takes a user-defined hook that returns a boolean such as `useIsSignedIn` or `useIsSignedOut`. + +This prevents navigating to protected screens when signed out and unmounts auth screens after sign-in, so the back button cannot return to them. + +If you previously used `navigationKey` to reset a screen when auth state changes, duplicate the screen in both auth groups. The group name is used for the key, so switching groups resets the screen. For example, declare `Help` in both the signed-in and signed-out groups instead of using `navigationKey`. + +Loading UI should live outside the navigation tree, meaning outside `` / ``, not in a `Loading` screen or group. Keep `screens` and `groups` for actual navigable routes only. + +Use `.with()` for wrappers around a mounted navigator, not for boot or loading gates that should happen before rendering ``. + +```tsx +const App = () => { + const isLoading = useIsLoading(); + + if (isLoading) { + return ; + } + + return ; +}; +``` + +Before: + +```tsx +function App() { + const isSignedIn = useIsSignedIn(); + + return ( + + {isSignedIn ? ( + <> + + + + ) : ( + <> + + + + )} + + + ); +} +``` + +After: + +```tsx +const RootStack = createNativeStackNavigator({ + groups: { + SignedIn: { + if: useIsSignedIn, + screens: { + Home: HomeScreen, + Profile: ProfileScreen, + Help: HelpScreen, + }, + }, + SignedOut: { + if: useIsSignedOut, + screens: { + SignIn: SignInScreen, + SignUp: SignUpScreen, + Help: HelpScreen, + }, + }, + }, +}); +``` + +### 6. Use `.with()` for wrappers, providers, and dynamic navigator props + +If the dynamic navigator is rendered in a component that uses hooks for navigator-level behavior, or has wrappers around the mounted navigator, use `.with()` to provide this wrapper. This applies to navigator-level props such as `initialRouteName`, `backBehavior`, `screenOptions`, and `screenListeners` that are derived dynamically. + +#### Wrapping with a provider and dynamic options + +Before: + +```tsx +function MyStack() { + const someValue = useSomeHook(); + + return ( + + + + + + ); +} +``` + +After: + +```tsx +const MyStack = createNativeStackNavigator({ + screens: { + Home: HomeScreen, + }, +}).with(({ Navigator }) => { + const someValue = useSomeHook(); + + return ( + + + + ); +}); +``` + +#### Per-screen dynamic options via `screenOptions` callback + +If each screen has different options, use a `screenOptions` callback and switch on `route.name`. + +Before: + +```tsx +function MyStack() { + const getSomething = useSomeHook(); + + return ( + + + + + + + ); +} +``` + +After: + +```tsx +const MyStack = createNativeStackNavigator({ + screens: { + Home: HomeScreen, + Profile: ProfileScreen, + }, +}).with(({ Navigator }) => { + const getSomething = useSomeHook(); + + return ( + + { + switch (route.name) { + case 'Home': + return { + title: getSomething('First'), + }; + case 'Profile': + return { + title: getSomething('Second'), + }; + default: + return {}; + } + }} + /> + + ); +}); +``` + +#### Replacing render callbacks with context + +Static screens cannot receive extra props via render callbacks. Move the data to React context and provide it via `.with()`. + +Before: + +```tsx + + {(props) => } + +``` + +After: + +```tsx +const TokenContext = React.createContext(''); + +function ChatScreen() { + const token = React.useContext(TokenContext); + + return ; +} + +const MyStack = createNativeStackNavigator({ + screens: { + Chat: ChatScreen, + }, +}).with(({ Navigator }) => { + const token = useToken(); + + return ( + + + + ); +}); +``` + +### 7. Migrate screen-level linking + +Use screen-level `linking` to replace the old root `linking.config.screens` structure. + +Omit `linking` on a screen when the default kebab-case path is acceptable. If the path is identical to the auto path such as `Details` to `details`, remove the redundant `linking` entry. + +Add `linking` for custom paths or when you need path params with `parse` or `stringify`. + +Before: + +```tsx +const linking = { + prefixes: ['https://example.com'], + config: { + screens: { + Home: '', + Profile: { + path: 'user/:id', + parse: { id: Number }, + }, + Settings: 'settings', + }, + }, +}; +``` + +After: + +```tsx +const RootStack = createNativeStackNavigator({ + screens: { + Home: { + screen: HomeScreen, + linking: '', // explicit root path; omit if this is the first leaf screen or the initialRouteName + }, + Profile: { + screen: ProfileScreen, + linking: { + path: 'user/:id', + parse: { id: Number }, + }, + }, + Settings: SettingsScreen, + }, +}); +``` + +Linking paths are auto-generated for leaf screens using kebab-case of the screen name. The first leaf screen, or the `initialRouteName` if set, gets the path `/` unless you set an explicit empty path on another screen. + +To control auto-generated linking, pass `enabled` on the root `linking` prop: `enabled: 'auto'` generates paths for all leaf screens, and `enabled: false` disables linking entirely. + +If a screen previously had a custom path such as `linking: 'contacts'` and you remove it, the auto path becomes kebab-case of the screen name such as `TabContacts` to `tab-contacts`. This breaks existing URLs and deep links. Keep explicit `linking` when you need to preserve existing paths. + +If screens containing navigators have `linking` set to `''` or `'/'`, it is usually redundant and can be removed. + +Keep TypeScript param typing on the screen component with `StaticScreenProps`. Screen-level `linking` config is for URL parsing and serialization only. + +### 8. Update types + +#### Getting navigation and route access + +Use `StaticScreenProps` to type the screen's `route` prop. + +Use the default `useNavigation()` type provided through the global `ReactNavigation.RootParamList` augmentation for navigator-agnostic navigation calls. + +Prefer the screen `route` prop over `useRoute` when available. Use `useNavigationState` separately when you need navigation state. + +Before: + +```tsx +function ProfileScreen({ + navigation, + route, +}: NativeStackScreenProps) { + const id = route.params.id; + navigation.navigate('Home'); +} +``` + +After: + +```tsx +type ProfileScreenProps = StaticScreenProps<{ + id: string; +}>; + +function ProfileScreen({ route }: ProfileScreenProps) { + const navigation = useNavigation(); + const id = route.params.id; + + navigation.navigate('Home'); +} +``` + +Use `StaticScreenProps` to type the `route` prop. If you need navigator-specific APIs such as `push`, `pop`, `openDrawer`, or `setOptions`, you can manually annotate `useNavigation`, but this is not type-safe and should be kept to a minimum. + +If you need a navigator-specific navigation object: + +```tsx +type RootStackParamList = StaticParamList; + +type ProfileNavigationProp = NativeStackNavigationProp< + RootStackParamList, + 'Profile' +>; + +const navigation = useNavigation(); +``` + +#### Remove manual param lists + +Remove all hand-written param-list declarations created only to support dynamic typing. + +If a param list is absolutely necessary, derive it from the navigator type: + +```tsx +type SomeStackType = typeof SomeStack; +type SomeStackParamList = StaticParamList; +``` + +If a static navigator nests a dynamic navigator, annotate the dynamic navigator screen with `StaticScreenProps>` so the nesting is reflected in the root param list. + +For the root navigator, keep the single source of truth in the `RootParamList` augmentation shown below. + +Avoid circular dependencies by: + +- Using `StaticScreenProps` for screen params instead of shared hand-written param lists +- Using the default `useNavigation()` type where possible instead of navigator-specific aliases +- Deleting obsolete shared type files when they become empty + +#### Root type augmentation + +Place the global `RootParamList` augmentation next to the root static navigator. This is the single source of truth for the default types used by `useNavigation`, `Link`, refs, and related APIs. + +```tsx +const RootStack = createNativeStackNavigator({ + screens: { + // ... + }, +}); + +type RootStackParamList = StaticParamList; + +declare global { + namespace ReactNavigation { + interface RootParamList extends RootStackParamList {} + } +} +``` + +#### Typing params + +Use `StaticScreenProps` to annotate route params, including screens that use `linking`. + +A path such as `user/:userId` defines URL parsing and serialization. Keep the TypeScript param type on the screen component with `StaticScreenProps`. + +If the params are not strings, use `parse` and `stringify` in the `linking` config: + +```tsx +type ArticleScreenProps = StaticScreenProps<{ + date: Date; +}>; + +function ArticleScreen({ route }: ArticleScreenProps) { + return
; +} + +const RootStack = createNativeStackNavigator({ + screens: { + Article: { + screen: ArticleScreen, + linking: { + path: 'article/:date', + parse: { + date: (date: string) => new Date(date), + }, + stringify: { + date: (date: Date) => date.toISOString(), + }, + }, + }, + }, +}); +``` + +The runtime parsing comes from `linking`. The compile-time param type comes from `StaticScreenProps`. + +Avoid `any`, non-null assertions, and `as` assertions. + +#### Full before/after example + +Before: + +```tsx +type MyStackParamList = { + Article: { author: string }; + Albums: undefined; +}; + +const Stack = createNativeStackNavigator(); + +function ArticleScreen({ + navigation, + route, +}: NativeStackScreenProps) { + return