Skip to content
Agentmart
Submit a tool

navigation-and-routing

Builds and checks Expo Router routes and layouts against a contract: typed hrefs, params, protected flows, modals, back guards.

Skill by zakariaf

IndexedMIT★ 11
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/zakariaf-navigation-and-routing

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Builds and checks Expo Router routes and layouts against a contract: typed hrefs, params, protected flows, modals, back guards. Use when adding a route, link or guard, or Back acts up. Not for deep links (deep-links-and-universal-links).

Use cases: documents

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 26, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found
Runs as
Node.js package

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/navigation-and-routing

What's inside

Skill name
navigation-and-routing
Description
Builds and checks Expo Router routes and layouts against a contract: typed hrefs, params, protected flows, modals, back guards. Use when adding a route, link or guard, or Back acts up. Not for deep links (deep-links-and-universal-links).
License
MIT
Instructions
204 lines in SKILL.md · view on GitHub

Before installing

  • Ships executable scripts

Files (67)

  • SKILL.md
  • shared.json · assets/
  • placeholder-screen.tsx · examples/
  • debug-route.tsx · examples/react-navigation/
  • navigation-root.tsx · examples/react-navigation/
  • navigation.json · examples/react-navigation/
  • react-navigation.d.ts · examples/react-navigation/
  • root-stack.tsx · examples/react-navigation/
  • route-guards.ts · examples/react-navigation/
  • route-params.ts · examples/react-navigation/
  • use-task-editor-model.ts · examples/react-navigation/
  • contract-and-checker.md · references/
  • expo-router-in-practice.md · references/
  • react-navigation-static.md · references/
  • check-lib.mjs · scripts/
  • check-navigation.mjs · scripts/
  • object-literal.mjs · scripts/lib/
  • source-scan.mjs · scripts/lib/
  • selftest.mjs · scripts/
  • navigation.json · templates/contracts/
  • navigation.schema.json · templates/contracts/
  • +not-found.tsx · templates/src/app/
  • not-found-navigation.test.tsx · templates/src/features/not-found/
  • not-found-screen.test.tsx · templates/src/features/not-found/
  • not-found-screen.tsx · templates/src/features/not-found/
  • use-not-found-model.test.tsx · templates/src/features/not-found/
  • use-not-found-model.ts · templates/src/features/not-found/
  • protected-routes.test.tsx · templates/src/features/root-navigator/
  • task-detail-params.test.ts · templates/src/features/task-detail/
  • task-detail-params.ts · templates/src/features/task-detail/
  • use-discard-guard.android.test.tsx · templates/src/features/task-editor/
  • use-discard-guard.test.tsx · templates/src/features/task-editor/
  • use-discard-guard.ts · templates/src/features/task-editor/
  • ar.json · templates/src/i18n/catalogs/
  • en.json · templates/src/i18n/catalogs/
  • app.config.ts · tests/bases/app/
  • navigation.json · tests/bases/app/contracts/
  • package.json · tests/bases/app/
  • _layout.tsx · tests/bases/app/src/app/(app)/
  • compose.tsx · tests/bases/app/src/app/(app)/
  • index.tsx · tests/bases/app/src/app/(app)/
  • [taskId].tsx · tests/bases/app/src/app/(app)/tasks/
  • edit.tsx · tests/bases/app/src/app/(app)/tasks/[taskId]/
  • _layout.tsx · tests/bases/app/src/app/
  • debug.tsx · tests/bases/app/src/app/
  • sign-in.tsx · tests/bases/app/src/app/
  • test-build.ts · tests/bases/app/src/features/debug/
  • app-root.tsx · tests/bases/app/src/features/root-navigator/
  • app-stack.tsx · tests/bases/app/src/features/root-navigator/
  • root-stack.tsx · tests/bases/app/src/features/root-navigator/
  • use-task-editor-model.ts · tests/bases/app/src/features/task-editor/
  • use-tasks-model.ts · tests/bases/app/src/features/tasks/
  • session-selectors.ts · tests/bases/app/src/stores/session/
  • task-row.tsx · tests/bases/app/src/ui/
  • tsconfig.json · tests/bases/app/
  • package.json · tests/bases/react-navigation/
  • use-home-model.ts · tests/bases/react-navigation/src/features/home/
  • task-row.tsx · tests/bases/react-navigation/src/ui/
  • tsconfig.json · tests/bases/react-navigation/
  • CASES.json · tests/fixtures/expo-router/
  • …and 7 more

SKILL.md (first lines)

# Navigation and routing

An Expo app has one navigation tree, built by Expo Router from the files in `src/app/`, whose routes
equal a repo-owned route contract (`contracts/navigation.json`): flows switch by guards, hrefs and
params are checked, screens with unsaved work cannot be lost by a stray Back, and every URL lands
somewhere. React Navigation 7's static API is the fenced alternative. A script proves it.

## Rules that must hold

1. **One router, one tree: Expo Router's.** The root `src/app/_layout.tsx` default-exports a layout
   that renders the root navigator, directly or through the components it renders (app-startup-and-bootstrap's
   root skeleton: `AppRoot` > `RootStack` > `AppStack`). No `NavigationContainer`,
   `createStaticNavigation` or `@react-navigation/*` import beside it: expo-router 57 carries its own
   React Navigation, and a second copy has its own contexts and its own back stack. (`root-layout`,
   `one-navigator`)
2. **The contract is the route table.** Every route file (route id = its path under `src/app` without
   the extension) is in `contracts/navigation.json` `routes`, and every contract route has a file;
   `home` names the first screen. A route change starts in the contract, so a stray file (Expo Router
   bundles everything in `src/app`) or a forgotten screen is caught. (`contract`, `route-table`,
   `home-route`)
3. **Flows switch by guards, never by navigating.** Signed out / signed in, onboarding / main and
   test-build-only screens are `Stack.Protected` groups whose `guard` reads a store selector, each in
   the contract (`protected`). No `<Redirect>` in a layout and no `router.*` call from a layout's
   effect: they render the wrong screen first and leave it in the history, while a guard turning false
   removes its screens from the history. Guards hide screens; the server still checks data.
   (`protected-guard`, `flow-by-guard`)
4. **Hrefs are typed.** `experiments.typedRoutes` is on (CI generates the types with
   `npx expo customize tsconfig.json` before `tsc`), and app code never writes `as Href`: a renamed
   route then fails `tsc` wherever it is linked. (`typed-routes`, `href-cast`)
5. **Params are parsed at the edge.** Any URL can open a dynamic route with any text, and
   `useLocalSearchParams<T>()` is a cast. Each dynamic route has a valibot parser returning a `Result`
   (`paramParsers`), the model shows the screen's not-found state on failure, and params stay ids and
   short plain values. (`params-parsed`)
6. **A screen with unsaved work blocks its removal.** Its model hook calls `usePreventRemove` (the
   template's `useDiscardGuard`), which sees the header back button, the iOS swipe, a sheet's swipe
   down, Android's back button and predictive back gesture, and `router.back()`; Back asks before
   discarding. The contract lists it in `guarded`. Turn the iOS swipe off (`gestureEnabled` and
   `fullScreenGestureEnabled` false, `"gesture": "off"`) only where starting it already harms. The
   guard cannot see a kill: save drafts as they change. (`guarded-prevent-remove`,
   `guarded-gesture-off`)
7. **Going back never stacks a second home.** `router.navigate` and `push` (and `<Link>`) push, so the
   home route is reached with `router.dismissTo(home)` or `router.back()` (`<Link dismissTo>`).
   (`go-back`)
8. **A destination is a route; a layer over a live screen is not.** Modal destinations are routes with
   a modal `presentation`, exactly the contract's `modals` (on Android `modal` opens like a push,
   `formSheet` is a bottom sheet). Confirmations, the discard dialog and a pause layer render inside
   the screen or the dialog host, because a route change would cover or unmount what they sit on.
   (`modal-presentation`)
9. **Every URL lands somewhere.** `src/app/+not-found.tsx` renders for an unknown URL (without it the
   router shows its development page); its one action is `dismissTo('/')`, so Back cannot return to
   the dead link, and it never prints the URL. (`not-found`)
10. **The native stack.** `Stack` from `expo-router` (react-native-screens): the platform draws the
    transitions, swipe and header. The JavaScript stack only with `"jsStack": true` and a reason.
    Every `<Stack>` takes its animation from motion-and-animation's `useScreenAnimation()`.
    (`native-stack`)
11. **Navigation lives in screens' model hooks.** `src/ui`, `src/stores`, `src/services`, `src/data`,
    `src/api` and `src/core` never import a router: components take `onPress`, stores change state.
    The one exception is a root kit (`src/services/<concern>/<concern>-root-kit.tsx`, registered in
    app-startup-and-bootstrap's `createRootKits`), whose effects route notification taps beside the
    root Stack. Navigation state is never persisted; an open editor is reopened from its saved draft.
    (`nav-in-screens`)
12. **Debug routes are gated and guarded.** A test-build screen renders only through `TEST_ONLY`
    (debug-menu-and-test-hooks' gate, so production bundles drop it) and sits in a protected group,
    so a production deep link cannot open it. (`debug-gated`)
13. **Placeholders are listed and never ship.** While screens are built one by one, an unbuilt route
    renders the placeholder screen, so the route table, its typed hrefs and every model hook's
    navigation exist from day one (no no-op handlers); the contract lists them and `--release` fails
    while any is left. (`placeholder-route`, `placeholder-release`)
14. **With React Navigation 7 instead (appendix):** one static config whose groups (`if` hooks) equal
    the contract's `groups`, one `createStaticNavigation` container (never `<NavigationContainer>`),
    `RootParamList` from `StaticParamList`, plain-data params, `popTo` to go back, no `reset()` to
    switch flows, and the stack wrapped with `.with()` so pushes cross-fade under reduce motion.
    (`group-if`, `static-container`, `typed-routes`, `params-serialisable`, `flow-by-guard`,
    `motion-transition`)

## Workflow

1. **Read what the task needs** in [references/expo-router-in-practice.md](references/expo-router-in-practice.md):

   | Task | Section |
   |---|---|
   | first setup, the root layout, reopening work after a kill | "The root layout and the startup order" |
   | a new screen, a tab, a header | "File routes", "Layouts: one navigator tree" |
   | a link or a param | "Typed routes and hrefs", "Params: parse them at the edge" |
   | sign-in, onboarding, test-only screens | "Flows: Stack.Protected, never a redirect", "Debug routes and placeholders" |
   | a modal, a sheet, a dialog | "Modals and what is not a route" |
   | Back, the swipe, Android back, unsaved changes | "Back handling (iOS and Android)", "Going back to a screen already in the stack" |
   | a route test | "Testing navigation" |

   With React Navigation instead of Expo Router, read [references/react-navigation-static.md](references/react-navigation-static.md)
   and imitate `examples/react-navigation/`.
2. **Set up once.** Copy `templates/contracts/navigation.json` (and its schema) and list the app's
   routes; copy `templates/src/app/+not-found.tsx` with `templates/src/features/not-found/` and add the
   `not-found.*` keys of `templates/src/i18n/catalogs/` to every catalog (i18n-strings-and-catalogs).
   Turn on `experiments.typedRoutes` in `app.config.ts`. The root layout and `AppStack` come from
   app-startup-and-bootstrap's root skeleton; add the root `Stack.Protected` groups inside `AppStack`.
3. **Add a route:** add its id to the contract, create the thin route file rendering the feature's
   screen (architecture-and-boundaries), add its `Stack.Screen` in the layout only for options (a
   title, `presentation`), and put its navigation calls in the feature's model hook.
4. **A dynamic route:** write `src/features/<feature>/<feature>-params.ts` after
   `templates/src/features/task-detail/task-detail-params.ts` (with its test), map it in
   `paramParsers`, and render the not-found state on a parse failure.
5. **A flow:** wrap its screens in `<Stack.Protected guard={...}>` fed by a store selector, add the
   group to `protected`, and test it the way `templates/src/features/root-navigator/protected-routes.test.tsx`
   does (copy that test when the app adds its first flow; it also pins the router facts).
6. **A screen with unsaved work:** copy `templates/src/features/task-editor/use-discard-guard.ts` with
   both tests, call it from the model hook with the "unsaved" boolean, render the dialog while
   `isPromptOpen`, leave after a save through `leave()`, and list the route in `guarded`.
7. **Run the tests**, paths first (`--selectProjects` takes every following word as a project):
   `npx jest src/features/not-found src/features/root-navigator src/features/task-editor src/features/task-detail --selectProjects unit android`,
   then `npm run typecheck` and lint as quality-gates requires.
8. **Run the checker:** `node ${CLAUDE_SKILL_DIR}/scripts/check-navigation.mjs .`; fix every `FAIL`
   line (file, rule, fix) and rerun until `RESULT: PASS`. Rules and contract keys:
   [references/contract-and-checker.md](references/contract-and-checker.md). Before a release, run it
   with `--release`.

## Definition of done

- [ ] `contracts/navigation.json` lists exactly the app's routes, `home`, every protected group, every

From zakariaf/ReactNative-Skills, MIT.

More from zakariaf/ReactNative-Skills

Signals

Stars
11
Forks
2
Open issues
0
Last commit (this folder)
Oct 17, 2026 (11 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from zakariaf

Cite this page

navigation-and-routing skill. Agent Mart. https://agentmart.dev/skills/zakariaf-navigation-and-routing (last updated 2026-10-24)