diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000000..f2fdd04100 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,7 @@ +# Browsers we support +Chrome >= 73 +ChromeAndroid >= 75 +Firefox >= 67 +Edge >= 17 +Safari >= 12.1 +iOS >= 11.3 diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000000..e5b6d8d6a6 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000000..19a0281431 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", + "changelog": [ + "@remix-run/changelog-github", + { "repo": "remix-run/react-router" } + ], + "commit": false, + "fixed": [ + [ + "create-react-router", + "react-router", + "react-router-dom", + "@react-router/architect", + "@react-router/cloudflare", + "@react-router/dev", + "@react-router/fs-routes", + "@react-router/express", + "@react-router/node", + "@react-router/remix-routes-option-adapter", + "@react-router/serve" + ] + ], + "linked": [], + "access": "public", + "baseBranch": "dev", + "updateInternalDependencies": "patch", + "bumpVersionsWithWorkspaceProtocolOnly": true, + "ignore": ["integration", "integration-*", "@playground/*"], + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + } +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..49a812d5b4 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,23 @@ +/fixtures/ +node_modules/ +pnpm-lock.yaml +/docs/api +examples/**/dist/ +worker-configuration.d.ts +/playground/ +/playground-local/ +integration/helpers/**/dist/ +integration/helpers/**/build/ +playwright-report/ +test-results/ +build.utils.d.ts +.wrangler/ +.tmp/ +.react-router/ +.react-router-parcel/ +packages/**/dist/ +packages/react-router-dom/server.d.ts +packages/react-router-dom/server.js +packages/react-router-dom/server.mjs +tutorial/dist/ +public/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..2e0c041bd5 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,96 @@ +{ + "extends": ["react-app"], + "rules": { + "import/first": "off", + "@typescript-eslint/consistent-type-imports": "error" + }, + "overrides": [ + { + "files": ["**/__tests__/**"], + "plugins": ["jest"], + "extends": ["plugin:jest/recommended"] + }, + { + "files": ["integration/**/*.*"], + "rules": { + "react-hooks/rules-of-hooks": "off" + }, + "env": { + "jest/globals": false + } + }, + { + "files": ["packages/react-router-dev/config/default-rsc-entries/*"], + "rules": { + "@typescript-eslint/consistent-type-imports": "off" + } + }, + { + // Only apply JSDoc lint rules to files we auto-generate docs for + "files": [ + "packages/react-router/lib/components.tsx", + "packages/react-router/lib/hooks.tsx", + "packages/react-router/lib/dom/lib.tsx", + "packages/react-router/lib/dom/ssr/components.tsx", + "packages/react-router/lib/dom/ssr/server.tsx", + "packages/react-router/lib/dom-export/hydrated-router.tsx", + "packages/react-router/lib/dom/server.tsx", + "packages/react-router/lib/router/utils.ts", + "packages/react-router/lib/rsc/browser.tsx", + "packages/react-router/lib/rsc/server.rsc.ts", + "packages/react-router/lib/rsc/server.ssr.tsx", + "packages/react-router/lib/rsc/html-stream/browser.ts", + ], + "plugins": ["jsdoc"], + "rules": { + "jsdoc/check-access": "error", + "jsdoc/check-alignment": "error", + "jsdoc/check-param-names": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-tag-names": [ + "error", + { + "definedTags": ["additionalExamples", "category", "mode"] + } + ], + "jsdoc/no-defaults": "error", + "jsdoc/no-multi-asterisks": ["error", { "allowWhitespace": true }], + "jsdoc/require-description": "error", + "jsdoc/require-param": ["error", { "enableRootFixer": false }], + "jsdoc/require-param-description": "error", + "jsdoc/require-param-name": "error", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-check": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/sort-tags": [ + "error", + { + "tagSequence": [ + { + "tags": ["description"] + }, + { + "tags": ["example"] + }, + { + "tags": ["additionalExamples"] + }, + { + "tags": [ + "name", + "public", + "private", + "category", + "mode", + "param", + "returns" + ] + } + ] + } + ] + } + } + ], + "reportUnusedDisableDirectives": true +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..360491fde8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: react-router +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..4ef4cbaa8e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,75 @@ +name: ๐Ÿ› Bug Report +description: Something is wrong with React Router +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thank you for helping to improve React Router! + + Please note that **all** bugs must have a **minimal** and **runnable** reproduction, meaning that it is not just pointing to a deployed site or a branch in your existing application, or showing a small code snippet. For more information, please refer to the [Bug/Issue Process Guidelines](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#bugissue-process). + + ## If you are using **Framework Mode**, you have 2 preferred options + + ### Option 1 - Create a failing integration test + + ๐Ÿ† The most helpful reproduction is to use our _bug report integration test_ template: + + 1. [Fork `remix-run/react-router`](https://github.com/remix-run/react-router/fork) + 2. Open [`integration/bug-report-test.ts`](https://github.com/remix-run/react-router/blob/dev/integration/bug-report-test.ts) in your editor + 3. Follow the instructions to create a failing bug report test + 4. Link to your forked branch with the failing test in this issue + + ### Option 2 - Create a **minimal** reproduction + + - ๐Ÿฅ‡ Link to a [StackBlitz](https://reactrouter.com/new) environment + - ๐Ÿฅˆ Link to a GitHub repository containing a minimal reproduction app + + ## If you are using **Data** or **Declarative Mode** + + Create a **minimal** reproduction + + - ๐Ÿฅ‡ Link to a CodeSandbox repro: [TS](https://codesandbox.io/templates/react-vite-ts) | [JS](https://codesandbox.io/templates/react-vite) + - ๐Ÿฅˆ Link to a GitHub repository containing a minimal reproduction app + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Link to reproduction and steps to reproduce the behavior + placeholder: Go to https://stackblitz.com/edit/... and click the "Submit" button + validations: + required: true + - type: textarea + id: system-info + attributes: + label: System Info + description: Output of `npx envinfo --system --npmPackages '{vite,react-router,@react-router/*}' --binaries --browsers` + render: shell + placeholder: System, Binaries, Browsers + validations: + required: true + - type: dropdown + id: package-manager + attributes: + label: Used Package Manager + description: Select the used package manager + options: + - npm + - yarn + - pnpm + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Actual Behavior + description: A concise description of what you're experiencing. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..173a136c4b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: ๐Ÿ’ก Feature Request + url: https://github.com/remix-run/react-router/discussions/new?category=proposals + about: If you've got an idea for a new feature in React Router, please open a new Discussion with the `Proposals` label + - name: ๐Ÿค” Usage Question (Github Discussions) + url: https://github.com/remix-run/remix/discussions/new?category=q-a + about: Open a Discussion in GitHub with the `Q&A` label + - name: ๐Ÿ’ฌ Remix Discord Channel + url: https://rmx.as/discord + about: Interact with other people using React Router and Remix ๐Ÿ“€ diff --git a/.github/ISSUE_TEMPLATE/documentation_isse.yml b/.github/ISSUE_TEMPLATE/documentation_isse.yml new file mode 100644 index 0000000000..c548d51124 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_isse.yml @@ -0,0 +1,21 @@ +name: ๐Ÿ“š Documentation Issue +description: Something is wrong with the React Router docs +title: "[Docs]: " +labels: + - docs +body: + - type: markdown + attributes: + value: | + Thank you for contributing! + + For documentation updates - we would happily accept PRs, so feel free to update and + open a PR to the `main` branch. Otherwise let us know in this issue what you felt + was missing or incorrect. + + - type: textarea + attributes: + label: Describe what's incorrect/missing in the documentation + description: A concise description of what you expected to see in the docs + validations: + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..253bcb76ba --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/close-feature-pr.yml b/.github/workflows/close-feature-pr.yml new file mode 100644 index 0000000000..b08418ca44 --- /dev/null +++ b/.github/workflows/close-feature-pr.yml @@ -0,0 +1,28 @@ +# Close a singular pull request that implements a feature that has not +# gone through the Proposal process +# Triggered by adding the `feature-request` label to an issue + +name: ๐Ÿšช Check Feature PR + +on: + pull_request_target: + types: [labeled] + +jobs: + close-feature-pr: + name: ๐Ÿšช Check Feature PR + if: github.repository == 'remix-run/react-router' && github.event.label.name == 'feature-request' + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + + - name: ๐Ÿšช Close PR + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr comment ${{ github.event.pull_request.number }} -F ./scripts/close-feature-pr.md + gh pr edit ${{ github.event.pull_request.number }} --remove-label ${{ github.event.label.name }} + gh pr close ${{ github.event.pull_request.number }} diff --git a/.github/workflows/close-no-repro-issue.yml b/.github/workflows/close-no-repro-issue.yml new file mode 100644 index 0000000000..c0e22f26ce --- /dev/null +++ b/.github/workflows/close-no-repro-issue.yml @@ -0,0 +1,25 @@ +# Close a singular issue without a reproduction +# Triggered by adding the `no-reproduction` label to an issue + +name: ๐Ÿšช Close issue without a reproduction + +on: + issues: + types: [labeled] + +jobs: + close-no-repro-issue: + name: ๐Ÿšช Close issue + if: github.repository == 'remix-run/react-router' && github.event.label.name == 'no-reproduction' + runs-on: ubuntu-latest + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + + - name: ๐Ÿšช Close issue + env: + GH_TOKEN: ${{ github.token }} + run: | + gh issue comment ${{ github.event.issue.number }} -F ./scripts/close-no-repro-issue.md + gh issue edit ${{ github.event.issue.number }} --remove-label ${{ github.event.label.name }} + gh issue close ${{ github.event.issue.number }} -r "not planned"; diff --git a/.github/workflows/close-no-repro-issues.yml b/.github/workflows/close-no-repro-issues.yml new file mode 100644 index 0000000000..efc77ff396 --- /dev/null +++ b/.github/workflows/close-no-repro-issues.yml @@ -0,0 +1,49 @@ +# This is a bulk-close script that was used initially to find and close issues +# without a repro, but moving forward we'll likely use the singular version +# (close-no-repro-issue.yml) on new issues which is driven by a label added to +# the issue + +name: ๐Ÿšช Close issues without a reproduction + +on: + workflow_dispatch: + inputs: + dryRun: + type: boolean + description: "Dry Run? (no issues will be closed)" + default: false + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + close-no-repro-issues: + name: ๐Ÿšช Close issues + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + env: + CI: "true" + GH_TOKEN: ${{ github.token }} + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4.1.0 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + # required for --experimental-strip-types + node-version: 22 + cache: "pnpm" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: ๐Ÿšช Close Issues (Dry Run) + if: ${{ inputs.dryRun }} + run: node --experimental-strip-types ./scripts/close-no-repro-issues.ts --dryRun + + - name: ๐Ÿšช Close Issues + if: ${{ ! inputs.dryRun }} + run: node --experimental-strip-types ./scripts/close-no-repro-issues.ts diff --git a/.github/workflows/deduplicate-lock-file.yml b/.github/workflows/deduplicate-lock-file.yml new file mode 100644 index 0000000000..3174fcfcbb --- /dev/null +++ b/.github/workflows/deduplicate-lock-file.yml @@ -0,0 +1,49 @@ +name: โš™๏ธ Deduplicate lock file + +on: + push: + branches: + - dev + paths: + - pnpm-lock.yaml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dedupe: + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + with: + token: ${{ secrets.FORMAT_PAT }} + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + + - name: โš™๏ธ Dedupe lock file + run: pnpm dedupe && rm -rf ./node_modules && pnpm install + + - name: ๐Ÿ’ช Commit + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + + git add . + if [ -z "$(git status --porcelain)" ]; then + echo "๐Ÿ’ฟ no deduplication needed" + exit 0 + fi + git commit -m "chore: deduplicate \`pnpm-lock.yaml\`" + git push + echo "๐Ÿ’ฟ pushed dedupe changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..1e6391dfd8 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,67 @@ +name: ๐Ÿ“š Docs + +on: + push: + branches: + - main + - dev + paths: + - "packages/**/*.ts" + - "packages/**/*.tsx" + - "scripts/docs.ts" + workflow_dispatch: + inputs: + branch: + description: "Branch to generate docs on" + required: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + docs: + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + with: + token: ${{ secrets.FORMAT_PAT }} + ref: ${{ github.event.inputs.branch }} + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: pnpm + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ— Build + run: pnpm build + + - name: ๐Ÿ“š Generate Typedoc Docs + run: pnpm run docs:typedoc + + - name: ๐Ÿ“š Generate Markdown Docs + run: pnpm run docs:jsdoc --write + + - name: ๐Ÿ’ช Commit + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + + git add . + if [ -z "$(git status --porcelain)" ]; then + echo "๐Ÿ’ฟ no docs changed" + exit 0 + fi + git commit -m "chore: generate markdown docs from jsdocs" + git push + echo "๐Ÿ’ฟ pushed docs changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000000..ff63cd21ee --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,54 @@ +name: ๐Ÿ‘” Format + +on: + push: + branches: + - main + - dev + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + format: + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + with: + token: ${{ secrets.FORMAT_PAT }} + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: pnpm + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ”ƒ Sort contributors.yml + run: sort --ignore-case --output contributors.yml contributors.yml + + - name: ๐Ÿ‘” Format + run: pnpm format + + - name: ๐Ÿ’ช Commit + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + + git add . + if [ -z "$(git status --porcelain)" ]; then + echo "๐Ÿ’ฟ no formatting changed" + exit 0 + fi + git commit -m "chore: format" + git push + echo "๐Ÿ’ฟ pushed formatting changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)" diff --git a/.github/workflows/integration-full.yml b/.github/workflows/integration-full.yml new file mode 100644 index 0000000000..cb1d064ee2 --- /dev/null +++ b/.github/workflows/integration-full.yml @@ -0,0 +1,53 @@ +name: Branch + +# main/dev branches will get the full run across +# all OS/browsers for multiple node versions + +on: + push: + branches: + - main + - dev + paths-ignore: + - ".changeset/**" + - "decisions/**" + - "docs/**" + - "examples/**" + - "jest/**" + - "scripts/**" + - "tutorial/**" + - "contributors.yml" + - "**/*.md" + +jobs: + build: + name: "โš™๏ธ Build" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-build.yml + + integration-ubuntu: + name: "๐Ÿ‘€ Integration Test" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-integration.yml + with: + os: "ubuntu-latest" + node_version: "[20, 22]" + browser: '["chromium", "firefox"]' + + integration-windows: + name: "๐Ÿ‘€ Integration Test" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-integration.yml + with: + os: "windows-latest" + node_version: "[22]" + browser: '["msedge"]' + + integration-macos: + name: "๐Ÿ‘€ Integration Test" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-integration.yml + with: + os: "macos-latest" + node_version: "[20, 22]" + browser: '["webkit"]' diff --git a/.github/workflows/integration-pr-ubuntu.yml b/.github/workflows/integration-pr-ubuntu.yml new file mode 100644 index 0000000000..ac25713e08 --- /dev/null +++ b/.github/workflows/integration-pr-ubuntu.yml @@ -0,0 +1,35 @@ +name: PR (Base) + +# All PRs touching code will run tests on ubuntu/node/chromium + +on: + pull_request: + paths-ignore: + - ".changeset/**" + - "decisions/**" + - "docs/**" + - "examples/**" + - "jest/**" + - "scripts/**" + - "tutorial/**" + - "contributors.yml" + - "**/*.md" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: "โš™๏ธ Build" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-build.yml + + integration-chromium: + name: "๐Ÿ‘€ Integration Test" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-integration.yml + with: + os: "ubuntu-latest" + node_version: "[22]" + browser: '["chromium"]' diff --git a/.github/workflows/integration-pr-windows-macos.yml b/.github/workflows/integration-pr-windows-macos.yml new file mode 100644 index 0000000000..f3d12c7681 --- /dev/null +++ b/.github/workflows/integration-pr-windows-macos.yml @@ -0,0 +1,45 @@ +name: PR (Full) + +# PRs touching react-router-dev will also run on Windows and OSX + +on: + pull_request: + paths: + - "pnpm-lock.yaml" + - "integration/**" + - "packages/react-router-dev/**" + - "!**/*.md" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + integration-firefox: + name: "๐Ÿ‘€ Integration Test" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-integration.yml + with: + os: "ubuntu-latest" + node_version: "[22]" + browser: '["firefox"]' + + integration-msedge: + name: "๐Ÿ‘€ Integration Test" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-integration.yml + with: + os: "windows-latest" + node_version: "[22]" + browser: '["msedge"]' + timeout: 120 + + integration-webkit: + name: "๐Ÿ‘€ Integration Test" + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/shared-integration.yml + with: + os: "macos-latest" + node_version: "[22]" + browser: '["webkit"]' + timeout: 40 diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 0000000000..5fed41a852 --- /dev/null +++ b/.github/workflows/no-response.yml @@ -0,0 +1,34 @@ +name: ๐Ÿฅบ No Response + +on: + schedule: + # Schedule for five minutes after the hour, every hour + - cron: "5 * * * *" + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿฅบ Handle Ghosting + uses: actions/stale@v10 + with: + days-before-close: 10 + close-issue-message: > + This issue has been automatically closed because we haven't received a + response from the original author ๐Ÿ™ˆ. This automation helps keep the issue + tracker clean from issues that aren't actionable. Please reach out if you + have more information for us! ๐Ÿ™‚ + close-pr-message: > + This PR has been automatically closed because we haven't received a + response from the original author ๐Ÿ™ˆ. This automation helps keep the issue + tracker clean from PRs that aren't actionable. Please reach out if you + have more information for us! ๐Ÿ™‚ + # don't automatically mark issues/PRs as stale + days-before-stale: -1 + stale-issue-label: needs-response + stale-pr-label: needs-response diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..c4849e6bf3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,257 @@ +# We use this singular file for all of our releases because we can only specify +# a singular GitHub workflow file in npm's Trusted Publishing configuration. +# See https://docs.npmjs.com/trusted-publishers for more info. +# +# Specific jobs only run on the proper trigger: +# +# - Changesets-driven pre-releases/stable releases +# - Trigger: push to release-next/release-v6 branch +# - jobs: release -> find_package_version -> comment +# - Nightly releases +# - Trigger: schedule/cron +# - jobs: release-nightly +# - Experimental releases (from a workflow_dispatch trigger) +# - Trigger: workflow_dispatch +# - jobs: release-experimental + +name: ๐Ÿšข Release +on: + # Changesets-driven prereleases and stable releases + push: + branches: + - "release-next" + - "release-v6" + # Nightly releases + schedule: + - cron: "0 7 * * *" # every day at 12AM PST + # Experimental Releases + workflow_dispatch: + inputs: + branch: + required: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CI: true + +jobs: + release: + name: ๐Ÿฆ‹ Changesets Release + if: github.repository == 'remix-run/react-router' && github.event_name == 'push' + runs-on: ubuntu-latest + outputs: + published_packages: ${{ steps.changesets.outputs.publishedPackages }} + published: ${{ steps.changesets.outputs.published }} + permissions: + contents: write # enable pushing changes to the origin + id-token: write # enable generation of an ID token for publishing + pull-requests: write # enable opening a PR for the release + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version: 24 # Needed for npm@11 for Trusted Publishing + cache: "pnpm" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + # This action has two responsibilities. The first time the workflow runs + # (initial push to a `release-*` branch) it will create a new branch and + # then open a PR with the related changes for the new version. After the + # PR is merged, the workflow will run again and this action will build + + # publish to npm. + - name: ๐Ÿš€ PR / Publish + id: changesets + # PLEASE KEEP THIS PINNED TO 1.4.10 to avoid a regression in 1.5.* + # See https://github.com/changesets/action/issues/465 + uses: changesets/action@v1.4.10 + with: + version: pnpm run changeset:version + commit: "chore: Update version for release" + title: "chore: Update version for release" + publish: pnpm run release + createGithubReleases: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + find_package_version: + name: ๐Ÿฆ‹ Find Package + needs: [release] + runs-on: ubuntu-latest + if: github.repository == 'remix-run/react-router' && github.event_name == 'push' && github.ref_name != 'release-v6' && needs.release.outputs.published == 'true' + outputs: + package_version: ${{ steps.find_package_version.outputs.package_version }} + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version: 24 # Needed for npm@11 for Trusted Publishing + cache: "pnpm" + + - id: find_package_version + run: | + package_version=$(node ./scripts/find-release-from-changeset.js) + echo "package_version=${package_version}" >> $GITHUB_OUTPUT + env: + PACKAGE_VERSION_TO_FOLLOW: "react-router" + PUBLISHED_PACKAGES: ${{ needs.release.outputs.published_packages }} + + comment: + name: ๐Ÿ“ Comment on related issues and pull requests + if: github.repository == 'remix-run/react-router' && github.event_name == 'push' && github.ref_name != 'release-v6' && needs.find_package_version.outputs.package_version != '' + needs: [release, find_package_version] + runs-on: ubuntu-latest + permissions: + issues: write # enable commenting on released issues + pull-requests: write # enable commenting on released pull requests + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: ๐Ÿ“ Comment on related issues and pull requests + uses: remix-run/release-comment-action@v0.4.2 + with: + DIRECTORY_TO_CHECK: "./packages" + PACKAGE_NAME: "react-router" + ISSUE_LABELS_TO_REMOVE: "awaiting release" + + # HEADS UP! this "nightly" job will only ever run on the `main` branch due to + # it being a cron job, and the last commit on main will be what github shows + # as the trigger however in the checkout below we specify the `dev` branch, + # so all the scripts in this job will be ran from that, confusing i know, so + # in some cases we'll need to create multiple PRs when modifying nightly + # release processes + release-nightly: + name: ๐ŸŒ’ Nightly Release + if: github.repository == 'remix-run/react-router' && github.event_name == 'schedule' + runs-on: ubuntu-latest + permissions: + contents: write # enable pushing changes to the origin + id-token: write # enable generation of an ID token for publishing + outputs: + # will be undefined if there's no release necessary + NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }} + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + with: + ref: dev + # checkout using a custom token so that we can push later on + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version: 24 # Needed for npm@11 for Trusted Publishing + cache: "pnpm" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ•ต๏ธ Check for changes + id: version + run: | + SHORT_SHA=$(git rev-parse --short HEAD) + + # get latest nightly tag + LATEST_NIGHTLY_TAG=$(git tag -l v0.0.0-nightly-\* --sort=-creatordate | head -n 1) + + # check if last commit to dev starts with the nightly tag we're about + # to create (minus the date) + # if it is, we'll skip the nightly creation + # if not, we'll create a new nightly tag + if [[ ${LATEST_NIGHTLY_TAG} == v0.0.0-nightly-${SHORT_SHA}-* ]]; then + echo "๐Ÿ›‘ Latest nightly tag is the same as the latest commit sha, skipping nightly release" + else + # yyyyMMdd format (e.g. 20221207) + DATE=$(date '+%Y%m%d') + # v0.0.0-nightly-- + NEXT_VERSION=0.0.0-nightly-${SHORT_SHA}-${DATE} + # set output so it can be used in other jobs + echo "NEXT_VERSION=${NEXT_VERSION}" >> $GITHUB_OUTPUT + fi + + - name: โคด๏ธ Update version + if: steps.version.outputs.NEXT_VERSION + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + git checkout -b nightly/${{ steps.version.outputs.NEXT_VERSION }} + pnpm run version ${{steps.version.outputs.NEXT_VERSION}} + git push origin --tags + + - name: ๐Ÿ— Build + if: steps.version.outputs.NEXT_VERSION + run: pnpm build + + - name: ๐Ÿš€ Publish + if: steps.version.outputs.NEXT_VERSION + run: pnpm run publish + + release-experimental: + name: ๐Ÿงช Experimental Release + if: github.repository == 'remix-run/react-router' && github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write # enable pushing changes to the origin + id-token: write # enable generation of an ID token for publishing + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + with: + ref: ${{ github.event.inputs.branch }} + # checkout using a custom token so that we can push later on + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version: 24 # Needed for npm@11 for Trusted Publishing + cache: "pnpm" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: โคด๏ธ Update version + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + SHORT_SHA=$(git rev-parse --short HEAD) + NEXT_VERSION=0.0.0-experimental-${SHORT_SHA} + git checkout -b experimental/${NEXT_VERSION} + pnpm run version ${NEXT_VERSION} + git push origin --tags + + - name: ๐Ÿ— Build + run: pnpm build + + - name: ๐Ÿš€ Publish + run: pnpm run publish diff --git a/.github/workflows/shared-build.yml b/.github/workflows/shared-build.yml new file mode 100644 index 0000000000..4a3617aa6c --- /dev/null +++ b/.github/workflows/shared-build.yml @@ -0,0 +1,38 @@ +name: ๐Ÿ› ๏ธ Build + +on: + workflow_call: + +env: + CI: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + + # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297 + # - uses: google/wireit@setup-github-actions-caching/v2 + + - name: Disable GitHub Actions Annotations + run: | + echo "::remove-matcher owner=tsc::" + echo "::remove-matcher owner=eslint-compact::" + echo "::remove-matcher owner=eslint-stylish::" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ— Build + run: pnpm build diff --git a/.github/workflows/shared-integration.yml b/.github/workflows/shared-integration.yml new file mode 100644 index 0000000000..782507c3ac --- /dev/null +++ b/.github/workflows/shared-integration.yml @@ -0,0 +1,69 @@ +name: ๐Ÿงช Test (Integration) + +on: + workflow_call: + inputs: + os: + required: true + type: string + node_version: + required: true + # this is limited to string | boolean | number (https://github.community/t/can-action-inputs-be-arrays/16457) + # but we want to pass an array (node_version: "[20, 22]"), + # so we'll need to manually stringify it for now + type: string + browser: + required: true + # this is limited to string | boolean | number (https://github.community/t/can-action-inputs-be-arrays/16457) + # but we want to pass an array (browser: "['chromium', 'firefox']"), + # so we'll need to manually stringify it for now + type: string + timeout: + required: false + type: number + default: 40 + +env: + CI: true + +jobs: + integration: + name: "${{ inputs.os }} / node@${{ matrix.node }} / ${{ matrix.browser }}" + strategy: + fail-fast: false + matrix: + node: ${{ fromJSON(inputs.node_version) }} + browser: ${{ fromJSON(inputs.browser) }} + + runs-on: ${{ inputs.os }} + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node ${{ matrix.node }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: "pnpm" + + # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297 + # - uses: google/wireit@setup-github-actions-caching/v2 + + - name: Disable GitHub Actions Annotations + run: | + echo "::remove-matcher owner=tsc::" + echo "::remove-matcher owner=eslint-compact::" + echo "::remove-matcher owner=eslint-stylish::" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ“ฅ Install Playwright + run: npx playwright install --with-deps ${{ matrix.browser }} + + - name: ๐Ÿ‘€ Run Integration Tests ${{ matrix.browser }} + run: "pnpm test:integration --project=${{ matrix.browser }}" + timeout-minutes: ${{inputs.timeout}} diff --git a/.github/workflows/support.yml b/.github/workflows/support.yml new file mode 100644 index 0000000000..be506d9b36 --- /dev/null +++ b/.github/workflows/support.yml @@ -0,0 +1,25 @@ +name: "Support Requests" + +on: + issues: + types: [labeled, unlabeled, reopened] + +permissions: + issues: write + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/support-requests@v4 + with: + issue-comment: > + :wave: @{issue-author}, we use the issue tracker exclusively for bug reports + and feature requests. However, this issue appears to be a support request. + + For usage questions, please use [Stack Overflow](https://stackoverflow.com/questions/tagged/react-router) + or [Discord](https://rmx.as/discord) where there are a lot more people ready to help you out, or + [post a new question](https://github.com/remix-run/react-router/discussions/new?category=q-a) in the + Discussions tab of this repository. + + Please feel free to clarify your issue if you think it was closed prematurely. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..2bf0dedbe9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,70 @@ +name: ๐Ÿงช Test + +on: + push: + branches: + - main + - dev + tags-ignore: + - v* + paths-ignore: + - "docs/**" + - "**/README.md" + pull_request: + paths-ignore: + - "docs/**" + - "**/*.md" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: "๐Ÿงช Test: (Node: ${{ matrix.node }})" + strategy: + fail-fast: false + matrix: + node: + - 20 + - 22 + + runs-on: ubuntu-latest + + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: pnpm + check-latest: true + + # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297 + # - uses: google/wireit@setup-github-actions-caching/v2 + + - name: Disable GitHub Actions Annotations + run: | + echo "::remove-matcher owner=tsc::" + echo "::remove-matcher owner=eslint-compact::" + echo "::remove-matcher owner=eslint-stylish::" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ— Build + run: pnpm build + + - name: ๐Ÿ” Typecheck + run: pnpm typecheck + + - name: ๐Ÿ”ฌ Lint + run: pnpm lint + + - name: ๐Ÿงช Run tests + run: pnpm test diff --git a/.gitignore b/.gitignore index aa4c1ddffc..53e9a5baf1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,39 @@ -examples/**/*-bundle.js +.DS_Store +npm-debug.log + +/website/build/ +node_modules/ + +/examples/*/yarn.lock +/examples/*/pnpm-lock.yaml +/examples/*/dist +/tutorial/dist +/playground-local/ +/integration/playwright-report +/integration/test-results + +# v5 build files +/packages/*/cjs/ +/packages/*/esm/ +/packages/*/umd/ + +# v6 build files +/build/ +/packages/*/dist/ +/packages/*/LICENSE.md + +# v7 build files +.react-router + +.wireit +.eslintcache +.parcel-cache +.tmp +tsup.config.bundled_*.mjs +build.utils.d.ts +worker-configuration.d.ts +/.env +/NOTES.md + +# v7 reference docs +/public \ No newline at end of file diff --git a/.npmignore b/.npmignore deleted file mode 100644 index b5dbfd74e4..0000000000 --- a/.npmignore +++ /dev/null @@ -1,7 +0,0 @@ -CONTRIBUTING.md -bower.json -examples -karma.conf.js -scripts -specs -webpack.config.js diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..555ffa6830 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +ignore-workspace-cycles=true +enable-pre-post-scripts=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..8fdd954df9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fde32f5f25..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: node_js -node_js: - - "0.10" -before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..fae8e3d8a9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a7337556..6781d23a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,229 +1,4169 @@ -v0.9.3 - Wed, 08 Oct 2014 14:44:52 GMT --------------------------------------- - -- [caf3a2b](../../commit/caf3a2b) [fixed] scrollBehavior='none' on path update - + -v0.9.2 - Wed, 08 Oct 2014 05:33:30 GMT --------------------------------------- +# React Router Releases -- [d57f830](../../commit/d57f830) [changed] Public interface for Location objects -- [6723dc5](../../commit/6723dc5) [added] ability to set params/query in Redirect -- [60f9eb4](../../commit/60f9eb4) [fixed] encoded ampersands in query params -- [668773c](../../commit/668773c) [fixed] transitioning to paths with . - - -v0.9.1 - Mon, 06 Oct 2014 20:55:32 GMT --------------------------------------- +This page lists all releases/release notes for React Router back to `v6.0.0`. For releases prior to v6, please refer to the [Github Releases Page](https://github.com/remix-run/react-router/releases). -- [76fe696](../../commit/76fe696) [fixed] trailing comma +We manage release notes in this file instead of the paginated Github Releases Page for 2 reasons: +- Pagination in the Github UI means that you cannot easily search release notes for a large span of releases at once +- The paginated Github interface also cuts off longer releases notes without indication in list view, and you need to click into the detail view to see the full set of release notes -v0.9.0 - Mon, 06 Oct 2014 19:37:27 GMT --------------------------------------- - -- [5aae2a8](../../commit/5aae2a8) [added] onChange event to Routes -- [ba65269](../../commit/ba65269) [removed] AsyncState -- [4d8c7a1](../../commit/4d8c7a1) [removed] `` -- [4d8c7a1](../../commit/4d8c7a1) [removed] `` -- [ed0cf62](../../commit/ed0cf62) [added] Navigation mixin for components that need to modify the URL -- [ed0cf62](../../commit/ed0cf62) [added] CurrentPath mixin for components that need to know the current URL path -- [ed0cf62](../../commit/ed0cf62) [added] getActiveRoutes, getActiveParams, and getActiveQuery methods to ActiveState mixin -- [ed0cf62](../../commit/ed0cf62) [removed] Awkward updateActiveState callback from ActiveState mixin -- [ed0cf62](../../commit/ed0cf62) [removed] Router.PathState (use Router.CurrentPath instead) -- [ed0cf62](../../commit/ed0cf62) [removed] Router.Transitions (use Router.Navigation instead) -- [ed0cf62](../../commit/ed0cf62) [removed] Router.RouteLookup (because it was useless) -- [ed0cf62](../../commit/ed0cf62) [added] `` alias of "imitateBrowser" -- [ed0cf62](../../commit/ed0cf62) [changed] `` => `` will be useful for SSR +
+ Table of Contents +- [React Router Releases](#react-router-releases) + - [v7.9.5](#v795) + - [What's Changed](#whats-changed) + - [Instrumentation (unstable)](#instrumentation-unstable) + - [Patch Changes](#patch-changes) + - [Unstable Changes](#unstable-changes) + - [v7.9.4](#v794) + - [What's Changed](#whats-changed-1) + - [`useRoute()` (unstable)](#useroute-unstable) + - [Patch Changes](#patch-changes-1) + - [Unstable Changes](#unstable-changes-1) + - [v7.9.3](#v793) + - [Patch Changes](#patch-changes-2) + - [v7.9.2](#v792) + - [What's Changed](#whats-changed-2) + - [RSC Framework Mode (unstable)](#rsc-framework-mode-unstable) + - [Fetcher Reset (unstable)](#fetcher-reset-unstable) + - [Patch Changes](#patch-changes-3) + - [Unstable Changes](#unstable-changes-2) + - [v7.9.1](#v791) + - [Patch Changes](#patch-changes-4) + - [v7.9.0](#v790) + - [What's Changed](#whats-changed-3) + - [Stable Middleware and Context APIs](#stable-middleware-and-context-apis) + - [Minor Changes](#minor-changes) + - [Patch Changes](#patch-changes-5) + - [Unstable Changes](#unstable-changes-3) + - [v7.8.2](#v782) + - [Patch Changes](#patch-changes-6) + - [Unstable Changes](#unstable-changes-4) + - [v7.8.1](#v781) + - [Patch Changes](#patch-changes-7) + - [Unstable Changes](#unstable-changes-5) + - [v7.8.0](#v780) + - [What's Changed](#whats-changed-4) + - [Consistently named `loaderData` values](#consistently-named-loaderdata-values) + - [Improvements/fixes to the middleware APIs (unstable)](#improvementsfixes-to-the-middleware-apis-unstable) + - [Minor Changes](#minor-changes-1) + - [Patch Changes](#patch-changes-8) + - [Unstable Changes](#unstable-changes-6) + - [Changes by Package](#changes-by-package) + - [v7.7.1](#v771) + - [Patch Changes](#patch-changes-9) + - [Unstable Changes](#unstable-changes-7) + - [v7.7.0](#v770) + - [What's Changed](#whats-changed-5) + - [Unstable RSC APIs](#unstable-rsc-apis) + - [Minor Changes](#minor-changes-2) + - [Patch Changes](#patch-changes-10) + - [Unstable Changes](#unstable-changes-8) + - [Changes by Package](#changes-by-package-1) + - [v7.6.3](#v763) + - [Patch Changes](#patch-changes-11) + - [v7.6.2](#v762) + - [Patch Changes](#patch-changes-12) + - [v7.6.1](#v761) + - [Patch Changes](#patch-changes-13) + - [Unstable Changes](#unstable-changes-9) + - [v7.6.0](#v760) + - [What's Changed](#whats-changed-6) + - [`routeDiscovery` Config Option](#routediscovery-config-option) + - [Automatic Types for Future Flags](#automatic-types-for-future-flags) + - [Minor Changes](#minor-changes-3) + - [Patch Changes](#patch-changes-14) + - [Unstable Changes](#unstable-changes-10) + - [Changes by Package](#changes-by-package-2) + - [v7.5.3](#v753) + - [Patch Changes](#patch-changes-15) + - [v7.5.2](#v752) + - [Security Notice](#security-notice) + - [Patch Changes](#patch-changes-16) + - [v7.5.1](#v751) + - [Patch Changes](#patch-changes-17) + - [Unstable Changes](#unstable-changes-11) + - [v7.5.0](#v750) + - [What's Changed](#whats-changed-7) + - [`route.lazy` Object API](#routelazy-object-api) + - [Minor Changes](#minor-changes-4) + - [Patch Changes](#patch-changes-18) + - [Unstable Changes](#unstable-changes-12) + - [Changes by Package](#changes-by-package-3) + - [v7.4.1](#v741) + - [Security Notice](#security-notice-1) + - [Patch Changes](#patch-changes-19) + - [Unstable Changes](#unstable-changes-13) + - [v7.4.0](#v740) + - [Minor Changes](#minor-changes-5) + - [Patch Changes](#patch-changes-20) + - [Unstable Changes](#unstable-changes-14) + - [Changes by Package](#changes-by-package-4) + - [v7.3.0](#v730) + - [Minor Changes](#minor-changes-6) + - [Patch Changes](#patch-changes-21) + - [Unstable Changes](#unstable-changes-15) + - [Client-side `context` (unstable)](#client-side-context-unstable) + - [Middleware (unstable)](#middleware-unstable) + - [Middleware `context` parameter](#middleware-context-parameter) + - [`unstable_SerializesTo`](#unstable_serializesto) + - [Changes by Package](#changes-by-package-5) + - [v7.2.0](#v720) + - [What's Changed](#whats-changed-8) + - [Type-safe `href` utility](#type-safe-href-utility) + - [Prerendering with a SPA Fallback](#prerendering-with-a-spa-fallback) + - [Allow a root `loader` in SPA Mode](#allow-a-root-loader-in-spa-mode) + - [Minor Changes](#minor-changes-7) + - [Patch Changes](#patch-changes-22) + - [Unstable Changes](#unstable-changes-16) + - [Split Route Modules (unstable)](#split-route-modules-unstable) + - [Changes by Package](#changes-by-package-6) + - [v7.1.5](#v715) + - [Patch Changes](#patch-changes-23) + - [v7.1.4](#v714) + - [Patch Changes](#patch-changes-24) + - [v7.1.3](#v713) + - [Patch Changes](#patch-changes-25) + - [v7.1.2](#v712) + - [Patch Changes](#patch-changes-26) + - [v7.1.1](#v711) + - [Patch Changes](#patch-changes-27) + - [v7.1.0](#v710) + - [Minor Changes](#minor-changes-8) + - [Patch Changes](#patch-changes-28) + - [Changes by Package](#changes-by-package-7) + - [v7.0.2](#v702) + - [Patch Changes](#patch-changes-29) + - [v7.0.1](#v701) + - [Patch Changes](#patch-changes-30) + - [v7.0.0](#v700) + - [Breaking Changes](#breaking-changes) + - [Package Restructuring](#package-restructuring) + - [Removed Adapter Re-exports](#removed-adapter-re-exports) + - [Removed APIs](#removed-apis) + - [Minimum Versions](#minimum-versions) + - [Adopted Future Flag Behaviors](#adopted-future-flag-behaviors) + - [Vite Compiler](#vite-compiler) + - [Exposed Router Promises](#exposed-router-promises) + - [Other Notable Changes](#other-notable-changes) + - [`routes.ts`](#routests) + - [Type-safety improvements](#type-safety-improvements) + - [Prerendering](#prerendering) + - [Major Changes (`react-router`)](#major-changes-react-router) + - [Major Changes (`@react-router/*`)](#major-changes-react-router-1) + - [Minor Changes](#minor-changes-9) + - [Patch Changes](#patch-changes-31) + - [Changes by Package](#changes-by-package-8) +- [React Router v6 Releases](#react-router-v6-releases) + - [v6.30.1](#v6301) + - [Patch Changes](#patch-changes-32) + - [v6.30.0](#v6300) + - [Minor Changes](#minor-changes-10) + - [Patch Changes](#patch-changes-33) + - [v6.29.0](#v6290) + - [Minor Changes](#minor-changes-11) + - [Patch Changes](#patch-changes-34) + - [v6.28.2](#v6282) + - [Patch Changes](#patch-changes-35) + - [v6.28.1](#v6281) + - [Patch Changes](#patch-changes-36) + - [v6.28.0](#v6280) + - [What's Changed](#whats-changed-9) + - [Minor Changes](#minor-changes-12) + - [Patch Changes](#patch-changes-37) + - [v6.27.0](#v6270) + - [What's Changed](#whats-changed-10) + - [Stabilized APIs](#stabilized-apis) + - [Minor Changes](#minor-changes-13) + - [Patch Changes](#patch-changes-38) + - [v6.26.2](#v6262) + - [Patch Changes](#patch-changes-39) + - [v6.26.1](#v6261) + - [Patch Changes](#patch-changes-40) + - [v6.26.0](#v6260) + - [Minor Changes](#minor-changes-14) + - [Patch Changes](#patch-changes-41) + - [v6.25.1](#v6251) + - [Patch Changes](#patch-changes-42) + - [v6.25.0](#v6250) + - [What's Changed](#whats-changed-11) + - [Stabilized `v7_skipActionErrorRevalidation`](#stabilized-v7_skipactionerrorrevalidation) + - [Minor Changes](#minor-changes-15) + - [Patch Changes](#patch-changes-43) + - [v6.24.1](#v6241) + - [Patch Changes](#patch-changes-44) + - [v6.24.0](#v6240) + - [What's Changed](#whats-changed-12) + - [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war) + - [Minor Changes](#minor-changes-16) + - [Patch Changes](#patch-changes-45) + - [v6.23.1](#v6231) + - [Patch Changes](#patch-changes-46) + - [v6.23.0](#v6230) + - [What's Changed](#whats-changed-13) + - [Data Strategy (unstable)](#data-strategy-unstable) + - [Skip Action Error Revalidation (unstable)](#skip-action-error-revalidation-unstable) + - [Minor Changes](#minor-changes-17) + - [v6.22.3](#v6223) + - [Patch Changes](#patch-changes-47) + - [v6.22.2](#v6222) + - [Patch Changes](#patch-changes-48) + - [v6.22.1](#v6221) + - [Patch Changes](#patch-changes-49) + - [v6.22.0](#v6220) + - [What's Changed](#whats-changed-14) + - [Core Web Vitals Technology Report Flag](#core-web-vitals-technology-report-flag) + - [Minor Changes](#minor-changes-18) + - [Patch Changes](#patch-changes-50) + - [v6.21.3](#v6213) + - [Patch Changes](#patch-changes-51) + - [v6.21.2](#v6212) + - [Patch Changes](#patch-changes-52) + - [v6.21.1](#v6211) + - [Patch Changes](#patch-changes-53) + - [v6.21.0](#v6210) + - [What's Changed](#whats-changed-15) + - [`future.v7_relativeSplatPath`](#futurev7_relativesplatpath) + - [Partial Hydration](#partial-hydration) + - [Minor Changes](#minor-changes-19) + - [Patch Changes](#patch-changes-54) + - [v6.20.1](#v6201) + - [Patch Changes](#patch-changes-55) + - [v6.20.0](#v6200) + - [Minor Changes](#minor-changes-20) + - [Patch Changes](#patch-changes-56) + - [v6.19.0](#v6190) + - [What's Changed](#whats-changed-16) + - [`unstable_flushSync` API](#unstable_flushsync-api) + - [Minor Changes](#minor-changes-21) + - [Patch Changes](#patch-changes-57) + - [v6.18.0](#v6180) + - [What's Changed](#whats-changed-17) + - [New Fetcher APIs](#new-fetcher-apis) + - [Persistence Future Flag (`future.v7_fetcherPersist`)](#persistence-future-flag-futurev7_fetcherpersist) + - [Minor Changes](#minor-changes-22) + - [Patch Changes](#patch-changes-58) + - [v6.17.0](#v6170) + - [What's Changed](#whats-changed-18) + - [View Transitions ๐Ÿš€](#view-transitions-) + - [Minor Changes](#minor-changes-23) + - [Patch Changes](#patch-changes-59) + - [v6.16.0](#v6160) + - [Minor Changes](#minor-changes-24) + - [Patch Changes](#patch-changes-60) + - [v6.15.0](#v6150) + - [Minor Changes](#minor-changes-25) + - [Patch Changes](#patch-changes-61) + - [v6.14.2](#v6142) + - [Patch Changes](#patch-changes-62) + - [v6.14.1](#v6141) + - [Patch Changes](#patch-changes-63) + - [v6.14.0](#v6140) + - [What's Changed](#whats-changed-19) + - [JSON/Text Submissions](#jsontext-submissions) + - [Minor Changes](#minor-changes-26) + - [Patch Changes](#patch-changes-64) + - [v6.13.0](#v6130) + - [What's Changed](#whats-changed-20) + - [`future.v7_startTransition`](#futurev7_starttransition) + - [Minor Changes](#minor-changes-27) + - [Patch Changes](#patch-changes-65) + - [v6.12.1](#v6121) + - [Patch Changes](#patch-changes-66) + - [v6.12.0](#v6120) + - [What's Changed](#whats-changed-21) + - [`React.startTransition` support](#reactstarttransition-support) + - [Minor Changes](#minor-changes-28) + - [Patch Changes](#patch-changes-67) + - [v6.11.2](#v6112) + - [Patch Changes](#patch-changes-68) + - [v6.11.1](#v6111) + - [Patch Changes](#patch-changes-69) + - [v6.11.0](#v6110) + - [Minor Changes](#minor-changes-29) + - [Patch Changes](#patch-changes-70) + - [v6.10.0](#v6100) + - [What's Changed](#whats-changed-22) + - [Minor Changes](#minor-changes-30) + - [`future.v7_normalizeFormMethod`](#futurev7_normalizeformmethod) + - [Patch Changes](#patch-changes-71) + - [v6.9.0](#v690) + - [What's Changed](#whats-changed-23) + - [`Component`/`ErrorBoundary` route properties](#componenterrorboundary-route-properties) + - [Introducing Lazy Route Modules](#introducing-lazy-route-modules) + - [Minor Changes](#minor-changes-31) + - [Patch Changes](#patch-changes-72) + - [v6.8.2](#v682) + - [Patch Changes](#patch-changes-73) + - [v6.8.1](#v681) + - [Patch Changes](#patch-changes-74) + - [v6.8.0](#v680) + - [Minor Changes](#minor-changes-32) + - [Patch Changes](#patch-changes-75) + - [v6.7.0](#v670) + - [Minor Changes](#minor-changes-33) + - [Patch Changes](#patch-changes-76) + - [v6.6.2](#v662) + - [Patch Changes](#patch-changes-77) + - [v6.6.1](#v661) + - [Patch Changes](#patch-changes-78) + - [v6.6.0](#v660) + - [What's Changed](#whats-changed-24) + - [Minor Changes](#minor-changes-34) + - [Patch Changes](#patch-changes-79) + - [v6.5.0](#v650) + - [What's Changed](#whats-changed-25) + - [Minor Changes](#minor-changes-35) + - [Patch Changes](#patch-changes-80) + - [v6.4.5](#v645) + - [Patch Changes](#patch-changes-81) + - [v6.4.4](#v644) + - [Patch Changes](#patch-changes-82) + - [v6.4.3](#v643) + - [Patch Changes](#patch-changes-83) + - [v6.4.2](#v642) + - [Patch Changes](#patch-changes-84) + - [v6.4.1](#v641) + - [Patch Changes](#patch-changes-85) + - [v6.4.0](#v640) + - [What's Changed](#whats-changed-26) + - [Remix Data APIs](#remix-data-apis) + - [Patch Changes](#patch-changes-86) + - [v6.3.0](#v630) + - [Minor Changes](#minor-changes-36) + - [v6.2.2](#v622) + - [Patch Changes](#patch-changes-87) + - [v6.2.1](#v621) + - [Patch Changes](#patch-changes-88) + - [v6.2.0](#v620) + - [Minor Changes](#minor-changes-37) + - [Patch Changes](#patch-changes-89) + - [v6.1.1](#v611) + - [Patch Changes](#patch-changes-90) + - [v6.1.0](#v610) + - [Minor Changes](#minor-changes-38) + - [Patch Changes](#patch-changes-91) + - [v6.0.2](#v602) + - [Patch Changes](#patch-changes-92) + - [v6.0.1](#v601) + - [Patch Changes](#patch-changes-93) + - [v6.0.0](#v600) -v0.8.0 - Sat, 04 Oct 2014 05:39:02 GMT --------------------------------------- +
-- [d2aa7cb](../../commit/d2aa7cb) [added] `` -- [637c0ac](../../commit/637c0ac) [added] `` -- [f2bf4bd](../../commit/f2bf4bd) [removed] RouteStore -- [f2bf4bd](../../commit/f2bf4bd) [added] Router.PathState for keeping track of the current URL path -- [f2bf4bd](../../commit/f2bf4bd) [added] Router.RouteLookup for looking up routes -- [f2bf4bd](../../commit/f2bf4bd) [added] Router.Transitions for transitioning to other routes -- [f2bf4bd](../../commit/f2bf4bd) [added] Pluggable scroll behaviors -- [f2bf4bd](../../commit/f2bf4bd) [changed] `` => `` -- [f2bf4bd](../../commit/f2bf4bd) [removed] `` -- [f2bf4bd](../../commit/f2bf4bd) [removed] Router.transitionTo, Router.replaceWith, Router.goBack -- [97dbf2d](../../commit/97dbf2d) [added] transition.wait(promise) -- [3787179](../../commit/3787179) [changed] Transition retry now uses replaceWith. -- [e0b708f](../../commit/e0b708f) [added] Ability to transitionTo absolute URLs -- [c1493b5](../../commit/c1493b5) [changed] #259 support dots in named params -- [a4ce7c8](../../commit/a4ce7c8) [changed] isActive is an instance method -- [a4ce7c8](../../commit/a4ce7c8) [removed] `` + -- [273625a](../../commit/273625a) [fixed] Active state on ``s with key prop -- [283d3f6](../../commit/283d3f6) [added] RouteStore#registerChildren -- [a030648](../../commit/a030648) [changed] Relaxed MemoryStore invariant -- [e028768](../../commit/e028768) [added] `` component -- [6878120](../../commit/6878120) [added] onAbortedTransition, onActiveStateChange, onTransitionError Routes props -- [58073ca](../../commit/58073ca) [changed] Transition#cancelReason => abortReason -- [6d1ae95](../../commit/6d1ae95) [fixed] sibling array route configs -- [0e7a182](../../commit/0e7a182) [added] pluggable history implementations closes #166 -- [ca96f86](../../commit/ca96f86) [fixed] typo in Link -- [f3dc513](../../commit/f3dc513) [added] onClick handler to `` -- [b9f92f9](../../commit/b9f92f9) [changed] updated rf-changelog +## v7.9.5 +Date: 2025-10-29 -v0.5.2 - Thu, 07 Aug 2014 18:25:47 GMT --------------------------------------- +### What's Changed -- [21f4f57](../../commit/21f4f57) [added] preserveScrollPosition Route/Routes props -- [f3b4de8](../../commit/f3b4de8) [added] support for extra props in Links, fixes #170 -- [829a9ec](../../commit/829a9ec) [added] `` component -- [0a49665](../../commit/0a49665) [added] Router.makeHref -- [2100b8c](../../commit/2100b8c) [changed] handlers receive route name -- [154afba](../../commit/154afba) [changed] location of public modules +#### Instrumentation (unstable) +This release adds new `unstable_instrumentation` APIs that will allow you to add runtime instrumentation logic to various aspects of your application (server handler, client navigations/fetches, loaders, actions, middleware, `route.lazy`). For more information, please see the [docs](https://reactrouter.com/7.9.5/how-to/instrumentation). -v0.5.1 - Mon, 04 Aug 2014 22:16:38 GMT --------------------------------------- +### Patch Changes -- [08f5a69](../../commit/08f5a69) [fixed] location="history" fallback -- [87b1c2a](../../commit/87b1c2a) [fixed] Navigation to root URL can fail -- [760f021](../../commit/760f021) [fixed] infinite loop in RouteStore.unregisterRoute -- [5fea685](../../commit/5fea685) [added] Router.AsyncState mixin -- [395a590](../../commit/395a590) [changed] fallback to window.location for history -- [2a3582e](../../commit/2a3582e) [changed] make URLStore.push idempotent -- [4c4f87b](../../commit/4c4f87b) [fixed] alt click on Link should count as modified -- [97c02f1](../../commit/97c02f1) [fixed] middle click on `` +- `react-router` - Ensure action handlers run for routes with middleware even if no loader is present ([#14443](https://github.com/remix-run/react-router/pull/14443)) +- `@react-router/dev` - Ensure route navigation doesn't remove CSS `link` elements used by dynamic imports ([#14463](https://github.com/remix-run/react-router/pull/14463)) +- `@react-router/dev` - Typegen: only register route module types for routes within the app directory ([#14439](https://github.com/remix-run/react-router/pull/14439)) +### Unstable Changes -v0.5.0 - Sat, 26 Jul 2014 22:38:36 GMT --------------------------------------- +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ -- [5af49d4](../../commit/5af49d4) [changed] Split `` component from `` +- `react-router` - Move `unstable_RSCHydratedRouter` and utils to `react-router/dom` export ([#14457](https://github.com/remix-run/react-router/pull/14457)) +- `react-router` - Add a type-safe `handle` field to `unstable_useRoute()` ([#14462](https://github.com/remix-run/react-router/pull/14462)) + For example: -v0.4.2 - Sat, 26 Jul 2014 18:23:43 GMT --------------------------------------- + ```ts + // app/routes/admin.tsx + const handle = { hello: "world" }; + ``` -- [2fc9976](../../commit/2fc9976) [fixed] eslint cleanup; trailing comma fix for IE -- [b8018b1](../../commit/b8018b1) [added] animation example + ```ts + // app/routes/some-other-route.tsx + export default function Component() { + const admin = useRoute("routes/admin"); + if (!admin) throw new Error("Not nested within 'routes/admin'"); + console.log(admin.handle); + // ^? { hello: string } + } + ``` +- `react-router` - Add `unstable_instrumentations` API to allow users to add observability to their apps by instrumenting route loaders, actions, middlewares, lazy, as well as server-side request handlers and client side navigations/fetches ([#14412](https://github.com/remix-run/react-router/pull/14412)) + - Framework Mode: + - `entry.server.tsx`: `export const unstable_instrumentations = [...]` + - `entry.client.tsx`: `` + - Data Mode + - `createBrowserRouter(routes, { unstable_instrumentations: [...] })` +- `react-router` - Add a new `unstable_pattern` parameter to loaders/actions/middleware which contains the un-interpolated route pattern (i.e., `/blog/:slug`) which is useful for aggregating logs/metrics by route in instrumentation code ([#14412](https://github.com/remix-run/react-router/pull/14412)) +- `@react-router/dev` - Introduce a `prerender.unstable_concurrency` option, to support running the pre-rendering concurrently, potentially speeding up the build ([#14380](https://github.com/remix-run/react-router/pull/14380)) -v0.4.1 - Thu, 24 Jul 2014 21:35:07 GMT --------------------------------------- +**Full Changelog**: [`v7.9.4...v7.9.5`](https://github.com/remix-run/react-router/compare/react-router@7.9.4...react-router@7.9.5) -- [8152d67](../../commit/8152d67) [changed] repo location to rackt/react-router -- [0ac4dea](../../commit/0ac4dea) [removed] Dependency on react/lib/emptyFunction +## v7.9.4 +Date: 2025-10-08 -v0.4.0 - Thu, 24 Jul 2014 19:41:04 GMT --------------------------------------- +### What's Changed -- [0be4bf7](../../commit/0be4bf7) [changed] npm registry name to react-router :D +#### `useRoute()` (unstable) +This release includes a new `unstable_useRoute()` hook that provides a type-safe way to access route `loaderData`/`actionData` from a specific route in Framework Mode. Think if it like a better version of `useRouteLoaderData` that works with the typegen system and also supports `actionData`. Check out the changelog entry below for more information. -v0.3.5 - Wed, 23 Jul 2014 14:52:30 GMT --------------------------------------- +### Patch Changes -- [0a7298c](../../commit/0a7298c) [removed] browserify.transforms from package.json -- [ebf54ab](../../commit/ebf54ab) [removed] Dependency on react/lib/merge +- `@react-router/dev` - Update `valibot` dependency to `^1.1.0` ([#14379](https://github.com/remix-run/react-router/pull/14379)) +- `@react-router/node` - Validate format of incoming session ids in `createFileSessionStorage` ([#14426](https://github.com/remix-run/react-router/pull/14426)) +### Unstable Changes -v0.3.4 - Tue, 22 Jul 2014 21:02:48 GMT --------------------------------------- +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ -- [2598837](../../commit/2598837) [fixed] bower build -- [8c428ff](../../commit/8c428ff) [fixed] dist min build +- `react-router` - handle external redirects in from server actions ([#14400](https://github.com/remix-run/react-router/pull/14400)) +- `react-router` - New (unstable) `useRoute` hook for accessing data from specific routes ([#14407](https://github.com/remix-run/react-router/pull/14407)) + For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.` -v0.3.3 - Tue, 22 Jul 2014 20:46:57 GMT --------------------------------------- + ```tsx + // app/routes/admin.tsx + import { Outlet } from "react-router"; -- [92b9077](../../commit/92b9077) [changed] file name of dist builds + export const loader = () => ({ message: "Hello, loader!" }); + export const action = () => ({ count: 1 }); -v0.3.2 - Tue, 22 Jul 2014 19:47:41 GMT --------------------------------------- + export default function Component() { + return ( +
+ {/* ... */} + + {/* ... */} +
+ ); + } + ``` -- [3a4732e](../../commit/3a4732e) [changed] global export to ReactRouter + You might even want to create a reusable widget that all of the routes nested under `admin` could use: + ```tsx + import { unstable_useRoute as useRoute } from "react-router"; -v0.3.1 - Tue, 22 Jul 2014 19:40:14 GMT --------------------------------------- + export function AdminWidget() { + // How to get `message` and `count` from `admin` route? + } + ``` -- [baf2257](../../commit/baf2257) [fixed] dist files + In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in: + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/dmin"); + // ^^^^^^^^^^^ + } + ``` -v0.3.0 - Tue, 22 Jul 2014 19:34:11 GMT --------------------------------------- + `useRoute` returns `undefined` if the route is not part of the current page: -- [e827870](../../commit/e827870) [added] bower support -- [58e7b98](../../commit/58e7b98) [changed] activeRoute -> activeRouteHandler -- [0177cdd](../../commit/0177cdd) [fixed] Pass the correct component instance to willTransitionFrom hooks -- [3b590e0](../../commit/3b590e0) [changed] Upgrade to React 0.11.0 -- [51e1be2](../../commit/51e1be2) [fixed] Use peerDeps -- [a8df2f0](../../commit/a8df2f0) [added] Browser builds for version 0.2.1 -- [bb066b8](../../commit/bb066b8) [added] Browser build script -- [baf79b6](../../commit/baf79b6) [fixed] Avoid some warnings -- [8d30552](../../commit/8d30552) [changed] README to make use of activeRoute clearer in JSX. -- [991dede](../../commit/991dede) [changed] activeRoute is a function that returns null when no child routes are active. -- [73570ed](../../commit/73570ed) [changed] activeRoute can render with props and children. -- [8562482](../../commit/8562482) [added] ActiveState mixin -- [616f8bf](../../commit/616f8bf) [changed] Preserve forward slashes in URL params -- [6c74c69](../../commit/6c74c69) [changed] Combine URL helpers into URL module + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + } + ``` + Note: the `root` route is the exception since it is guaranteed to be part of the current page. + As a result, `useRoute` never returns `undefined` for `root`. -v0.2.1 - Mon, 14 Jul 2014 17:31:21 GMT --------------------------------------- + `loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error: -- [0f86654](../../commit/0f86654) [fixed] checks for class instead of components -- [a3d6e2a](../../commit/a3d6e2a) [changed] Render empty div before transition hooks -- [f474ab1](../../commit/f474ab1) [changed] '.' is no longer a path delimeter -- [f3dcdd7](../../commit/f3dcdd7) [fixed] injectParams invariant should not throw on values that coerce to false. -- [468bf3b](../../commit/468bf3b) [changed] Deprecate Router interface -- [31d1a6e](../../commit/31d1a6e) [added] renderComponentToString() + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + const { loaderData, actionData } = admin; + console.log(loaderData); + // ^? { message: string } | undefined + console.log(actionData); + // ^? { count: number } | undefined + } + ``` + If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments: -v0.2.0 - Tue, 24 Jun 2014 04:59:24 GMT --------------------------------------- + ```tsx + export function AdminWidget() { + const currentRoute = useRoute(); + currentRoute.loaderData; + currentRoute.actionData; + } + ``` -- [468bf3b](../../commit/468bf3b) [changed] Deprecate Router interface -- [31d1a6e](../../commit/31d1a6e) [added] renderComponentToString() + This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`. + Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route. + As a result, `loaderData` and `actionData` are typed as `unknown`. + If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`: -v0.1.0 - Thu, 19 Jun 2014 19:11:38 GMT --------------------------------------- + ```tsx + export function AdminWidget({ + message, + count, + }: { + message: string; + count: number; + }) { + /* ... */ + } + ``` +**Full Changelog**: [`v7.9.3...v7.9.4`](https://github.com/remix-run/react-router/compare/react-router@7.9.3...react-router@7.9.4) +## v7.9.3 + +Date: 2025-09-26 + +### Patch Changes + +- `react-router` - Fix Data Mode regression causing a 404 during initial load in when `middleware` exists without any `loader` functions ([#14393](https://github.com/remix-run/react-router/pull/14393)) +- `react-router` - Do not try to use `turbo-stream` to decode CDN errors that never reached the server ([#14385](https://github.com/remix-run/react-router/pull/14385)) + - This was logic we used to have in Remix v2 that got lost in the adoption of Single Fetch + - This permits the actual CDN error to bubble to the `ErrorBoundary` instead of a generic _"Unable to decode turbo-stream response"_ error + +**Full Changelog**: [`v7.9.2...v7.9.3`](https://github.com/remix-run/react-router/compare/react-router@7.9.2...react-router@7.9.3) + +## v7.9.2 + +Date: 2025-09-24 + +### What's Changed + +This release contains a handful of bug fixes, but we think you'll be most excited about the new unstable stuff ๐Ÿ˜‰. + +#### RSC Framework Mode (unstable) + +This release includes our first release of unstable support for RSC in Framework Mode! You can read more about it in our [blog post](https://remix.run/blog/rsc-framework-mode-preview) and the [docs](https://reactrouter.com/how-to/react-server-components#rsc-framework-mode). + +#### Fetcher Reset (unstable) + +This release also includes a new (long-requested) `fetcher.unstable_reset()` API to reset fetchers back to their initial `idle` state. + +### Patch Changes + +- `react-router` - Ensure client-side router runs client `middleware` during initialization data load (if required) even if no loaders exist ([#14348](https://github.com/remix-run/react-router/pull/14348)) +- `react-router` - Fix `middleware` prop not being supported on `` when used with a data router via `createRoutesFromElements` ([#14357](https://github.com/remix-run/react-router/pull/14357)) +- `react-router` - Update `createRoutesStub` to work with `middleware` ([#14348](https://github.com/remix-run/react-router/pull/14348)) + - You will need to set the `` flag to enable the proper `context` type +- `react-router` - Update Lazy Route Discovery manifest requests to use a singular comma-separated `paths` query param instead of repeated `p` query params ([#14321](https://github.com/remix-run/react-router/pull/14321)) + - This is because Cloudflare has a hard limit of 100 URL search param key/value pairs when used as a key for caching purposes + - If more that 100 paths were included, the cache key would be incomplete and could produce false-positive cache hits +- `react-router` - Fail gracefully on manifest version mismatch logic if `sessionStorage` access is blocked ([#14335](https://github.com/remix-run/react-router/pull/14335)) +- `react-router` - Update `useOutlet` returned element to have a stable identity in-between route changes ([#13382](https://github.com/remix-run/react-router/pull/13382)) +- `react-router` - Handle encoded question mark and hash characters in ancestor splat routes ([#14249](https://github.com/remix-run/react-router/pull/14249)) +- `@react-router/dev` - Switch internal vite plugin Response logic to use `@remix-run/node-fetch-server` ([#13927](https://github.com/remix-run/react-router/pull/13927)) +- `@react-router/dev` - Fix `presets` `future` flags being ignored during config resolution ([#14369](https://github.com/remix-run/react-router/pull/14369)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - Add `fetcher.unstable_reset()` API ([#14206](https://github.com/remix-run/react-router/pull/14206)) +- `react-router` - In RSC Data Mode, handle SSR'd client errors and re-try in the browser ([#14342](https://github.com/remix-run/react-router/pull/14342)) +- `react-router` - Enable full transition support for the RSC router ([#14362](https://github.com/remix-run/react-router/pull/14362)) +- `@react-router/dev` - Add unstable support for RSC Framework Mode ([#14336](https://github.com/remix-run/react-router/pull/14336)) +- `@react-router/serve` - Disable `compression()` middleware in RSC framework mode ([#14381](https://github.com/remix-run/react-router/pull/14381)) + +**Full Changelog**: [`v7.9.1...v7.9.2`](https://github.com/remix-run/react-router/compare/react-router@7.9.1...react-router@7.9.2) + +## v7.9.1 + +Date: 2025-09-12 + +### Patch Changes + +- Fix internal `Future` interface naming from `middleware` -> `v8_middleware` ([#14327](https://github.com/remix-run/react-router/pull/14327)) + +**Full Changelog**: [`v7.9.0...v7.9.1`](https://github.com/remix-run/react-router/compare/react-router@7.9.0...react-router@7.9.1) + +## v7.9.0 + +Date: 2025-09-12 + +### What's Changed + +#### Stable Middleware and Context APIs + +We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: + +- [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) +- [`createContext`](https://reactrouter.com/api/utils/createContext) +- `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option +- `` [`getContext`](https://reactrouter.com/api/framework-routers/HydratedRouter#getcontext) prop + +Please see the [Middleware Docs](https://reactrouter.com/how-to/middleware), the [Middleware RFC](https://github.com/remix-run/remix/discussions/7642), and the [Client-side Context RFC](https://github.com/remix-run/react-router/discussions/9856) for more information. + +### Minor Changes + +- Stabilize middleware and context APIs ([#14215](https://github.com/remix-run/react-router/pull/14215)) + +### Patch Changes + +- `react-router` - Update `href()` to correctly process routes that have an extension after the parameter or are a single optional parameter ([#13797](https://github.com/remix-run/react-router/pull/13797)) +- `react-router` - Escape HTML in `meta()` JSON-LD content ([#14316](https://github.com/remix-run/react-router/pull/14316)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - RSC: Add react-server `Await` component implementation ([#14261](https://github.com/remix-run/react-router/pull/14261)) +- `react-router` - RSC: Fix hydration errors for routes that only have client loaders when using RSC in Data Mode along with a custom basename ([#14264](https://github.com/remix-run/react-router/pull/14264)) +- `react-router` - RSC: Make `href` function available in a `react-server` context ([#14262](https://github.com/remix-run/react-router/pull/14262)) +- `react-router` - RSC: Decode each time `getPayload()` is called to allow for "in-context" decoding and hoisting of contextual assets ([#14248](https://github.com/remix-run/react-router/pull/14248)) + +**Full Changelog**: [`v7.8.2...v7.9.0`](https://github.com/remix-run/react-router/compare/react-router@7.8.2...react-router@7.9.0) + +## v7.8.2 + +Date: 2025-08-22 + +### Patch Changes + +- `react-router` - Maintain `ReadonlyMap` and `ReadonlySet` types in server response data. ([#13092](https://github.com/remix-run/react-router/pull/13092)) +- `react-router` - Fix `basename` usage without a leading slash in data routers ([#11671](https://github.com/remix-run/react-router/pull/11671)) +- `react-router` - Fix `TypeError` if you throw from `patchRoutesOnNavigation` when no partial matches exist ([#14198](https://github.com/remix-run/react-router/pull/14198)) +- `react-router` - Properly escape interpolated param values in `generatePath()` ([#13530](https://github.com/remix-run/react-router/pull/13530)) +- `@react-router/dev` - Fix potential memory leak in default `entry.server` ([#14200](https://github.com/remix-run/react-router/pull/14200)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +**Client-side `onError`** + +- `react-router` - Add ``/`` prop for client side error reporting ([#14162](https://github.com/remix-run/react-router/pull/14162)) + +**Middleware** + +- `react-router` - Delay serialization of `.data` redirects to 202 responses until after middleware chain ([#14205](https://github.com/remix-run/react-router/pull/14205)) +- `react-router` - Update client middleware so it returns the `dataStrategy` results up the chain allowing for more advanced post-processing middleware ([#14151](https://github.com/remix-run/react-router/pull/14151), [#14212](https://github.com/remix-run/react-router/pull/14212)) +- `react-router` - Remove Data Mode `future.unstable_middleware` flag from `createBrowserRouter` ([#14213](https://github.com/remix-run/react-router/pull/14213)) + - This is only needed as a Framework Mode flag because of the route modules and the `getLoadContext` type behavior change + - In Data Mode, it's an opt-in feature because it's just a new property on a route object, so there's no behavior changes that necessitate a flag + +**RSC** + +- `react-router` - Allow opting out of revalidation on server actions with hidden `$SKIP_REVALIDATION` input ([#14154](https://github.com/remix-run/react-router/pull/14154)) + +**Full Changelog**: [`v7.8.1...v7.8.2`](https://github.com/remix-run/react-router/compare/react-router@7.8.1...react-router@7.8.2) + +## v7.8.1 + +Date: 2025-08-15 + +### Patch Changes + +- `react-router` - Fix usage of optional path segments in nested routes defined using absolute paths ([#14135](https://github.com/remix-run/react-router/pull/14135)) +- `react-router` - Fix optional static segment matching in `matchPath` ([#11813](https://github.com/remix-run/react-router/pull/11813)) +- `react-router` - Fix pre-rendering when a `basename` is set with `ssr:false` ([#13791](https://github.com/remix-run/react-router/pull/13791)) +- `react-router` - Properly convert returned/thrown `data()` values to `Response` instances via `Response.json()` in resource routes and middleware ([#14159](https://github.com/remix-run/react-router/pull/14159), [#14181](https://github.com/remix-run/react-router/pull/14181)) +- `@react-router/dev` - Update generated `Route.MetaArgs` type so `loaderData` is only potentially undefined when an `ErrorBoundary` export is present ([#14173](https://github.com/remix-run/react-router/pull/14173)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +**Middleware** + +- `react-router` - Bubble client pre-`next` middleware errors to the shallowest ancestor that needs to load, not strictly the shallowest ancestor with a loader ([#14150](https://github.com/remix-run/react-router/pull/14150)) +- `react-router` - Propagate non-redirect `Response` values thrown from middleware to the error boundary on document/data requests ([#14182](https://github.com/remix-run/react-router/pull/14182)) + +**RSC** + +- `react-router` - Provide `isRouteErrorResponse` utility in `react-server` environments ([#14166](https://github.com/remix-run/react-router/pull/14166)) +- `react-router` - Handle `meta` and `links` Route Exports in RSC Data Mode ([#14136](https://github.com/remix-run/react-router/pull/14136)) + +**Full Changelog**: [`v7.8.0...v7.8.1`](https://github.com/remix-run/react-router/compare/react-router@7.8.0...react-router@7.8.1) + +## v7.8.0 + +Date: 2025-08-07 + +### What's Changed + +#### Consistently named `loaderData` values + +Ever noticed the discrepancies in loader data values handed to you by the framework? Like, we call it `loaderData` in your component props, but then `match.data` in your matches? Yeah, us too - as well as some keen-eyed React Router users who raised this in a proposal. We've added new `loaderData` fields alongside existing `data` fields in a few lingering spots to align with the `loaderData` naming used in the new `Route.*` APIs. + +#### Improvements/fixes to the middleware APIs (unstable) + +The biggest set of changes in `7.8.0` are to the `unstable_middleware` API's as we move closer to stabilizing them. If you've adopted the middleware APIs for early testing, please read the middleware changes below carefully. We hope to stabilize these soon so please let us know of any feedback you have on the API's in their current state! + +### Minor Changes + +- `react-router` - Add `nonce` prop to `Links` & `PrefetchPageLinks` ([#14048](https://github.com/remix-run/react-router/pull/14048)) +- `react-router` - Add `loaderData` arguments/properties alongside existing `data` arguments/properties to provide consistency and clarity between `loaderData` and `actionData` across the board ([#14047](https://github.com/remix-run/react-router/pull/14047)) + - Updated types: `Route.MetaArgs`, `Route.MetaMatch`, `MetaArgs`, `MetaMatch`, `Route.ComponentProps.matches`, `UIMatch` + - `@deprecated` warnings have been added to the existing `data` properties to point users to new `loaderData` properties, in preparation for removing the `data` properties in a future major release + +### Patch Changes + +- `react-router` - Prevent _"Did not find corresponding fetcher result"_ console error when navigating during a `fetcher.submit` revalidation ([#14114](https://github.com/remix-run/react-router/pull/14114)) +- `react-router` - Switch Lazy Route Discovery manifest URL generation to use a standalone `URLSearchParams` instance instead of `URL.searchParams` to avoid a major performance bottleneck in Chrome ([#14084](https://github.com/remix-run/react-router/pull/14084)) +- `react-router` - Adjust internal RSC usage of `React.use` to avoid Webpack compilation errors when using React 18 ([#14113](https://github.com/remix-run/react-router/pull/14113)) +- `react-router` - Remove dependency on `@types/node` in TypeScript declaration files ([#14059](https://github.com/remix-run/react-router/pull/14059)) +- `react-router` - Fix types for `UIMatch` to reflect that the `loaderData`/`data` properties may be `undefined` ([#12206](https://github.com/remix-run/react-router/pull/12206)) + - When an `ErrorBoundary` is being rendered, not all active matches will have loader data available, since it may have been their `loader` that threw to trigger the boundary + - The `UIMatch.data` type was not correctly handing this and would always reflect the presence of data, leading to the unexpected runtime errors when an `ErrorBoundary` was rendered + - โš ๏ธ This may cause some type errors to show up in your code for unguarded `match.data` accesses - you should properly guard for `undefined` values in those scenarios. + + ```tsx + // app/root.tsx + export function loader() { + someFunctionThatThrows(); // โŒ Throws an Error + return { title: "My Title" }; + } + + export function Layout({ children }: { children: React.ReactNode }) { + let matches = useMatches(); + let rootMatch = matches[0] as UIMatch>>; + // ^ rootMatch.data is currently incorrectly typed here, so TypeScript does + // not complain if you do the following which throws an error at runtime: + let { title } = rootMatch.data; // ๐Ÿ’ฅ + + return ...; + } + ``` + +- `@react-router/dev` - Fix rename without mkdir in Vite plugin ([#14105](https://github.com/remix-run/react-router/pull/14105)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +**RSC** + +- `react-router` - Fix Data Mode issue where routes that return `false` from `shouldRevalidate` would be replaced by an `` ([#14071](https://github.com/remix-run/react-router/pull/14071)) +- `react-router` - Proxy server action side-effect redirects from actions for document and `callServer` requests ([#14131](https://github.com/remix-run/react-router/pull/14131)) + +**Middleware** + +- `react-router` - Change the `unstable_getContext` signature on `RouterProvider`, `HydratedRouter`, and `unstable_RSCHydratedRouter` so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to construct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097)) + - See the [docs](https://reactrouter.com/api/data-routers/createBrowserRouter#optsunstable_getcontext) for more information + - โš ๏ธ This is a breaking change if you have adopted the `unstable_getContext` prop +- `react-router` - Run client middleware on client navigations even if no loaders exist ([#14106](https://github.com/remix-run/react-router/pull/14106)) +- `react-router` - Convert internal middleware implementations to use the new `unstable_generateMiddlewareResponse` API ([#14103](https://github.com/remix-run/react-router/pull/14103)) +- `react-router` - Ensure resource route errors go through `handleError` w/middleware enabled ([#14078](https://github.com/remix-run/react-router/pull/14078)) +- `react-router` - Propagate returned `Response` from server middleware if `next` wasn't called ([#14093](https://github.com/remix-run/react-router/pull/14093)) +- `react-router` - Allow server middlewares to return `data()` values which will be converted into a `Response` ([#14093](https://github.com/remix-run/react-router/pull/14093), [#14128](https://github.com/remix-run/react-router/pull/14128)) +- `react-router` - Update middleware error handling so that the `next` function never throws and instead handles any middleware errors at the proper `ErrorBoundary` and returns the `Response` up through the ancestor `next` function ([#14118](https://github.com/remix-run/react-router/pull/14118)) + - See the [error handling docs](https://reactrouter.com/how-to/middleware#next-and-error-handling) for more information + - โš ๏ธ This changes existing functionality so if you are currently wrapping `next` calls in `try`/`catch` you should be able to remove those +- `react-router` - Bubble client-side middleware errors prior to `next` to the appropriate ancestor error boundary ([#14138](https://github.com/remix-run/react-router/pull/14138)) +- `react-router` - When middleware is enabled, make the `context` parameter read-only (`Readonly`) so that TypeScript will not allow you to write arbitrary fields to it in loaders, actions, or middleware. ([#14097](https://github.com/remix-run/react-router/pull/14097)) +- `react-router` - Rename and alter the signature/functionality of the `unstable_respond` API in `staticHandler.query`/`staticHandler.queryRoute` ([#14103](https://github.com/remix-run/react-router/pull/14103)) + - This only impacts users using `createStaticHandler()` for manual data loading during non-Framework Mode SSR + - The API has been renamed to `unstable_generateMiddlewareResponse` for clarity + - The main functional change is that instead of running the loaders/actions before calling `unstable_respond` and handing you the result, we now pass a `query`/`queryRoute` function as a parameter and you execute the loaders/actions inside your callback, giving you full access to pre-processing and error handling + - The `query` version of the API now has a signature of `(query: (r: Request) => Promise) => Promise` + - The `queryRoute` version of the API now has a signature of `(queryRoute: (r: Request) => Promise) => Promise` + - This allows for more advanced usages such as running logic before/after calling `query` and direct error handling of errors thrown from query + - โš ๏ธ This is a breaking change if you've adopted the `staticHandler` `unstable_respond` API + + ```tsx + let response = await staticHandler.query(request, { + requestContext: new unstable_RouterContextProvider(), + async unstable_generateMiddlewareResponse(query) { + try { + // At this point we've run middleware top-down so we need to call the + // handlers and generate the Response to bubble back up the middleware + let result = await query(request); + if (isResponse(result)) { + return result; // Redirects, etc. + } + return await generateHtmlResponse(result); + } catch (error: unknown) { + return generateErrorResponse(error); + } + }, + }); + ``` + +- `@react-router/{architect,cloudflare,express,node}` - Change the `getLoadContext` signature (`type GetLoadContextFunction`) when `future.unstable_middleware` is enabled so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to construct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097)) + - This also removes the `type unstable_InitialContext` export + - See the [middleware `getLoadContext` docs](https://reactrouter.com/how-to/middleware#changes-to-getloadcontextapploadcontext) for more information + - โš ๏ธ This is a breaking change if you have adopted middleware and are using a custom server with a `getLoadContext` function + +### Changes by Package + +- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/create-react-router/CHANGELOG.md#780) +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router/CHANGELOG.md#780) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-architect/CHANGELOG.md#780) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-cloudflare/CHANGELOG.md#780) +- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-dev/CHANGELOG.md#780) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-express/CHANGELOG.md#780) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-fs-routes/CHANGELOG.md#780) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-node/CHANGELOG.md#780) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#780) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-serve/CHANGELOG.md#780) + +**Full Changelog**: [`v7.7.1...v7.8.0`](https://github.com/remix-run/react-router/compare/react-router@7.7.1...react-router@7.8.0) + +## v7.7.1 + +Date: 2025-07-24 + +### Patch Changes + +- `@react-router/dev` - Update to Prettier v3 for formatting when running `react-router reveal --no-typescript` ([#14049](https://github.com/remix-run/react-router/pull/14049)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - RSC Data Mode: fix bug where routes with errors weren't forced to revalidate when `shouldRevalidate` returned `false` ([#14026](https://github.com/remix-run/react-router/pull/14026)) +- `react-router` - RSC Data Mode: fix `Matched leaf route at location "/..." does not have an element or Component` warnings when error boundaries are rendered ([#14021](https://github.com/remix-run/react-router/pull/14021)) + +**Full Changelog**: [`v7.7.0...v7.7.1`](https://github.com/remix-run/react-router/compare/react-router@7.7.0...react-router@7.7.1) + +## v7.7.0 + +Date: 2025-07-16 + +### What's Changed + +#### Unstable RSC APIs + +We're excited to introduce experimental support for RSC in Data Mode via the following new APIs: + +- [`unstable_RSCHydratedRouter`](https://reactrouter.com/api/rsc/RSCHydratedRouter) +- [`unstable_RSCStaticRouter`](https://reactrouter.com/api/rsc/RSCStaticRouter) +- [`unstable_createCallServer`](https://reactrouter.com/api/rsc/createCallServer) +- [`unstable_getRSCStream`](https://reactrouter.com/api/rsc/getRSCStream) +- [`unstable_matchRSCServerRequest`](https://reactrouter.com/api/rsc/matchRSCServerRequest) +- [`unstable_routeRSCServerRequest`](https://reactrouter.com/api/rsc/routeRSCServerRequest) + +For more information, check out the [blog post](https://remix.run/blog/react-router-and-react-server-components) and the [RSC Docs](https://reactrouter.com/how-to/react-server-components). + +### Minor Changes + +- `create-react-router` - Add Deno as a supported and detectable package manager. Note that this detection will only work with Deno versions 2.0.5 and above. If you are using an older version version of Deno then you must specify the --package-manager CLI flag set to `deno`. ([#12327](https://github.com/remix-run/react-router/pull/12327)) +- `@react-router/remix-config-routes-adapter` - Export `DefineRouteFunction` type alongside `DefineRoutesFunction` ([#13945](https://github.com/remix-run/react-router/pull/13945)) + +### Patch Changes + +- `react-router` - Handle `InvalidCharacterError` when validating cookie signature ([#13847](https://github.com/remix-run/react-router/pull/13847)) +- `react-router` - Pass a copy of `searchParams` to the `setSearchParams` callback function to avoid mutations of the internal `searchParams` instance ([#12784](https://github.com/remix-run/react-router/pull/12784)) + - This causes bugs if you mutate the current stateful `searchParams` when a navigation is blocked because the internal instance gets out of sync with `useLocation().search` +- `react-router` - Support invalid `Date` in `turbo-stream` v2 fork ([#13684](https://github.com/remix-run/react-router/pull/13684)) +- `react-router` - In Framework Mode, clear critical CSS in development after initial render ([#13872](https://github.com/remix-run/react-router/pull/13872), [#13995](https://github.com/remix-run/react-router/pull/13995)) +- `react-router` - Strip search parameters from `patchRoutesOnNavigation` `path` param for fetcher calls ([#13911](https://github.com/remix-run/react-router/pull/13911)) +- `react-router` - Skip scroll restoration on `useRevalidator()` calls because they're not new locations ([#13671](https://github.com/remix-run/react-router/pull/13671)) +- `react-router` - Support unencoded UTF-8 routes in prerender config with `ssr` set to `false` ([#13699](https://github.com/remix-run/react-router/pull/13699)) +- `react-router` - Do not throw if the url hash is not a valid URI component ([#13247](https://github.com/remix-run/react-router/pull/13247)) +- `react-router` - Remove `Content-Length` header from Single Fetch responses ([#13902](https://github.com/remix-run/react-router/pull/13902)) +- `react-router` - Fix a regression in `createRoutesStub` introduced with the middleware feature ([#13946](https://github.com/remix-run/react-router/pull/13946)) + - As part of that work we altered the signature to align with the new middleware APIs without making it backwards compatible with the prior `AppLoadContext` API + - This permitted `createRoutesStub` to work if you were opting into middleware and the updated `context` typings, but broke `createRoutesStub` for users not yet opting into middleware + - We've reverted this change and re-implemented it in such a way that both sets of users can leverage it + - โš ๏ธ This may be a breaking bug for if you have adopted the unstable Middleware feature and are using `createRoutesStub` with the updated API. + + ```tsx + // If you have not opted into middleware, the old API should work again + let context: AppLoadContext = { + /*...*/ + }; + let Stub = createRoutesStub(routes, context); + + // If you have opted into middleware, you should now pass an instantiated + // `unstable_routerContextProvider` instead of a `getContext` factory function. + let context = new unstable_RouterContextProvider(); + context.set(SomeContext, someValue); + let Stub = createRoutesStub(routes, context); + ``` + +- `@react-router/dev` - Update `vite-node` to `^3.2.2` to support Vite 7 ([#13781](https://github.com/remix-run/react-router/pull/13781)) +- `@react-router/dev` - Properly handle `https` protocol in dev mode ([#13746](https://github.com/remix-run/react-router/pull/13746)) +- `@react-router/dev` - Fix missing styles when Vite's `build.cssCodeSplit` option is disabled ([#13943](https://github.com/remix-run/react-router/pull/13943)) +- `@react-router/dev` - Allow `.mts` and `.mjs` extensions for route config file ([#13931](https://github.com/remix-run/react-router/pull/13931)) +- `@react-router/dev` - Fix prerender file locations when `cwd` differs from project root ([#13824](https://github.com/remix-run/react-router/pull/13824)) +- `@react-router/dev` - Improve chunk error logging when a chunk cannot be found during the build ([#13799](https://github.com/remix-run/react-router/pull/13799)) +- `@react-router/dev` - Fix incorrectly configured `externalConditions` which had enabled `module` condition for externals and broke builds with certain packages (like Emotion) ([#13871](https://github.com/remix-run/react-router/pull/13871)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- Add unstable RSC support for Data Mode ([#13700](https://github.com/remix-run/react-router/pull/13700)) + - For more information, see the [RSC documentation](https://reactrouter.com/how-to/react-server-components) + +### Changes by Package + +- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/create-react-router/CHANGELOG.md#770) +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router/CHANGELOG.md#770) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-architect/CHANGELOG.md#770) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-cloudflare/CHANGELOG.md#770) +- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-dev/CHANGELOG.md#770) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-express/CHANGELOG.md#770) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-fs-routes/CHANGELOG.md#770) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-node/CHANGELOG.md#770) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#770) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-serve/CHANGELOG.md#770) + +**Full Changelog**: [`v7.6.3...v7.7.0`](https://github.com/remix-run/react-router/compare/react-router@7.6.3...react-router@7.7.0) + +## v7.6.3 + +Date: 2025-06-27 + +### Patch Changes + +- `react-router` - Do not serialize types for `useRouteLoaderData` ([#13752](https://github.com/remix-run/react-router/pull/13752)) + - For types to distinguish a `clientLoader` from a `serverLoader`, you MUST annotate `clientLoader` args: + + ```ts + // ๐Ÿ‘‡ annotation required to skip serializing types + export function clientLoader({}: Route.ClientLoaderArgs) { + return { fn: () => "earth" }; + } + + function SomeComponent() { + const data = useRouteLoaderData("routes/this-route"); + const planet = data?.fn() ?? "world"; + return

Hello, {planet}!

; + } + ``` + +- `@react-router/cloudflare` - Remove `tsup` from `peerDependencies` ([#13757](https://github.com/remix-run/react-router/pull/13757)) +- `@react-router/dev` - Add Vite 7 support ([#13748](https://github.com/remix-run/react-router/pull/13748)) +- `@react-router/dev` - Skip `package.json` resolution checks when a custom `entry.server.(j|t)sx` file is provided ([#13744](https://github.com/remix-run/react-router/pull/13744)) +- `@react-router/dev` - Add validation for a route's id not being 'root' ([#13792](https://github.com/remix-run/react-router/pull/13792)) +- `@react-router/fs-routes` `@react-router/remix-config-routes-adapter` - Use `replaceAll` for normalizing windows file system slashes ([#13738](https://github.com/remix-run/react-router/pull/13738)) +- `@react-router/node` - Remove old "install" package exports ([#13762](https://github.com/remix-run/react-router/pull/13762)) + +**Full Changelog**: [`v7.6.2...v7.6.3`](https://github.com/remix-run/react-router/compare/react-router@7.6.2...react-router@7.6.3) + +## v7.6.2 + +Date: 2025-06-03 + +### Patch Changes + +- `create-react-router` - Update `tar-fs` ([#13675](https://github.com/remix-run/react-router/pull/13675)) +- `react-router` - (INTERNAL) Slight refactor of internal `headers()` function processing for use with RSC ([#13639](https://github.com/remix-run/react-router/pull/13639)) +- `react-router` `@react-router/dev` - Avoid additional `with-props` chunk in Framework Mode by moving route module component prop logic from the Vite plugin to `react-router` ([#13650](https://github.com/remix-run/react-router/pull/13650)) +- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled and an absolute Vite `base` has been configured, ensure critical CSS is handled correctly during development ([#13598](https://github.com/remix-run/react-router/pull/13598)) +- `@react-router/dev` - Update `vite-node` ([#13673](https://github.com/remix-run/react-router/pull/13673)) +- `@react-router/dev` - Fix typegen for non-{.js,.jsx,.ts,.tsx} routes like .mdx ([#12453](https://github.com/remix-run/react-router/pull/12453)) +- `@react-router/dev` - Fix href types for optional dynamic params ([#13725](https://github.com/remix-run/react-router/pull/13725)) + + 7.6.1 introduced fixes for `href` when using optional static segments, + but those fixes caused regressions with how optional dynamic params worked in 7.6.0: + + ```ts + // 7.6.0 + href("/users/:id?"); // โœ… + href("/users/:id?", { id: 1 }); // โœ… + + // 7.6.1 + href("/users/:id?"); // โŒ + href("/users/:id?", { id: 1 }); // โŒ + ``` + + Now, optional static segments are expanded into different paths for `href`, but optional dynamic params are not. + This way `href` can unambiguously refer to an exact URL path, all while keeping the number of path options to a minimum. + + ```ts + // 7.6.2 + + // path: /users/:id?/edit? + href(" + // ^ suggestions when cursor is here: + // + // /users/:id? + // /users/:id?/edit + ``` + + Additionally, you can pass `params` from component props without needing to narrow them manually: + + ```ts + declare const params: { id?: number }; + + // 7.6.0 + href("/users/:id?", params); + + // 7.6.1 + href("/users/:id?", params); // โŒ + "id" in params ? href("/users/:id", params) : href("/users"); // works... but is annoying + + // 7.6.2 + href("/users/:id?", params); // restores behavior of 7.6.0 + ``` + +**Full Changelog**: [`v7.6.1...v7.6.2`](https://github.com/remix-run/react-router/compare/react-router@7.6.1...react-router@7.6.2) + +## v7.6.1 + +Date: 2025-05-25 + +### Patch Changes + +- `react-router` - Partially revert optimization added in `7.1.4` to reduce calls to `matchRoutes` because it surfaced other issues ([#13562](https://github.com/remix-run/react-router/pull/13562)) +- `react-router` - Update `Route.MetaArgs` to reflect that `data` can be potentially `undefined` ([#13563](https://github.com/remix-run/react-router/pull/13563)) + - This is primarily for cases where a route `loader` threw an error to it's own `ErrorBoundary`, but it also arises in the case of a 404 which renders the root `ErrorBoundary`/`meta` but the root `loader` did not run because not routes matched +- `react-router` - Avoid initial fetcher execution 404 error when Lazy Route Discovery is interrupted by a navigation ([#13564](https://github.com/remix-run/react-router/pull/13564)) +- `react-router` - Properly `href` replaces splats `*` ([#13593](https://github.com/remix-run/react-router/pull/13593)) + - `href("/products/*", { "*": "/1/edit" }); // -> /products/1/edit` +- `@react-router/architect` - Update `@architect/functions` from `^5.2.0` to `^7.0.0` ([#13556](https://github.com/remix-run/react-router/pull/13556)) +- `@react-router/dev` - Prevent typegen with route files that are outside the `app/` directory ([#12996](https://github.com/remix-run/react-router/pull/12996)) +- `@react-router/dev` - Add additional logging to `build` command output when cleaning assets from server build ([#13547](https://github.com/remix-run/react-router/pull/13547)) +- `@react-router/dev` - Don't clean assets from server build when `build.ssrEmitAssets` has been enabled in Vite config ([#13547](https://github.com/remix-run/react-router/pull/13547)) +- `@react-router/dev` - Fix typegen when same route is used at multiple paths ([#13574](https://github.com/remix-run/react-router/pull/13574)) + - For example, `routes/route.tsx` is used at 4 different paths here: + + ```ts + import { type RouteConfig, route } from "@react-router/dev/routes"; + export default [ + route("base/:base", "routes/base.tsx", [ + route("home/:home", "routes/route.tsx", { id: "home" }), + route("changelog/:changelog", "routes/route.tsx", { id: "changelog" }), + route("splat/*", "routes/route.tsx", { id: "splat" }), + ]), + route("other/:other", "routes/route.tsx", { id: "other" }), + ] satisfies RouteConfig; + ``` + + - Previously, typegen would arbitrarily pick one of these paths to be the "winner" and generate types for the route module based on that path + - Now, typegen creates unions as necessary for alternate paths for the same route file + +- `@react-router/dev` - Better types for `params` ([#13543](https://github.com/remix-run/react-router/pull/13543)) + - For example: + + ```ts + // routes.ts + import { type RouteConfig, route } from "@react-router/dev/routes"; + + export default [ + route("parent/:p", "routes/parent.tsx", [ + route("route/:r", "routes/route.tsx", [ + route("child1/:c1a/:c1b", "routes/child1.tsx"), + route("child2/:c2a/:c2b", "routes/child2.tsx"), + ]), + ]), + ] satisfies RouteConfig; + ``` + + - Previously, `params` for `routes/route` were calculated as `{ p: string, r: string }`. + - This incorrectly ignores params that could come from child routes + - If visiting `/parent/1/route/2/child1/3/4`, the actual params passed to `routes/route` will have a type of `{ p: string, r: string, c1a: string, c1b: string }` + - Now, `params` are aware of child routes and autocompletion will include child params as optionals: + + ```ts + params.| + // ^ cursor is here and you ask for autocompletion + // p: string + // r: string + // c1a?: string + // c1b?: string + // c2a?: string + // c2b?: string + ``` + + - You can also narrow the types for `params` as it is implemented as a normalized union of params for each page that includes `routes/route`: + + ```ts + if (typeof params.c1a === 'string') { + params.| + // ^ cursor is here and you ask for autocompletion + // p: string + // r: string + // c1a: string + // c1b: string + } + ``` + +- `@react-router/dev` - Fix `href` for optional segments ([#13595](https://github.com/remix-run/react-router/pull/13595)) + - Type generation now expands paths with optionals into their corresponding non-optional paths + - For example, the path `/user/:id?` gets expanded into `/user` and `/user/:id` to more closely model visitable URLs + - `href` then uses these expanded (non-optional) paths to construct type-safe paths for your app: + + ```ts + // original: /user/:id? + // expanded: /user & /user/:id + href("/user"); // โœ… + href("/user/:id", { id: 1 }); // โœ… + ``` + + - This becomes even more important for static optional paths where there wasn't a good way to indicate whether the optional should be included in the resulting path: + + ```ts + // original: /products/:id/detail? + + // before + href("/products/:id/detail?"); // โŒ How can we tell `href` to include or omit `detail?` segment with a complex API? + + // now + // expanded: /products/:id & /products/:id/detail + href("/product/:id"); // โœ… + href("/product/:id/detail"); // โœ… + ``` + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `@react-router/dev` - Renamed internal `react-router/route-module` export to `react-router/internal` ([#13543](https://github.com/remix-run/react-router/pull/13543)) +- `@react-router/dev` - Removed `Info` export from generated `+types/*` files ([#13543](https://github.com/remix-run/react-router/pull/13543)) +- `@react-router/dev` - Normalize dirent entry path across node versions when generating SRI manifest ([#13591](https://github.com/remix-run/react-router/pull/13591)) + +**Full Changelog**: [`v7.6.0...v7.6.1`](https://github.com/remix-run/react-router/compare/react-router@7.6.0...react-router@7.6.1) + +## v7.6.0 + +Date: 2025-05-08 + +### What's Changed + +#### `routeDiscovery` Config Option + +We've added a new config option in `7.6.0` which grants you more control over the Lazy Route Discovery feature. You can now configure the `/__manifest` path if you're running multiple RR applications on the same server, or you can also disable the feature entirely if your application is small enough and the feature isn't necessary. + +```ts +// react-router.config.ts + +export default { + // You can modify the manifest path used: + routeDiscovery: { mode: "lazy", manifestPath: "/custom-manifest" } + + // Or you can disable this feature entirely and include all routes in the + // manifest on initial document load: + routeDiscovery: { mode: "initial" } + + // If you don't specify anything, the default config is as follows, which enables + // Lazy Route Discovery and makes manifest requests to the `/__manifest` path: + // routeDiscovery: { mode: "lazy", manifestPath: "/__manifest" } +} satisfies Config; +``` + +#### Automatic Types for Future Flags + +Some future flags alter the way types should work in React Router. Previously, you had to remember to manually opt-in to the new types. For example, for `future.unstable_middleware`: + +```ts +// react-router.config.ts + +// Step 1: Enable middleware +export default { + future: { + unstable_middleware: true, + }, +}; + +// Step 2: Enable middleware types +declare module "react-router" { + interface Future { + unstable_middleware: true; // ๐Ÿ‘ˆ Enable middleware types + } +} +``` + +It was up to you to keep the runtime future flags synced with the types for those flags. This was confusing and error-prone. + +Now, React Router will automatically enable types for future flags. That means you only need to specify the runtime future flag: + +```ts +// react-router.config.ts + +// Step 1: Enable middleware +export default { + future: { + unstable_middleware: true, + }, +}; + +// No step 2! That's it! +``` + +Behind the scenes, React Router will generate the corresponding `declare module` into `.react-router/types`. Currently this is done in `.react-router/types/+register.ts` but this is an implementation detail that may change in the future. + +### Minor Changes + +- `react-router` - Added a new `routeDiscovery` option in `react-router.config.ts` to configure Lazy Route Discovery behavior ([#13451](https://github.com/remix-run/react-router/pull/13451)) +- `react-router` - Add support for route component props in `createRoutesStub` ([#13528](https://github.com/remix-run/react-router/pull/13528)) + - This allows you to unit test your route components using the props instead of the hooks: + + ```tsx + let RoutesStub = createRoutesStub([ + { + path: "/", + Component({ loaderData }) { + let data = loaderData as { message: string }; + return
Message: {data.message}
; + }, + loader() { + return { message: "hello" }; + }, + }, + ]); + + render(); + + await waitFor(() => screen.findByText("Message: hello")); + ``` + +- `@react-router/dev` - Automatic types for future flags ([#13506](https://github.com/remix-run/react-router/pull/13506)) + +### Patch Changes + +You may notice this list is a bit larger than usual! The team ate their vegetables last week and spent the week [squashing bugs](https://x.com/BrooksLybrand/status/1918406062920589731) to work on lowering the issue count that had ballooned a bit since the v7 release. + +- `react-router` - Fix `react-router` module augmentation for `NodeNext` ([#13498](https://github.com/remix-run/react-router/pull/13498)) +- `react-router` - Don't bundle `react-router` in `react-router/dom` CJS export ([#13497](https://github.com/remix-run/react-router/pull/13497)) +- `react-router` - Fix bug where a submitting `fetcher` would get stuck in a `loading` state if a revalidating `loader` redirected ([#12873](https://github.com/remix-run/react-router/pull/12873)) +- `react-router` - Fix hydration error if a server `loader` returned `undefined` ([#13496](https://github.com/remix-run/react-router/pull/13496)) +- `react-router` - Fix initial load 404 scenarios in data mode ([#13500](https://github.com/remix-run/react-router/pull/13500)) +- `react-router` - Stabilize `useRevalidator`'s `revalidate` function ([#13542](https://github.com/remix-run/react-router/pull/13542)) +- `react-router` - Preserve status code if a `clientAction` throws a `data()` result in framework mode ([#13522](https://github.com/remix-run/react-router/pull/13522)) +- `react-router` - Be defensive against leading double slashes in paths to avoid `Invalid URL` errors from the URL constructor ([#13510](https://github.com/remix-run/react-router/pull/13510)) + - Note we do not sanitize/normalize these paths - we only detect them so we can avoid the error that would be thrown by `new URL("//", window.location.origin)` +- `react-router` - Remove `Navigator` declaration for `navigator.connection.saveData` to avoid messing with any other types beyond `saveData` in user land ([#13512](https://github.com/remix-run/react-router/pull/13512)) +- `react-router` - Fix `handleError` `params` values on `.data` requests for routes with a dynamic param as the last URL segment ([#13481](https://github.com/remix-run/react-router/pull/13481)) +- `react-router` - Don't trigger an `ErrorBoundary` UI before the reload when we detect a manifest version mismatch in Lazy Route Discovery ([#13480](https://github.com/remix-run/react-router/pull/13480)) +- `react-router` - Inline `turbo-stream@2.4.1` dependency and fix decoding ordering of `Map`/`Set` instances ([#13518](https://github.com/remix-run/react-router/pull/13518)) +- `react-router` - Only render dev warnings during dev ([#13461](https://github.com/remix-run/react-router/pull/13461)) +- `react-router` - Short circuit post-processing on aborted `dataStrategy` requests ([#13521](https://github.com/remix-run/react-router/pull/13521)) + - This resolves non-user-facing console errors of the form `Cannot read properties of undefined (reading 'result')` +- `@react-router/dev` - Support project root directories without a `package.json` if it exists in a parent directory ([#13472](https://github.com/remix-run/react-router/pull/13472)) +- `@react-router/dev` - When providing a custom Vite config path via the CLI `--config`/`-c` flag, default the project root directory to the directory containing the Vite config when not explicitly provided ([#13472](https://github.com/remix-run/react-router/pull/13472)) +- `@react-router/dev` - In a `routes.ts` context, ensure the `--mode` flag is respected for `import.meta.env.MODE` ([#13485](https://github.com/remix-run/react-router/pull/13485)) + - Previously, `import.meta.env.MODE` within a `routes.ts` context was always `"development"` for the `dev` and `typegen --watch` commands, but otherwise resolved to `"production"`. These defaults are still in place, but if a `--mode` flag is provided, this will now take precedence. +- `@react-router/dev` - Ensure consistent project root directory resolution logic in CLI commands ([#13472](https://github.com/remix-run/react-router/pull/13472)) +- `@react-router/dev` - When executing `react-router.config.ts` and `routes.ts` with `vite-node`, ensure that PostCSS config files are ignored ([#13489](https://github.com/remix-run/react-router/pull/13489)) +- `@react-router/dev` - When extracting critical CSS during development, ensure it's loaded from the client environment to avoid issues with plugins that handle the SSR environment differently ([#13503](https://github.com/remix-run/react-router/pull/13503)) +- `@react-router/dev` - Fix "Status message is not supported by HTTP/2" error during dev when using HTTPS ([#13460](https://github.com/remix-run/react-router/pull/13460)) +- `@react-router/dev` - Update config when `react-router.config.ts` is created or deleted during development ([#12319](https://github.com/remix-run/react-router/pull/12319)) +- `@react-router/dev` - Skip unnecessary `routes.ts` evaluation before Vite build is started ([#13513](https://github.com/remix-run/react-router/pull/13513)) +- `@react-router/dev` - Fix `TS2300: Duplicate identifier` errors caused by generated types ([#13499](https://github.com/remix-run/react-router/pull/13499)) +- Previously, routes that had the same full path would cause duplicate entries in the generated types for `href` (`.react-router/types/+register.ts`), causing type checking errors + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - Fix a few bugs with error bubbling in middleware use-cases ([#13538](https://github.com/remix-run/react-router/pull/13538)) +- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled, ensure that `build.assetsDir` in Vite config is respected when `environments.client.build.assetsDir` is not configured ([#13491](https://github.com/remix-run/react-router/pull/13491)) + +### Changes by Package + +- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/create-react-router/CHANGELOG.md#760) +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router/CHANGELOG.md#760) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-architect/CHANGELOG.md#760) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-cloudflare/CHANGELOG.md#760) +- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-dev/CHANGELOG.md#760) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-express/CHANGELOG.md#760) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-fs-routes/CHANGELOG.md#760) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-node/CHANGELOG.md#760) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#760) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-serve/CHANGELOG.md#760) + +**Full Changelog**: [`v7.5.3...v7.6.0`](https://github.com/remix-run/react-router/compare/react-router@7.5.3...react-router@7.6.0) + +## v7.5.3 + +Date: 2025-04-28 + +### Patch Changes + +- `react-router` - Fix bug where bubbled action errors would result in `loaderData` being cleared at the handling `ErrorBoundary` route ([#13476](https://github.com/remix-run/react-router/pull/13476)) +- `react-router` - Handle redirects from `clientLoader.hydrate` initial load executions ([#13477](https://github.com/remix-run/react-router/pull/13477)) + +**Full Changelog**: [`v7.5.2...v7.5.3`](https://github.com/remix-run/react-router/compare/react-router@7.5.2...react-router@7.5.3) + +## v7.5.2 + +Date: 2025-04-24 + +### Security Notice + +Fixed 2 security vulnerabilities that could result in cache-poisoning attacks by sending specific headers intended for build-time usage for SPA Mode and Pre-rendering ([GHSA-f46r-rw29-r322](https://github.com/remix-run/react-router/security/advisories/GHSA-f46r-rw29-r322), [GHSA-cpj6-fhp6-mr6j](https://github.com/remix-run/react-router/security/advisories/GHSA-cpj6-fhp6-mr6j)). + +### Patch Changes + +- `react-router` - Adjust approach for Pre-rendering/SPA Mode via headers ([#13453](https://github.com/remix-run/react-router/pull/13453)) +- `react-router` - Update Single Fetch to also handle the 204 redirects used in `?_data` requests in Remix v2 ([#13364](https://github.com/remix-run/react-router/pull/13364)) + - This allows applications to trigger a redirect on `.data` requests from outside the scope of React Router (i.e., an `express`/`hono` middleware) the same way they did in Remix v2 before Single Fetch was implemented + - This is a bit of an escape hatch - the recommended way to handle this is redirecting from a root route middleware + - To use this functionality, you may return from a `.data` request wih a response as follows: + - Set a 204 status code + - Set an `X-Remix-Redirect: ` header + - Optionally, set `X-Remix-Replace: true` or `X-Remix-Reload-Document: true` headers to replicate `replace()`/`redirectDocument()` functionality + - โš ๏ธ Please note that these responses rely on implementation details that are subject to change without a SemVer major release, and it is recommended you set up integration tests for your application to confirm this functionality is working correctly with each future React Router upgrade + +**Full Changelog**: [`v7.5.1...v7.5.2`](https://github.com/remix-run/react-router/compare/react-router@7.5.1...react-router@7.5.2) + +## v7.5.1 + +Date: 2025-04-17 + +### Patch Changes + +- `react-router` - When using the object-based `route.lazy` API, the `HydrateFallback` and `hydrateFallbackElement` properties are now skipped when lazy loading routes after hydration ([#13376](https://github.com/remix-run/react-router/pull/13376)) + - If you move the code for these properties into a separate file, since the hydrate properties were unused already (if the route wasn't present during hydration), you can avoid downloading them at all. For example: + + ```ts + createBrowserRouter([ + { + path: "/show/:showId", + lazy: { + loader: async () => (await import("./show.loader.js")).loader, + Component: async () => + (await import("./show.component.js")).Component, + HydrateFallback: async () => + (await import("./show.hydrate-fallback.js")).HydrateFallback, + }, + }, + ]); + ``` + +- `react-router` - Fix single fetch bug where no revalidation request would be made when navigating upwards to a reused parent route ([#13253](https://github.com/remix-run/react-router/pull/13253)) +- `react-router` - Properly revalidate pre-rendered paths when param values change when using `ssr:false` + `prerender` configs ([#13380](https://github.com/remix-run/react-router/pull/13380)) +- `react-router` - Fix pre-rendering when a loader returns a redirect ([#13365](https://github.com/remix-run/react-router/pull/13365)) +- `react-router` - Do not automatically add `null` to `staticHandler.query()` `context.loaderData` if routes do not have loaders ([#13223](https://github.com/remix-run/react-router/pull/13223)) + - This was a Remix v2 implementation detail inadvertently left in for React Router v7 + - Now that we allow returning `undefined` from loaders, our prior check of `loaderData[routeId] !== undefined` was no longer sufficient and was changed to a `routeId in loaderData` check - these `null` values can cause issues for this new check + - โš ๏ธ This could be a "breaking bug fix" for you if you are doing manual SSR with `createStaticHandler()`/``, and using `context.loaderData` to control `` hydration behavior on the client + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - Add better error messaging when `getLoadContext` is not updated to return a `Map` ([#13242](https://github.com/remix-run/react-router/pull/13242)) +- `react-router` - Update context type for `LoaderFunctionArgs`/`ActionFunctionArgs` when middleware is enabled ([#13381](https://github.com/remix-run/react-router/pull/13381)) +- `react-router` - Add a new `unstable_runClientMiddleware` argument to `dataStrategy` to enable middleware execution in custom `dataStrategy` implementations ([#13395](https://github.com/remix-run/react-router/pull/13395)) +- `react-router` - Add support for the new `unstable_shouldCallHandler`/`unstable_shouldRevalidateArgs` APIs in `dataStrategy` ([#13253](https://github.com/remix-run/react-router/pull/13253)) + +**Full Changelog**: [`v7.5.0...v7.5.1`](https://github.com/remix-run/react-router/compare/react-router@7.5.0...react-router@7.5.1) + +## v7.5.0 + +Date: 2025-04-04 + +### What's Changed + +#### `route.lazy` Object API + +We've introduced a new `route.lazy` API which gives you more granular control over the lazy loading of route properties that you could not achieve with the `route.lazy()` function signature. This is useful for Framework mode and performance-critical library mode applications. + +```ts +createBrowserRouter([ + { + path: "/show/:showId", + lazy: { + loader: async () => (await import("./show.loader.js")).loader, + action: async () => (await import("./show.action.js")).action, + Component: async () => (await import("./show.component.js")).Component, + }, + }, +]); +``` + +โš ๏ธ This is a breaking change if you have adopted the `route.unstable_lazyMiddleware` API which has been removed in favor of `route.lazy.unstable_middleware`. See the `Unstable Changes` section below for more information. + +### Minor Changes + +- `react-router` - Add granular object-based API for `route.lazy` to support lazy loading of individual route properties ([#13294](https://github.com/remix-run/react-router/pull/13294)) + +### Patch Changes + +- `@react-router/dev` - Update optional `wrangler` peer dependency range to support `wrangler` v4 ([#13258](https://github.com/remix-run/react-router/pull/13258)) +- `@react-router/dev` - Reinstate dependency optimization in the child compiler to fix `depsOptimizer is required in dev mode` errors when using `vite-plugin-cloudflare` and importing Node.js builtins ([#13317](https://github.com/remix-run/react-router/pull/13317)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - Introduce `future.unstable_subResourceIntegrity` flag that enables generation of an `importmap` with `integrity` for the scripts that will be loaded by the browser ([#13163](https://github.com/remix-run/react-router/pull/13163)) +- `react-router` - Remove support for the `route.unstable_lazyMiddleware` property ([#13294](https://github.com/remix-run/react-router/pull/13294)) + - In order to lazily load middleware, you can use the new object-based `route.lazy.unstable_middleware` API +- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled, ensure critical CSS in development works when using a custom Vite `base` has been configured ([#13305](https://github.com/remix-run/react-router/pull/13305)) + +### Changes by Package + +- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/create-react-router/CHANGELOG.md#750) +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router/CHANGELOG.md#750) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-architect/CHANGELOG.md#750) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-cloudflare/CHANGELOG.md#750) +- [`@react-router/dev`](http://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-dev/CHANGELOG.md#750) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-express/CHANGELOG.md#750) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-fs-routes/CHANGELOG.md#750) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-node/CHANGELOG.md#750) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#750) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-serve/CHANGELOG.md#750) + +**Full Changelog**: [`v7.4.1...v7.5.0`](https://github.com/remix-run/react-router/compare/react-router@7.4.1...react-router@7.5.0) + +## v7.4.1 + +Date: 2025-03-28 + +### Security Notice + +Fixed a security vulnerability that allowed URL manipulation and potential cache pollution via the `Host` and `X-Forwarded-Host` headers due to inadequate port sanitization ([GHSA-4q56-crqp-v477/CVE-2025-31137](https://github.com/remix-run/react-router/security/advisories/GHSA-4q56-crqp-v477)). + +### Patch Changes + +- `react-router` - Dedupe calls to `route.lazy` functions ([#13260](https://github.com/remix-run/react-router/pull/13260)) +- `@react-router/dev` - Fix path in prerender error messages ([#13257](https://github.com/remix-run/react-router/pull/13257)) +- `@react-router/dev` - Fix typegen for virtual modules when `moduleDetection` is set to `force` ([#13267](https://github.com/remix-run/react-router/pull/13267)) +- `@react-router/express` - Better validation of `x-forwarded-host` header to prevent potential security issues ([#13309](https://github.com/remix-run/react-router/pull/13309)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - Fix types on `unstable_MiddlewareFunction` to avoid type errors when a middleware doesn't return a value ([#13311](https://github.com/remix-run/react-router/pull/13311)) +- `react-router` - Add support for `route.unstable_lazyMiddleware` function to allow lazy loading of middleware logic ([#13210](https://github.com/remix-run/react-router/pull/13210)) + - โš ๏ธ We do not recommend adoption of this API currently as we are likely going to change it prior to the stable release of middleware + - โš ๏ธ This may be a breaking change if your app is currently returning `unstable_middleware` from `route.lazy` + - The `route.unstable_middleware` property is no longer supported in the return value from `route.lazy` + - If you want to lazily load middleware, you must use `route.unstable_lazyMiddleware` +- `@react-router/dev` - When both `future.unstable_middleware` and `future.unstable_splitRouteModules` are enabled, split `unstable_clientMiddleware` route exports into separate chunks when possible ([#13210](https://github.com/remix-run/react-router/pull/13210)) +- `@react-router/dev` - Improve performance of `future.unstable_middleware` by ensuring that route modules are only blocking during the middleware phase when the `unstable_clientMiddleware` has been defined ([#13210](https://github.com/remix-run/react-router/pull/13210)) + +**Full Changelog**: [`v7.4.0...v7.4.1`](https://github.com/remix-run/react-router/compare/react-router@7.4.0...react-router@7.4.1) + +## v7.4.0 + +Date: 2025-03-19 + +### Minor Changes + +- `@react-router/dev` - Generate types for `virtual:react-router/server-build` module ([#13152](https://github.com/remix-run/react-router/pull/13152)) + +### Patch Changes + +- `react-router` - Fix root loader data on initial load redirects in SPA mode ([#13222](https://github.com/remix-run/react-router/pull/13222)) +- `react-router` - Load ancestor pathless/index routes in lazy route discovery for upwards non-eager-discovery routing ([#13203](https://github.com/remix-run/react-router/pull/13203)) +- `react-router` - Fix `shouldRevalidate` behavior for `clientLoader`-only routes in `ssr:true` apps ([#13221](https://github.com/remix-run/react-router/pull/13221)) +- `@react-router/dev` - Fix conflicts with other Vite plugins that use the `configureServer` and/or `configurePreviewServer` hooks ([#13184](https://github.com/remix-run/react-router/pull/13184)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - If a middleware throws an error, ensure we only bubble the error itself via `next()` and are no longer leaking the `MiddlewareError` implementation detail ([#13180](https://github.com/remix-run/react-router/pull/13180)) + - โš ๏ธ This may be a breaking change if you are `catch`-ing errors thrown by the `next()` function in your middlewares +- `react-router` - Fix `RequestHandler` `loadContext` parameter type when middleware is enabled ([#13204](https://github.com/remix-run/react-router/pull/13204)) +- `react-router` - Update `Route.unstable_MiddlewareFunction` to have a return value of `Response | undefined` instead of `Response | void` ([#13199](https://github.com/remix-run/react-router/pull/13199)) +- `@react-router/dev` - When `future.unstable_splitRouteModules` is set to `"enforce"`, allow both splittable and unsplittable root route exports since it's always in a single chunk ([#13238](https://github.com/remix-run/react-router/pull/13238)) +- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled, allow plugins that override the default SSR environment (such as `@cloudflare/vite-plugin`) to be placed before or after the React Router plugin ([#13183](https://github.com/remix-run/react-router/pull/13183)) + +### Changes by Package + +- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/create-react-router/CHANGELOG.md#740) +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router/CHANGELOG.md#740) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-architect/CHANGELOG.md#740) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-cloudflare/CHANGELOG.md#740) +- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-dev/CHANGELOG.md#740) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-express/CHANGELOG.md#740) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-fs-routes/CHANGELOG.md#740) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-node/CHANGELOG.md#740) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#740) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-serve/CHANGELOG.md#740) + +**Full Changelog**: [`v7.3.0...v7.4.0`](https://github.com/remix-run/react-router/compare/react-router@7.3.0...react-router@7.4.0) + +## v7.3.0 + +Date: 2025-03-06 + +### Minor Changes + +- Add `fetcherKey` as a parameter to `patchRoutesOnNavigation` ([#13061](https://github.com/remix-run/react-router/pull/13061)) + +### Patch Changes + +- `react-router` - Detect and handle manifest-skew issues on new deploys during active sessions ([#13061](https://github.com/remix-run/react-router/pull/13061)) + - In framework mode, Lazy Route Discovery will now detect manifest version mismatches in active sessions after a new deploy + - On navigations to undiscovered routes, this mismatch will trigger a document reload of the destination path + - On `fetcher` calls to undiscovered routes, this mismatch will trigger a document reload of the current path +- `react-router` - Skip resource route flow in dev server in SPA mode ([#13113](https://github.com/remix-run/react-router/pull/13113)) +- `react-router` - Fix single fetch `_root.data` requests when a `basename` is used ([#12898](https://github.com/remix-run/react-router/pull/12898)) +- `react-router` - Fix types for `loaderData` and `actionData` that contained `Record`s ([#13139](https://github.com/remix-run/react-router/pull/13139)) + - โš ๏ธ This is a breaking change for users who have already adopted `unstable_SerializesTo` - see the note in the `Unstable Changes` section below for more information +- `@react-router/dev` - Fix support for custom client `build.rollupOptions.output.entryFileNames` ([#13098](https://github.com/remix-run/react-router/pull/13098)) +- `@react-router/dev` - Fix usage of `prerender` option when `serverBundles` option has been configured or provided by a preset, e.g. `vercelPreset` from `@vercel/react-router` ([#13082](https://github.com/remix-run/react-router/pull/13082)) +- `@react-router/dev` - Fix support for custom `build.assetsDir` ([#13077](https://github.com/remix-run/react-router/pull/13077)) +- `@react-router/dev` - Remove unused dependencies ([#13134](https://github.com/remix-run/react-router/pull/13134)) +- `@react-router/dev` - Stub all routes except root in "SPA Mode" server builds to avoid issues when route modules or their dependencies import non-SSR-friendly modules ([#13023](https://github.com/remix-run/react-router/pull/13023)) +- `@react-router/dev` - Remove unused Vite file system watcher ([#13133](https://github.com/remix-run/react-router/pull/13133)) +- `@react-router/dev` - Fix support for custom SSR build input when `serverBundles` option has been configured ([#13107](https://github.com/remix-run/react-router/pull/13107)) + - โš ๏ธ Note that for consumers using the `future.unstable_viteEnvironmentApi` and `serverBundles` options together, hyphens are no longer supported in server bundle IDs since they also need to be valid Vite environment names. +- `@react-router/dev` - Fix dev server when using HTTPS by stripping HTTP/2 pseudo headers from dev server requests ([#12830](https://github.com/remix-run/react-router/pull/12830)) +- `@react-router/dev` - Lazy load Cloudflare platform proxy on first dev server request when using the `cloudflareDevProxy` Vite plugin to avoid creating unnecessary `workerd` processes ([#13016](https://github.com/remix-run/react-router/pull/13016)) +- `@react-router/dev` - Fix duplicated entries in typegen for layout routes and their corresponding index route ([#13140](https://github.com/remix-run/react-router/pull/13140)) +- `@react-router/express` - Update `express` `peerDependency` to include v5 (https://github.com/remix-run/react-router/pull/13064) ([#12961](https://github.com/remix-run/react-router/pull/12961)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - Add `context` support to client side data routers (unstable) ([#12941](https://github.com/remix-run/react-router/pull/12941)) +- `react-router` - Support middleware on routes (unstable) ([#12941](https://github.com/remix-run/react-router/pull/12941)) +- `@react-router/dev` - Fix errors with `future.unstable_viteEnvironmentApi` when the `ssr` environment has been configured by another plugin to be a custom `Vite.DevEnvironment` rather than the default `Vite.RunnableDevEnvironment` ([#13008](https://github.com/remix-run/react-router/pull/13008)) +- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled and the `ssr` environment has `optimizeDeps.noDiscovery` disabled, define `optimizeDeps.entries` and `optimizeDeps.include` ([#13007](https://github.com/remix-run/react-router/pull/13007)) + +#### Client-side `context` (unstable) + +Your application `clientLoader`/`clientAction` functions (or `loader`/`action` in library mode) will now receive a `context` parameter on the client. This is an instance of `unstable_RouterContextProvider` that you use with type-safe contexts (similar to `React.createContext`) and is most useful with the corresponding `unstable_clientMiddleware` API: + +```ts +import { unstable_createContext } from "react-router"; + +type User = { + /*...*/ +}; + +const userContext = unstable_createContext(); + +const sessionMiddleware: Route.unstable_ClientMiddlewareFunction = async ({ + context, +}) => { + let user = await getUser(); + context.set(userContext, user); +}; + +export const unstable_clientMiddleware = [sessionMiddleware]; + +export function clientLoader({ context }: Route.ClientLoaderArgs) { + let user = context.get(userContext); + let profile = await getProfile(user.id); + return { profile }; +} +``` + +Similar to server-side requests, a fresh `context` will be created per navigation (or `fetcher` call). If you have initial data you'd like to populate in the context for every request, you can provide an `unstable_getContext` function at the root of your app: + +- Library mode - `createBrowserRouter(routes, { unstable_getContext })` +- Framework mode - `` + +This function should return an value of type `unstable_InitialContext` which is a `Map` of context's and initial values: + +```ts +const loggerContext = unstable_createContext<(...args: unknown[]) => void>(); + +function logger(...args: unknown[]) { + console.log(new Date.toISOString(), ...args); +} + +function unstable_getContext() { + let map = new Map(); + map.set(loggerContext, logger); + return map; +} +``` + +#### Middleware (unstable) + +Middleware is implemented behind a `future.unstable_middleware` flag. To enable, you must enable the flag and the types in your `react-router.config.ts` file: + +```ts +import type { Config } from "@react-router/dev/config"; +import type { Future } from "react-router"; + +declare module "react-router" { + interface Future { + unstable_middleware: true; // ๐Ÿ‘ˆ Enable middleware types + } +} + +export default { + future: { + unstable_middleware: true, // ๐Ÿ‘ˆ Enable middleware + }, +} satisfies Config; +``` + +โš ๏ธ Middleware is unstable and should not be adopted in production. There is at least one known de-optimization in route module loading for `clientMiddleware` that we will be addressing this before a stable release. + +โš ๏ธ Enabling middleware contains a breaking change to the `context` parameter passed to your `loader`/`action` functions - see below for more information. + +Once enabled, routes can define an array of middleware functions that will run sequentially before route handlers run. These functions accept the same parameters as `loader`/`action` plus an additional `next` parameter to run the remaining data pipeline. This allows middlewares to perform logic before and after handlers execute. + +```tsx +// Framework mode +export const unstable_middleware = [serverLogger, serverAuth]; // server +export const unstable_clientMiddleware = [clientLogger]; // client + +// Library mode +const routes = [ + { + path: "/", + // Middlewares are client-side for library mode SPA's + unstable_middleware: [clientLogger, clientAuth], + loader: rootLoader, + Component: Root, + }, +]; +``` + +Here's a simple example of a client-side logging middleware that can be placed on the root route: + +```tsx +const clientLogger: Route.unstable_ClientMiddlewareFunction = async ( + { request }, + next, +) => { + let start = performance.now(); + + // Run the remaining middlewares and all route loaders + await next(); + + let duration = performance.now() - start; + console.log(`Navigated to ${request.url} (${duration}ms)`); +}; +``` + +Note that in the above example, the `next`/`middleware` functions don't return anything. This is by design as on the client there is no "response" to send over the network like there would be for middlewares running on the server. The data is all handled behind the scenes by the stateful `router`. + +For a server-side middleware, the `next` function will return the HTTP `Response` that React Router will be sending across the wire, thus giving you a chance to make changes as needed. You may throw a new response to short circuit and respond immediately, or you may return a new or altered response to override the default returned by `next()`. + +```tsx +const serverLogger: Route.unstable_MiddlewareFunction = async ( + { request, params, context }, + next, +) => { + let start = performance.now(); + + // ๐Ÿ‘‡ Grab the response here + let res = await next(); + + let duration = performance.now() - start; + console.log(`Navigated to ${request.url} (${duration}ms)`); + + // ๐Ÿ‘‡ And return it here (optional if you don't modify the response) + return res; +}; +``` + +You can throw a `redirect` from a middleware to short circuit any remaining processing: + +```tsx +import { sessionContext } from "../context"; +const serverAuth: Route.unstable_MiddlewareFunction = ( + { request, params, context }, + next, +) => { + let session = context.get(sessionContext); + let user = session.get("user"); + if (!user) { + session.set("returnTo", request.url); + throw redirect("/login", 302); + } +}; +``` + +_Note that in cases like this where you don't need to do any post-processing you don't need to call the `next` function or return a `Response`._ + +Here's another example of using a server middleware to detect 404s and check the CMS for a redirect: + +```tsx +const redirects: Route.unstable_MiddlewareFunction = async ({ + request, + next, +}) => { + // attempt to handle the request + let res = await next(); + + // if it's a 404, check the CMS for a redirect, do it last + // because it's expensive + if (res.status === 404) { + let cmsRedirect = await checkCMSRedirects(request.url); + if (cmsRedirect) { + throw redirect(cmsRedirect, 302); + } + } + + return res; +}; +``` + +For more information on the `middleware` API/design, please see the [decision doc](https://github.com/remix-run/react-router/blob/release-next/decisions/0014-context-middleware.md). + +##### Middleware `context` parameter + +When middleware is enabled, your application will use a different type of `context` parameter in your loaders and actions to provide better type safety. Instead of `AppLoadContext`, `context` will now be an instance of `ContextProvider` that you can use with type-safe contexts (similar to `React.createContext`): + +```ts +import { unstable_createContext } from "react-router"; +import { Route } from "./+types/root"; +import type { Session } from "./sessions.server"; +import { getSession } from "./sessions.server"; + +let sessionContext = unstable_createContext(); + +const sessionMiddleware: Route.unstable_MiddlewareFunction = ({ + context, + request, +}) => { + let session = await getSession(request); + context.set(sessionContext, session); + // ^ must be of type Session +}; + +// ... then in some downstream middleware +const loggerMiddleware: Route.unstable_MiddlewareFunction = ({ + context, + request, +}) => { + let session = context.get(sessionContext); + // ^ typeof Session + console.log(session.get("userId"), request.method, request.url); +}; + +// ... or some downstream loader +export function loader({ context }: Route.LoaderArgs) { + let session = context.get(sessionContext); + let profile = await getProfile(session.get("userId")); + return { profile }; +} +``` + +If you are using a custom server with a `getLoadContext` function, the return value for initial context values passed from the server adapter layer is no longer an object and should now return an `unstable_InitialContext` (`Map`): + +```ts +let adapterContext = unstable_createContext(); + +function getLoadContext(req, res): unstable_InitialContext { + let map = new Map(); + map.set(adapterContext, getAdapterContext(req)); + return map; +} +``` + +#### `unstable_SerializesTo` + +`unstable_SerializesTo` added a way to register custom serialization types in Single Fetch for other library and framework authors like Apollo. It was implemented with branded type whose branded property that was made optional so that casting arbitrary values was easy: + +```ts +// without the brand being marked as optional +let x1 = 42 as unknown as unstable_SerializesTo; +// ^^^^^^^^^^ + +// with the brand being marked as optional +let x2 = 42 as unstable_SerializesTo; +``` + +However, this broke type inference in `loaderData` and `actionData` for any `Record` types as those would now (incorrectly) match `unstable_SerializesTo`. This affected all users, not just those that depended on `unstable_SerializesTo`. To fix this, the branded property of `unstable_SerializesTo` is marked as required instead of optional. + +For library and framework authors using `unstable_SerializesTo`, you may need to add `as unknown` casts before casting to `unstable_SerializesTo`. + +### Changes by Package + +- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/create-react-router/CHANGELOG.md#730) +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router/CHANGELOG.md#730) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-architect/CHANGELOG.md#730) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-cloudflare/CHANGELOG.md#730) +- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-dev/CHANGELOG.md#730) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-express/CHANGELOG.md#730) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-fs-routes/CHANGELOG.md#730) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-node/CHANGELOG.md#730) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#730) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-serve/CHANGELOG.md#730) + +**Full Changelog**: [`v7.2.0...v7.3.0`](https://github.com/remix-run/react-router/compare/react-router@7.2.0...react-router@7.3.0) + +## v7.2.0 + +Date: 2025-02-18 + +### What's Changed + +#### Type-safe `href` utility + +In framework mode, we now provide you with a fully type-safe `href` utility to give you all the warm and fuzzy feelings of path auto-completion and param validation for links in your application: + +```tsx +import { href } from "react-router"; + +export default function Component() { + const link = href("/blog/:slug", { slug: "my-first-post" }); + // ^ type-safe! ^ Also type-safe! + + return ( +
+ + +
+ ); +} +``` + +You'll now get type errors if you pass a bad path value or a bad param value: + +```ts +const badPath = href("/not/a/valid/path"); +// ^ Error! + +const badParam = href("/blog/:slug", { oops: "bad param" }); +// ^ Error! +``` + +#### Prerendering with a SPA Fallback + +This release enhances the ability to use a combination of pre-rendered paths alongside other paths that operate in "SPA Mode" when pre-rendering with `ssr:false`. + +- If you specify `ssr:false` without a `prerender` config, this is considered "SPA Mode" and the generated `index.html` file will only render down to the root route and will be able to hydrate for any valid application path +- If you specify `ssr:false` with a `prerender` config but _do not_ include the `/` path (i.e., `prerender: ['/blog/post']`), then we still generate a "SPA Mode" `index.html` file that can hydrate for any path in the application +- If you specify `ssr:false` and include the `/` path in your `prerender` config, the generated `index.html` file will be specific to the root index route, so we will now also generate a separate "SPA Mode" file in `__spa-fallback.html` that you can serve/hydrate for non-prerendered paths + +For more info, see the [Pre-rendering](https://reactrouter.com/dev/how-to/pre-rendering#pre-rendering-with-a-spa-fallback) docs for more info. + +#### Allow a root `loader` in SPA Mode + +SPA Mode used to prohibit the use of loaders in all routes so that we could hydrate for any path in the application. However, because the root route is always rendered at build time, we can lift this restriction for the root route. + +In order to use your build-time loader data during pre-rendering, we now also expose the `loaderData` as an optional prop for the `HydrateFallback` component on routes: + +- This will be defined so long as the `HydrateFallback` is rendering because _children_ routes are loading +- This will be `undefined` if the `HydrateFallback` is rendering because the route itself has it's own hydrating `clientLoader` + - In SPA mode, this will allow you to render loader root data into the SPA Mode HTML file + +### Minor Changes + +- `react-router` - New type-safe `href` utility that guarantees links point to actual paths in your app ([#13012](https://github.com/remix-run/react-router/pull/13012)) +- `@react-router/dev` - Generate a "SPA fallback" HTML file when pre-rendering the `/` route with `ssr:false` ([#12948](https://github.com/remix-run/react-router/pull/12948)) +- `@react-router/dev` - Allow a `loader` in the root route in SPA mode because it can be called/server-rendered at build time ([#12948](https://github.com/remix-run/react-router/pull/12948)) + - `Route.HydrateFallbackProps` now also receives `loaderData` + +### Patch Changes + +- `react-router` - Disable Lazy Route Discovery for all `ssr:false` apps and not just "SPA Mode" because there is no runtime server to serve the search-param-configured `__manifest` requests ([#12894](https://github.com/remix-run/react-router/pull/12894)) + - We previously only disabled this for "SPA Mode" but we realized it should apply to all `ssr:false` apps + - In those `prerender` scenarios we would pre-render the `/__manifest` file but that makes some unnecessary assumptions about the static file server behaviors +- `react-router` - Don't apply Single Fetch revalidation de-optimization when in SPA mode since there is no server HTTP request ([#12948](https://github.com/remix-run/react-router/pull/12948)) +- `react-router` - Properly handle revalidations to across a pre-render/SPA boundary ([#13021](https://github.com/remix-run/react-router/pull/13021)) + - In "hybrid" applications where some routes are pre-rendered and some are served from a SPA fallback, we need to avoid making `.data` requests if the path wasn't pre-rendered because the request will 404 + - We don't know all the pre-rendered paths client-side, however: + - All `loader` data in `ssr:false` mode is static because it's generated at build time + - A route must use a `clientLoader` to do anything dynamic + - Therefore, if a route only has a `loader` and not a `clientLoader`, we disable revalidation by default because there is no new data to retrieve + - We short circuit and skip single fetch `.data` request logic if there are no server loaders with `shouldLoad=true` in our single fetch `dataStrategy` + - This ensures that the route doesn't cause a `.data` request that would 404 after a submission +- `react-router` - Align dev server behavior with static file server behavior when `ssr:false` is set ([#12948](https://github.com/remix-run/react-router/pull/12948)) + - When no `prerender` config exists, only SSR down to the root `HydrateFallback` (SPA Mode) + - When a `prerender` config exists but the current path is not pre-rendered, only SSR down to the root `HydrateFallback` (SPA Fallback) + - Return a 404 on `.data` requests to non-pre-rendered paths +- `react-router` - Improve prefetch performance of CSS side effects in framework mode ([#12889](https://github.com/remix-run/react-router/pull/12889)) +- `react-router` - Properly handle interrupted manifest requests in lazy route discovery ([#12915](https://github.com/remix-run/react-router/pull/12915)) +- `@react-router/dev` - Handle custom `envDir` in Vite config ([#12969](https://github.com/remix-run/react-router/pull/12969)) +- `@react-router/dev` - Fix CLI parsing to allow argument-less `npx react-router` usage ([#12925](https://github.com/remix-run/react-router/pull/12925)) +- `@react-router/dev` - Skip action-only resource routes when using `prerender:true` ([#13004](https://github.com/remix-run/react-router/pull/13004)) +- `@react-router/dev` - Enhance invalid export detection when using `ssr:false` ([#12948](https://github.com/remix-run/react-router/pull/12948)) + - `headers`/`action` functions are prohibited in all routes with `ssr:false` because there will be no runtime server on which to run them + - `loader` functions are more nuanced and depend on whether a given route is prerendered + - When using `ssr:false` without a `prerender` config, only the `root` route can have a `loader` + - When using `ssr:false` with a `prerender` config, only routes matched by a `prerender` path can have a `loader` +- `@react-router/dev` - Error at build time in `ssr:false` + `prerender` apps for the edge case scenario of: ([#13021](https://github.com/remix-run/react-router/pull/13021)) + - A parent route has only a `loader` (does not have a `clientLoader`) + - The parent route is pre-rendered + - The parent route has children routes which are not prerendered + - This means that when the child paths are loaded via the SPA fallback, the parent won't have any `loaderData` because there is no server on which to run the `loader` + - This can be resolved by either adding a parent `clientLoader` or pre-rendering the child paths + - If you add a `clientLoader`, calling the `serverLoader()` on non-prerendered paths will throw a 404 +- `@react-router/dev` - Limit prerendered resource route `.data` files to only the target route ([#13004](https://github.com/remix-run/react-router/pull/13004)) +- `@react-router/dev` - Fix pre-rendering of binary files ([#13039](https://github.com/remix-run/react-router/pull/13039)) +- `@react-router/dev` - Fix typegen for repeated params ([#13012](https://github.com/remix-run/react-router/pull/13012)) + - In React Router, path parameters are keyed by their name, so for a path pattern like `/a/:id/b/:id?/c/:id`, the last `:id` will set the value for `id` in `useParams` and the `params` prop + - For example, `/a/1/b/2/c/3` will result in the value `{ id: 3 }` at runtime + - Previously, generated types for params incorrectly modeled repeated params with an array + - For example, `/a/1/b/2/c/3` generated a type like `{ id: [1,2,3] }`. + - To be consistent with runtime behavior, the generated types now correctly model the "last one wins" semantics of path parameters. + - For example, `/a/1/b/2/c/3` now generates a type like `{ id: 3 }`. +- `@react-router/dev` - Fix path to load `package.json` for `react-router --version` ([#13012](https://github.com/remix-run/react-router/pull/13012)) + +### Unstable Changes + +โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - Add `unstable_SerializesTo` brand type for library authors to register types serializable by React Router's streaming format (`turbo-stream`) ([#12264](https://github.com/remix-run/react-router/pull/12264)) +- `@react-router/dev` - Add unstable support for splitting route modules in framework mode via `future.unstable_splitRouteModules` ([#11871](https://github.com/remix-run/react-router/pull/11871)) +- `@react-router/dev` - Add `future.unstable_viteEnvironmentApi` flag to enable experimental Vite Environment API support ([#12936](https://github.com/remix-run/react-router/pull/12936)) + +#### Split Route Modules (unstable) + +> โš ๏ธ This feature is currently [unstable](https://reactrouter.com/community/api-development-strategy#unstable-flags), enabled by the `future.unstable_splitRouteModules` flag. Weโ€™d love any interested users to play with it locally and provide feedback, but we do not recommend using it in production yet. +> +> If you do choose to adopt this flag in production, please ensure you do sufficient testing against your production build to ensure that the optimization is working as expected. + +One of the conveniences of the [Route Module API](https://reactrouter.com/start/framework/route-module) is that everything a route needs is in a single file. Unfortunately this comes with a performance cost in some cases when using the `clientLoader`, `clientAction`, and `HydrateFallback` APIs. + +As a basic example, consider this route module: + +```tsx filename=routes/example.tsx +import { MassiveComponent } from "~/components"; + +export async function clientLoader() { + return await fetch("http://github.com/service/https://example.com/api").then((response) => + response.json(), + ); +} + +export default function Component({ loaderData }) { + return ; +} +``` + +In this example we have a minimal `clientLoader` export that makes a basic fetch call, whereas the default component export is much larger. This is a problem for performance because it means that if we want to navigate to this route client-side, the entire route module must be downloaded before the client loader can start running. + +To visualize this as a timeline: + +In the following timeline diagrams, different characters are used within the Route Module bars to denote the different Route Module APIs being exported. + +``` +Get Route Module: |--=======| +Run clientLoader: |-----| +Render: |-| +``` + +Instead, we want to optimize this to the following: + +``` +Get clientLoader: |--| +Get Component: |=======| +Run clientLoader: |-----| +Render: |-| +``` + +To achieve this optimization, React Router will split the route module into multiple smaller modules during the production build process. In this case, we'll end up with two separate [virtual modules](https://vite.dev/guide/api-plugin#virtual-modules-convention) โ€” one for the client loader and one for the component and its dependencies. + +```tsx filename=routes/example.tsx?route-chunk=clientLoader +export async function clientLoader() { + return await fetch("http://github.com/service/https://example.com/api").then((response) => + response.json(), + ); +} +``` + +```tsx filename=routes/example.tsx?route-chunk=main +import { MassiveComponent } from "~/components"; + +export default function Component({ loaderData }) { + return ; +} +``` + +> ๐Ÿ’ก This optimization is automatically applied in framework mode, but you can also implement it in library mode via `route.lazy` and authoring your route in multiple files as covered in our blog post on [lazy loading route modules.](https://remix.run/blog/lazy-loading-routes#advanced-usage-and-optimizations) + +Now that these are available as separate modules, the client loader and the component can be downloaded in parallel. This means that the client loader can be executed as soon as it's ready without having to wait for the component. + +This optimization is even more pronounced when more Route Module APIs are used. For example, when using `clientLoader`, `clientAction` and `HydrateFallback`, the timeline for a single route module during a client-side navigation might look like this: + +``` +Get Route Module: |--~~++++=======| +Run clientLoader: |-----| +Render: |-| +``` + +This would instead be optimized to the following: + +``` +Get clientLoader: |--| +Get clientAction: |~~| +Get HydrateFallback: SKIPPED +Get Component: |=======| +Run clientLoader: |-----| +Render: |-| +``` + +Note that this optimization only works when the Route Module APIs being split don't share code within the same file. For example, the following route module can't be split: + +```tsx filename=routes/example.tsx +import { MassiveComponent } from "~/components"; + +const shared = () => console.log("hello"); + +export async function clientLoader() { + shared(); + return await fetch("http://github.com/service/https://example.com/api").then((response) => + response.json(), + ); +} + +export default function Component({ loaderData }) { + shared(); + return ; +} +``` + +This route will still work, but since both the client loader and the component depend on the `shared` function defined within the same file, it will be de-optimized into a single route module. + +To avoid this, you can extract any code shared between exports into a separate file. For example: + +```tsx filename=routes/example/shared.tsx +export const shared = () => console.log("hello"); +``` + +You can then import this shared code in your route module without triggering the de-optimization: + +```tsx filename=routes/example/route.tsx +import { MassiveComponent } from "~/components"; +import { shared } from "./shared"; + +export async function clientLoader() { + shared(); + return await fetch("http://github.com/service/https://example.com/api").then((response) => + response.json(), + ); +} + +export default function Component({ loaderData }) { + shared(); + return ; +} +``` + +Since the shared code is in its own module, React Router is now able to split this route module into two separate virtual modules: + +```tsx filename=routes/example/route.tsx?route-chunk=clientLoader +import { shared } from "./shared"; + +export async function clientLoader() { + shared(); + return await fetch("http://github.com/service/https://example.com/api").then((response) => + response.json(), + ); +} +``` + +```tsx filename=routes/example/route.tsx?route-chunk=main +import { MassiveComponent } from "~/components"; +import { shared } from "./shared"; + +export default function Component({ loaderData }) { + shared(); + return ; +} +``` + +If your project is particularly performance sensitive, you can set the `unstable_splitRouteModules` future flag to `"enforce"`: + +```tsx filename=react-router-config.ts +export default { + future: { + unstable_splitRouteModules: "enforce", + }, +}; +``` + +This setting will raise an error if any route modules can't be split: + +``` +Error splitting route module: routes/example/route.tsx + +- clientLoader + +This export could not be split into its own chunk because it shares code with other exports. You should extract any shared code into its own module and then import it within the route module. +``` + +### Changes by Package + +- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/create-react-router/CHANGELOG.md#720) +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router/CHANGELOG.md#720) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-architect/CHANGELOG.md#720) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-cloudflare/CHANGELOG.md#720) +- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-dev/CHANGELOG.md#720) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-express/CHANGELOG.md#720) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-fs-routes/CHANGELOG.md#720) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-node/CHANGELOG.md#720) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#720) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-serve/CHANGELOG.md#720) + +**Full Changelog**: [`v7.1.5...v7.2.0`](https://github.com/remix-run/react-router/compare/react-router@7.1.5...react-router@7.2.0) + +## v7.1.5 + +Date: 2025-01-31 + +### Patch Changes + +- `react-router` - Fix regression introduced in `7.1.4` via [#12800](https://github.com/remix-run/react-router/pull/12800) that caused issues navigating to hash routes inside splat routes for applications using Lazy Route Discovery (`patchRoutesOnNavigation`) ([#12927](https://github.com/remix-run/react-router/pull/12927)) + +**Full Changelog**: [`v7.1.4...v7.1.5`](https://github.com/remix-run/react-router/compare/react-router@7.1.4...react-router@7.1.5) + +## v7.1.4 + +Date: 2025-01-30 + +### Patch Changes + +- `@react-router/dev` - Properly resolve Windows file paths to scan for Vite's dependency optimization when using the `unstable_optimizeDeps` future flag ([#12637](https://github.com/remix-run/react-router/pull/12637)) +- `@react-router/dev` - Fix prerendering when using a custom server - previously we ended up trying to import the users custom server when we actually want to import the virtual server build module ([#12759](https://github.com/remix-run/react-router/pull/12759)) +- `react-router` - Properly handle status codes that cannot have a body in single fetch responses (204, etc.) ([#12760](https://github.com/remix-run/react-router/pull/12760)) +- `react-router` - Properly bubble headers as `errorHeaders` when throwing a `data()` result ([#12846](https://github.com/remix-run/react-router/pull/12846)) + - Avoid duplication of `Set-Cookie` headers if also returned from `headers` +- `react-router` - Stop erroring on resource routes that return raw strings/objects and instead serialize them as `text/plain` or `application/json` responses ([#12848](https://github.com/remix-run/react-router/pull/12848)) + - This only applies when accessed as a resource route without the `.data` extension + - When accessed from a Single Fetch `.data` request, they will still be encoded via `turbo-stream` +- `react-router` - Optimize Lazy Route Discovery path discovery to favor a single `querySelectorAll` call at the `body` level instead of many calls at the sub-tree level ([#12731](https://github.com/remix-run/react-router/pull/12731)) +- `react-router` - Optimize route matching by skipping redundant `matchRoutes` calls when possible ([#12800](https://github.com/remix-run/react-router/pull/12800), [#12882](https://github.com/remix-run/react-router/pull/12882)) +- `react-router` - Internal reorg to clean up some duplicated route module types ([#12799](https://github.com/remix-run/react-router/pull/12799)) + +**Full Changelog**: [`v7.1.3...v7.1.4`](https://github.com/remix-run/react-router/compare/react-router@7.1.3...react-router@7.1.4) + +## v7.1.3 + +Date: 2025-01-17 + +### Patch Changes + +- `@react-router/dev` - Fix `reveal` and `routes` CLI commands ([#12745](https://github.com/remix-run/react-router/pull/12745)) + +**Full Changelog**: [`v7.1.2...v7.1.3`](https://github.com/remix-run/react-router/compare/react-router@7.1.2...react-router@7.1.3) + +## v7.1.2 + +Date: 2025-01-16 + +### Patch Changes + +- `react-router` - Fix issue with fetcher data cleanup in the data layer on fetcher unmount ([#12681](https://github.com/remix-run/react-router/pull/12681)) +- `react-router` - Do not rely on `symbol` for filtering out `redirect` responses from loader data ([#12694](https://github.com/remix-run/react-router/pull/12694)) + - Previously, some projects were getting type checking errors like: + ```ts + error TS4058: Return type of exported function has or is using name 'redirectSymbol' from external module "node_modules/..." but cannot be named. + ``` + - Now that `symbol`s are not used for the `redirect` response type, these errors should no longer be present +- `@react-router/dev` - Fix default external conditions in Vite v6 ([#12644](https://github.com/remix-run/react-router/pull/12644)) + - This fixes resolution issues with certain npm packages +- `@react-router/dev` - Fix mismatch in prerendering html/data files when path is missing a leading slash ([#12684](https://github.com/remix-run/react-router/pull/12684)) +- `@react-router/dev` - Use `module-sync` server condition when enabled in the runtime. This fixes React context mismatches (e.g. `useHref() may be used only in the context of a component.`) during development on Node 22.10.0+ when using libraries that have a peer dependency on React Router ([#12729](https://github.com/remix-run/react-router/pull/12729)) +- `@react-router/dev` - Fix `react-refresh` source maps ([#12686](https://github.com/remix-run/react-router/pull/12686)) + +**Full Changelog**: [`v7.1.1...v7.1.2`](https://github.com/remix-run/react-router/compare/react-router@7.1.1...react-router@7.1.2) + +## v7.1.1 + +Date: 2024-12-23 + +### Patch Changes + +- `@react-router/dev` - Fix for a crash when optional args are passed to the CLI ([#12609](https://github.com/remix-run/react-router/pull/12609)) + +**Full Changelog**: [`v7.1.0...v7.1.1`](https://github.com/remix-run/react-router/compare/react-router@7.1.0...react-router@7.1.1) + +## v7.1.0 + +Date: 2024-12-20 + +### Minor Changes + +- Add support for Vite v6 ([#12469](https://github.com/remix-run/react-router/pull/12469)) + +### Patch Changes + +- `react-router` - Throw unwrapped Single Fetch `redirect` to align with pre-Single Fetch behavior ([#12506](https://github.com/remix-run/react-router/pull/12506)) +- `react-router` - Ignore redirects when inferring loader data types ([#12527](https://github.com/remix-run/react-router/pull/12527)) +- `react-router` - Remove `` warning which suffers from false positives in a lazy route discovery world ([#12485](https://github.com/remix-run/react-router/pull/12485)) +- `create-react-router` - Fix missing `fs-extra` dependency ([#12556](https://github.com/remix-run/react-router/pull/12556)) +- `@react-router/dev`/`@react-router/serve` - Properly initialize `NODE_ENV` if not already set for compatibility with React 19 ([#12578](https://github.com/remix-run/react-router/pull/12578)) +- `@react-router/dev` - Remove the leftover/unused `abortDelay` prop from `ServerRouter` and update the default `entry.server.tsx` to use the new `streamTimeout` value for Single Fetch ([#12478](https://github.com/remix-run/react-router/pull/12478)) + - The `abortDelay` functionality was removed in v7 as it was coupled to the `defer` implementation from Remix v2, but this removal of this prop was missed + - If you were still using this prop in your `entry.server` file, it's likely your app is not aborting streams as you would expect and you will need to adopt the new [`streamTimeout`](https://reactrouter.com/explanation/special-files#streamtimeout) value introduced with Single Fetch +- `@react-router/fs-routes` - Throw error in `flatRoutes` if routes directory is missing ([#12407](https://github.com/remix-run/react-router/pull/12407)) + +### Changes by Package + +- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/create-react-router/CHANGELOG.md#710) +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router/CHANGELOG.md#710) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-architect/CHANGELOG.md#710) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-cloudflare/CHANGELOG.md#710) +- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-dev/CHANGELOG.md#710) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-express/CHANGELOG.md#710) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-fs-routes/CHANGELOG.md#710) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-node/CHANGELOG.md#710) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#710) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-serve/CHANGELOG.md#710) + +**Full Changelog**: [`v7.0.2...v7.1.0`](https://github.com/remix-run/react-router/compare/react-router@7.0.2...react-router@7.1.0) + +## v7.0.2 + +Date: 2024-12-02 + +### Patch Changes + +- `react-router` - Temporarily only use one build in export map so packages can have a peer dependency on react router ([#12437](https://github.com/remix-run/react-router/pull/12437)) +- `@react-router/dev` - Support `moduleResolution` `Node16` and `NodeNext` ([#12440](https://github.com/remix-run/react-router/pull/12440)) +- `@react-router/dev` - Generate wide `matches` and `params` types for child routes ([#12397](https://github.com/remix-run/react-router/pull/12397)) + - At runtime, `matches` includes child route matches and `params` include child route path parameters + - But previously, we only generated types for parent routes and the current route in `matches` and `params` + - To align our generated types more closely to the runtime behavior, we now generate more permissive, wider types when accessing child route information + +**Full Changelog**: [`v7.0.1...v7.0.2`](https://github.com/remix-run/react-router/compare/react-router@7.0.1...react-router@7.0.2) + +## v7.0.1 + +Date: 2024-11-22 + +### Patch Changes + +- `@react-router/dev` - Ensure typegen file watcher is cleaned up when Vite dev server restarts ([#12331](https://github.com/remix-run/react-router/pull/12331)) +- `@react-router/dev` - Pass route `error` to `ErrorBoundary` as a prop ([#12338](https://github.com/remix-run/react-router/pull/12338)) + +**Full Changelog**: [`v7.0.0...v7.0.1`](https://github.com/remix-run/react-router/compare/react-router@7.0.0...react-router@7.0.1) + +## v7.0.0 + +Date: 2024-11-21 + +### Breaking Changes + +#### Package Restructuring + +- The `react-router-dom`, `@remix-run/react`, `@remix-run/server-runtime`, and `@remix-run/router` have been collapsed into the `react-router` package + - To ease migration, `react-router-dom` is still published in v7 as a re-export of everything from `react-router` +- The `@remix-run/cloudflare-pages` and `@remix-run/cloudflare-workers` have been collapsed into `@react-router/cloudflare` package` +- The `react-router-dom-v5-compat` and `react-router-native` packages are removed starting with v7 + +#### Removed Adapter Re-exports + +Remix v2 used to re-export all common `@remix-run/server-runtime` APIs through the various runtime packages (`node`, `cloudflare`, `deno`) so that you wouldn't need an additional `@remix-run/server-runtime` dependency in your `package.json`. With the collapsing of packages into `react-router`, these common APIs are now no longer re-exported through the runtime adapters. You should import all common APIs from `react-router`, and only import runtime-specific APIs from the runtime packages: + +```jsx +// Runtime-specific APIs +import { createFileSessionStorage } from "@react-router/node"; +// Runtime-agnostic APIs +import { redirect, useLoaderData } from "react-router"; +``` + +#### Removed APIs + +The following APIs have been removed in React Router v7: + +- `json` +- `defer` +- `unstable_composeUploadHandlers` +- `unstable_createMemoryUploadHandler` +- `unstable_parseMultipartFormData` + +#### Minimum Versions + +React Router v7 requires the following minimum versions: + +- `node@20` + - React Router no longer provides an `installGlobals` method to [polyfill](https://reactrouter.com/dev/guides/deploying/custom-node#polyfilling-fetch) the `fetch` API +- `react@18`, `react-dom@18` + +#### Adopted Future Flag Behaviors + +Remix and React Router follow an [API Development Strategy](https://reactrouter.com/en/main/guides/api-development-strategy) leveraging "Future Flags" to avoid introducing a slew of breaking changes in a major release. Instead, breaking changes are introduced in minor releases behind a flag, allowing users to opt-in at their convenience. In the next major release, all future flag behaviors become the default behavior. + +The following previously flagged behaviors are now the default in React Router v7: + +- [React Router v6 flags](https://reactrouter.com/en/v6/upgrading/future) + - `future.v7_relativeSplatPath` + - `future.v7_startTransition` + - `future.v7_fetcherPersist` + - `future.v7_normalizeFormMethod` + - `future.v7_partialHydration` + - `future.v7_skipActionStatusRevalidation` +- [Remix v2 flags](https://remix.run/docs/en/v2/start/future-flags) + - `future.v3_fetcherPersist` + - `future.v3_relativeSplatPath` + - `future.v3_throwAbortReason` + - `future.v3_singleFetch` + - `future.v3_lazyRouteDiscovery` + - `future.v3_optimizeDeps` + +#### Vite Compiler + +The [Remix Vite plugin](https://remix.run/docs/en/2.12.1/start/future-flags#vite-plugin) is the proper way to build full-stack SSR apps using React Router v7. The former `esbuild`-based compiler is no longer available. + +**Renamed `vitePlugin` and `cloudflareDevProxyVitePlugin`** + +For Remix consumers migrating to React Router, the `vitePlugin` and `cloudflareDevProxyVitePlugin` exports have been renamed and moved ([#11904](https://github.com/remix-run/react-router/pull/11904)) + +```diff +-import { +- vitePlugin as remix, +- cloudflareDevProxyVitePlugin, +-} from "@remix/dev"; + ++import { reactRouter } from "@react-router/dev/vite"; ++import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare"; +``` + +**Removed `manifest` option** + +For Remix consumers migrating to React Router, the Vite plugin's `manifest` option has been removed. The `manifest` option been superseded by the more powerful `buildEnd` hook since it's passed the `buildManifest` argument. You can still write the build manifest to disk if needed, but you'll most likely find it more convenient to write any logic depending on the build manifest within the `buildEnd` hook itself. ([#11573](https://github.com/remix-run/react-router/pull/11573)) + +If you were using the `manifest` option, you can replace it with a `buildEnd` hook that writes the manifest to disk like this: + +```js +// react-router.config.ts +import { type Config } from "@react-router/dev/config"; +import { writeFile } from "node:fs/promises"; + +export default { + async buildEnd({ buildManifest }) { + await writeFile( + "build/manifest.json", + JSON.stringify(buildManifest, null, 2), + "utf-8" + ); + }, +} satisfies Config; +``` + +#### Exposed Router Promises + +Because React 19 will have first-class support for handling promises in the render pass (via `React.use` and `useAction`), we are now comfortable exposing the promises for the APIs that previously returned `undefined`: + +- `useNavigate()` +- `useSubmit()` +- `useFetcher().load` +- `useFetcher().submit` +- `useRevalidator().revalidate()` + +### Other Notable Changes + +#### `routes.ts` + +When using the React Router Vite plugin, routes are defined in `app/routes.ts`. Route config is exported via the `routes` export, conforming to the `RouteConfig` type. Route helper functions `route`, `index`, and `layout` are provided to make declarative type-safe route definitions easier. + +```ts +// app/routes.ts +import { + type RouteConfig, + route, + index, + layout, +} from "@react-router/dev/routes"; + +export const routes: RouteConfig = [ + index("./home.tsx"), + route("about", "./about.tsx"), + + layout("./auth/layout.tsx", [ + route("login", "./auth/login.tsx"), + route("register", "./auth/register.tsx"), + ]), + + route("concerts", [ + index("./concerts/home.tsx"), + route(":city", "./concerts/city.tsx"), + route("trending", "./concerts/trending.tsx"), + ]), +]; +``` + +For Remix consumers migrating to React Router, you can still configure file system routing within `routes.ts` using the `@react-router/fs-routes` package. A minimal route config that reproduces the default Remix setup looks like this: + +```ts +// app/routes.ts +import { type RouteConfig } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +export const routes: RouteConfig = flatRoutes(); +``` + +If you want to migrate from file system routing to config-based routes, you can mix and match approaches by spreading the results of the async `flatRoutes` function into the array of config-based routes. + +```ts +// app/routes.ts +import { type RouteConfig, route } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +export const routes: RouteConfig = [ + // Example config-based route: + route("/hello", "./routes/hello.tsx"), + + // File system routes scoped to a different directory: + ...(await flatRoutes({ + rootDirectory: "fs-routes", + })), +]; +``` + +If you were using Remix's `routes` option to use alternative file system routing conventions, you can adapt these to the new `RouteConfig` format using `@react-router/remix-config-routes-adapter`. + +For example, if you were using [Remix v1 route conventions](https://remix.run/docs/en/1.19.3/file-conventions/routes-files) in Remix v2, you can combine `@react-router/remix-config-routes-adapter` with `@remix-run/v1-route-convention` to adapt this to React Router: + +```ts +// app/routes.ts +import { type RouteConfig } from "@react-router/dev/routes"; +import { remixConfigRoutes } from "@react-router/remix-config-routes-adapter"; +import { createRoutesFromFolders } from "@remix-run/v1-route-convention"; + +export const routes: RouteConfig = remixConfigRoutes(async (defineRoutes) => { + return createRoutesFromFolders(defineRoutes, { + ignoredFilePatterns: ["**/.*", "**/*.css"], + }); +}); +``` + +Also note that, if you were using Remix's `routes` option to define config-based routes, you can also adapt these to the new `RouteConfig` format using `@react-router/remix-config-routes-adapter` with minimal code changes. While this makes for a fast migration path, we recommend migrating any config-based routes from Remix to the new `RouteConfig` format since it's a fairly straightforward migration. + +```diff +// app/routes.ts +-import { type RouteConfig } from "@react-router/dev/routes"; ++import { type RouteConfig, route } from "@react-router/dev/routes"; +-import { remixConfigRoutes } from "@react-router/remix-config-routes-adapter"; + +-export const routes: RouteConfig = remixConfigRoutes(async (defineRoutes) => { +- defineRoutes((route) => { +- route("/parent", "./routes/parent.tsx", () => [ +- route("/child", "./routes/child.tsx"), +- ]); +- }); +-}); ++export const routes: RouteConfig = [ ++ route("/parent", "./routes/parent.tsx", [ ++ route("/child", "./routes/child.tsx"), ++ ]), ++]; +``` + +#### Type-safety improvements + +React Router now generates types for each of your route modules and passes typed props to route module component exports ([#11961](https://github.com/remix-run/react-router/pull/11961), [#12019](https://github.com/remix-run/react-router/pull/12019)). You can access those types by importing them from `./+types/`. + +See [_How To > Route Module Type Safety_](https://reactrouter.com/dev/how-to/route-module-type-safety) and [_Explanations > Type Safety_](https://reactrouter.com/dev/explanation/type-safety) for more details. + +#### Prerendering + +React Router v7 includes a new `prerender` config in the vite plugin to support SSG use-cases. This will pre-render your `.html` and `.data` files at build time and so you can serve them statically at runtime from a running server or a CDN ([#11539](https://github.com/remix-run/react-router/pull/11539)) + +```ts +export default defineConfig({ + plugins: [ + reactRouter({ + async prerender({ getStaticPaths }) { + let slugs = await fakeGetSlugsFromCms(); + return [ + ...getStaticPaths(), + ...slugs.map((slug) => `/product/${slug}`), + ]; + }, + }), + tsconfigPaths(), + ], +}); + +async function fakeGetSlugsFromCms() { + await new Promise((r) => setTimeout(r, 1000)); + return ["shirt", "hat"]; +} +``` + +### Major Changes (`react-router`) + +- Remove the original `defer` implementation in favor of using raw promises via single fetch and `turbo-stream` ([#11744](https://github.com/remix-run/react-router/pull/11744)) + - This removes these exports from React Router: + - `defer` + - `AbortedDeferredError` + - `type TypedDeferredData` + - `UNSAFE_DeferredData` + - `UNSAFE_DEFERRED_SYMBOL` +- Collapse packages into `react-router`([#11505](https://github.com/remix-run/react-router/pull/11505)) + - `@remix-run/router` + - `react-router-dom` + - `@remix-run/server-runtime` + - `@remix-run/testing` + - As a note, the `react-router-dom` package is maintained to ease adoption but it simply re-exports all APIs from `react-router` +- Drop support for Node 16, React Router SSR now requires Node 18 or higher ([#11391](https://github.com/remix-run/react-router/pull/11391), [#11690](https://github.com/remix-run/react-router/pull/11690)) +- Remove `future.v7_startTransition` flag ([#11696](https://github.com/remix-run/react-router/pull/11696)) +- Expose the underlying router promises from the following APIs for composition in React 19 APIs: ([#11521](https://github.com/remix-run/react-router/pull/11521)) +- Remove `future.v7_normalizeFormMethod` future flag ([#11697](https://github.com/remix-run/react-router/pull/11697)) +- Imports/Exports cleanup ([#11840](https://github.com/remix-run/react-router/pull/11840)) + - Removed the following exports that were previously public API from `@remix-run/router` + - types + - `AgnosticDataIndexRouteObject` + - `AgnosticDataNonIndexRouteObject` + - `AgnosticDataRouteMatch` + - `AgnosticDataRouteObject` + - `AgnosticIndexRouteObject` + - `AgnosticNonIndexRouteObject` + - `AgnosticRouteMatch` + - `AgnosticRouteObject` + - `TrackedPromise` + - `unstable_AgnosticPatchRoutesOnMissFunction` + - `Action` -> exported as `NavigationType` via `react-router` + - `Router` exported as `RemixRouter` to differentiate from RR's `` + - API + - `getToPathname` (`@private`) + - `joinPaths` (`@private`) + - `normalizePathname` (`@private`) + - `resolveTo` (`@private`) + - `stripBasename` (`@private`) + - `createBrowserHistory` -> in favor of `createBrowserRouter` + - `createHashHistory` -> in favor of `createHashRouter` + - `createMemoryHistory` -> in favor of `createMemoryRouter` + - `createRouter` + - `createStaticHandler` -> in favor of wrapper `createStaticHandler` in RR Dom + - `getStaticContextFromError` + - Removed the following exports that were previously public API from `react-router` + - `Hash` + - `Pathname` + - `Search` +- Remove `future.v7_prependBasename` from the internalized `@remix-run/router` package ([#11726](https://github.com/remix-run/react-router/pull/11726)) +- Remove `future.v7_throwAbortReason` from internalized `@remix-run/router` package ([#11728](https://github.com/remix-run/react-router/pull/11728)) +- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675)) +- Renamed `RemixContext` to `FrameworkContext` ([#11705](https://github.com/remix-run/react-router/pull/11705)) +- Update the minimum React version to 18 ([#11689](https://github.com/remix-run/react-router/pull/11689)) +- `PrefetchPageDescriptor` replaced by `PageLinkDescriptor` ([#11960](https://github.com/remix-run/react-router/pull/11960)) +- Remove the `future.v7_partialHydration` flag ([#11725](https://github.com/remix-run/react-router/pull/11725)) + - This also removes the `` prop + - To migrate, move the `fallbackElement` to a `hydrateFallbackElement`/`HydrateFallback` on your root route + - Also worth nothing there is a related breaking changer with this future flag: + - Without `future.v7_partialHydration` (when using `fallbackElement`), `state.navigation` was populated during the initial load + - With `future.v7_partialHydration`, `state.navigation` remains in an `"idle"` state during the initial load +- Remove `future.v7_relativeSplatPath` future flag ([#11695](https://github.com/remix-run/react-router/pull/11695)) +- Remove remaining future flags ([#11820](https://github.com/remix-run/react-router/pull/11820)) + - React Router `v7_skipActionErrorRevalidation` + - Remix `v3_fetcherPersist`, `v3_relativeSplatPath`, `v3_throwAbortReason` +- Rename `createRemixStub` to `createRoutesStub` ([#11692](https://github.com/remix-run/react-router/pull/11692)) +- Remove `@remix-run/router` deprecated `detectErrorBoundary` option in favor of `mapRouteProperties` ([#11751](https://github.com/remix-run/react-router/pull/11751)) +- Add `react-router/dom` subpath export to properly enable `react-dom` as an optional `peerDependency` ([#11851](https://github.com/remix-run/react-router/pull/11851)) + - This ensures that we don't blindly `import ReactDOM from "react-dom"` in `` in order to access `ReactDOM.flushSync()`, since that would break `createMemoryRouter` use cases in non-DOM environments + - DOM environments should import from `react-router/dom` to get the proper component that makes `ReactDOM.flushSync()` available: + - If you are using the Vite plugin, use this in your `entry.client.tsx`: + - `import { HydratedRouter } from 'react-router/dom'` + - If you are not using the Vite plugin and are manually calling `createBrowserRouter`/`createHashRouter`: + - `import { RouterProvider } from "react-router/dom"` +- Remove `future.v7_fetcherPersist` flag ([#11731](https://github.com/remix-run/react-router/pull/11731)) +- Allow returning `undefined` from loaders and actions ([#11680](https://github.com/remix-run/react-router/pull/11680), [#12057]([https://github.com/remix-run/react-router/pull/1205)) +- Use `createRemixRouter`/`RouterProvider` in `entry.client` instead of `RemixBrowser` ([#11469](https://github.com/remix-run/react-router/pull/11469)) +- Remove the deprecated `json` utility ([#12146](https://github.com/remix-run/react-router/pull/12146)) + - You can use [`Response.json`](https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static) if you still need to construct JSON responses in your app + +### Major Changes (`@react-router/*`) + +- Remove `future.v3_singleFetch` flag ([#11522](https://github.com/remix-run/react-router/pull/11522)) +- Drop support for Node 16 and 18, update minimum Node version to 20 ([#11690](https://github.com/remix-run/react-router/pull/11690), [#12171](https://github.com/remix-run/react-router/pull/12171)) + - Remove `installGlobals()` as this should no longer be necessary +- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675)) +- No longer re-export APIs from `react-router` through different runtime/adapter packages ([#11702](https://github.com/remix-run/react-router/pull/11702)) +- For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs + - This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) + - `createCookie` + - `createCookieSessionStorage` + - `createMemorySessionStorage` + - `createSessionStorage` + - For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation](https://nodejs.org/api/webcrypto.html) + - Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: + - `createCookieFactory` + - `createSessionStorageFactory` + - `createCookieSessionStorageFactory` + - `createMemorySessionStorageFactory` +- Consolidate types previously duplicated across `@remix-run/router`, `@remix-run/server-runtime`, and `@remix-run/react` now that they all live in `react-router` ([#12177](https://github.com/remix-run/react-router/pull/12177)) + - Examples: `LoaderFunction`, `LoaderFunctionArgs`, `ActionFunction`, `ActionFunctionArgs`, `DataFunctionArgs`, `RouteManifest`, `LinksFunction`, `Route`, `EntryRoute` + - The `RouteManifest` type used by the "remix" code is now slightly stricter because it is using the former `@remix-run/router` `RouteManifest` + - `Record -> Record` + - Removed `AppData` type in favor of inlining `unknown` in the few locations it was used + - Removed `ServerRuntimeMeta*` types in favor of the `Meta*` types they were duplicated from +- Migrate Remix v2 type generics to React Router ([#12180](https://github.com/remix-run/react-router/pull/12180)) + - These generics are provided for Remix v2 migration purposes + - These generics and the APIs they exist on should be considered informally deprecated in favor of the new `Route.*` types + - Anyone migrating from React Router v6 should probably not leverage these new generics and should migrate straight to the `Route.*` types + - For React Router v6 users, these generics are new and should not impact your app, with one exception + - `useFetcher` previously had an optional generic (used primarily by Remix v2) that expected the data type + - This has been updated in v7 to expect the type of the function that generates the data (i.e., `typeof loader`/`typeof action`) + - Therefore, you should update your usages: + - โŒ `useFetcher()` + - โœ… `useFetcher()` +- Update `cookie` dependency to `^1.0.1` - please see the [release notes](https://github.com/jshttp/cookie/releases) for any breaking changes ([#12172](https://github.com/remix-run/react-router/pull/12172)) +- `@react-router/cloudflare` - For Remix consumers migrating to React Router, all exports from `@remix-run/cloudflare-pages` are now provided for React Router consumers in the `@react-router/cloudflare` package. There is no longer a separate package for Cloudflare Pages. ([#11801](https://github.com/remix-run/react-router/pull/11801)) +- `@react-router/cloudflare` - The `@remix-run/cloudflare-workers` package has been deprecated. Remix consumers migrating to React Router should use the `@react-router/cloudflare` package directly. For guidance on how to use `@react-router/cloudflare` within a Cloudflare Workers context, refer to the Cloudflare Workers template. ([#11801](https://github.com/remix-run/react-router/pull/11801)) +- `@react-router/dev` - For Remix consumers migrating to React Router, the `vitePlugin` and `cloudflareDevProxyVitePlugin` exports have been renamed and moved. ([#11904](https://github.com/remix-run/react-router/pull/11904)) +- `@react-router/dev` - For Remix consumers migrating to React Router who used the Vite plugin's `buildEnd` hook, the resolved `reactRouterConfig` object no longer contains a `publicPath` property since this belongs to Vite, not React Router ([#11575](https://github.com/remix-run/react-router/pull/11575)) +- `@react-router/dev` - For Remix consumers migrating to React Router, the Vite plugin's `manifest` option has been removed ([#11573](https://github.com/remix-run/react-router/pull/11573)) +- `@react-router/dev` - Update default `isbot` version to v5 and drop support for `isbot@3` ([#11770](https://github.com/remix-run/react-router/pull/11770)) + - If you have `isbot@4` or `isbot@5` in your `package.json`: + - You do not need to make any changes + - If you have `isbot@3` in your `package.json` and you have your own `entry.server.tsx` file in your repo + - You do not need to make any changes + - You can upgrade to `isbot@5` independent of the React Router v7 upgrade + - If you have `isbot@3` in your `package.json` and you do not have your own `entry.server.tsx` file in your repo + - You are using the internal default entry provided by React Router v7 and you will need to upgrade to `isbot@5` in your `package.json` +- `@react-router/dev` - For Remix consumers migrating to React Router, Vite manifests (i.e. `.vite/manifest.json`) are now written within each build subdirectory, e.g. `build/client/.vite/manifest.json` and `build/server/.vite/manifest.json` instead of `build/.vite/client-manifest.json` and `build/.vite/server-manifest.json`. This means that the build output is now much closer to what you'd expect from a typical Vite project. ([#11573](https://github.com/remix-run/react-router/pull/11573)) + - Originally the Remix Vite plugin moved all Vite manifests to a root-level `build/.vite` directory to avoid accidentally serving them in production, particularly from the client build. This was later improved with additional logic that deleted these Vite manifest files at the end of the build process unless Vite's `build.manifest` had been enabled within the app's Vite config. This greatly reduced the risk of accidentally serving the Vite manifests in production since they're only present when explicitly asked for. As a result, we can now assume that consumers will know that they need to manage these additional files themselves, and React Router can safely generate a more standard Vite build output. + +### Minor Changes + +- `react-router` - Params, loader data, and action data as props for route component exports ([#11961](https://github.com/remix-run/react-router/pull/11961)) +- `react-router` - Add route module type generation ([#12019](https://github.com/remix-run/react-router/pull/12019)) +- `react-router` - Remove duplicate `RouterProvider` implementations ([#11679](https://github.com/remix-run/react-router/pull/11679)) +- `react-router` - Stabilize `unstable_dataStrategy` ([#11969](https://github.com/remix-run/react-router/pull/11969)) +- `react-router` - Stabilize `unstable_patchRoutesOnNavigation` ([#11970](https://github.com/remix-run/react-router/pull/11970)) +- `react-router` - Add prefetching support to `Link`/`NavLink` when using Remix SSR ([#11402](https://github.com/remix-run/react-router/pull/11402)) +- `react-router` - Enhance `ScrollRestoration` so it can restore properly on an SSR'd document load ([#11401](https://github.com/remix-run/react-router/pull/11401)) +- `@react-router/dev` - Add support for the `prerender` config in the React Router vite plugin, to support existing SSG use-cases ([#11539](https://github.com/remix-run/react-router/pull/11539)) +- `@react-router/dev` - Remove internal `entry.server.spa.tsx` implementation which was not compatible with the Single Fetch async hydration approach ([#11681](https://github.com/remix-run/react-router/pull/11681)) +- `@react-router/serve`: Update `express.static` configurations to support new `prerender` API ([#11547](https://github.com/remix-run/react-router/pull/11547)) + - Assets in the `build/client/assets` folder are served as before, with a 1-year immutable `Cache-Control` header + - Static files outside of assets, such as pre-rendered `.html` and `.data` files are not served with a specific `Cache-Control` header + - `.data` files are served with `Content-Type: text/x-turbo` + - For some reason, when adding this via `express.static`, it seems to also add a `Cache-Control: public, max-age=0` to `.data` files + +### Patch Changes + +- Replace `substr` with `substring` ([#12080](https://github.com/remix-run/react-router/pull/12080)) +- `react-router` - Fix redirects returned from loaders/actions using `data()` ([#12021](https://github.com/remix-run/react-router/pull/12021)) +- `@react-router/dev` - Enable prerendering for resource routes ([#12200](https://github.com/remix-run/react-router/pull/12200)) +- `@react-router/dev` - resolve config directory relative to flat output file structure ([#12187](https://github.com/remix-run/react-router/pull/12187)) + +### Changes by Package + +- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router/CHANGELOG.md#700) +- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-architect/CHANGELOG.md#700) +- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-cloudflare/CHANGELOG.md#700) +- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-dev/CHANGELOG.md#700) +- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-express/CHANGELOG.md#700) +- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-fs-routes/CHANGELOG.md#700) +- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-node/CHANGELOG.md#700) +- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#700) +- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-serve/CHANGELOG.md#700) + +**Full Changelog**: [`v6.28.0...v7.0.0`](https://github.com/remix-run/react-router/compare/react-router@6.28.0...react-router@7.0.0) + +# React Router v6 Releases + +## v6.30.1 + +Date: 2025-05-20 + +### Patch Changes + +- Partially revert optimization added in `6.29.0` to reduce calls to `matchRoutes` because it surfaced other issues ([#13623](https://github.com/remix-run/react-router/pull/13623)) +- Stop logging invalid warning when `v7_relativeSplatPath` is set to `false` ([#13502](https://github.com/remix-run/react-router/pull/13502)) + +**Full Changelog**: [`v6.30.0...v6.30.1`](https://github.com/remix-run/react-router/compare/react-router@6.30.0...react-router@6.30.1) + +## v6.30.0 + +Date: 2025-02-27 + +### Minor Changes + +- Add `fetcherKey` as a parameter to `patchRoutesOnNavigation` ([#13109](https://github.com/remix-run/react-router/pull/13109)) + +### Patch Changes + +- Fix regression introduced in `6.29.0` via [#12169](https://github.com/remix-run/react-router/pull/12169) that caused issues navigating to hash routes inside splat routes for applications using Lazy Route Discovery (`patchRoutesOnNavigation`) ([#13108](https://github.com/remix-run/react-router/pull/13108)) + +**Full Changelog**: [`v6.29.0...v6.30.0`](https://github.com/remix-run/react-router/compare/react-router@6.29.0...react-router@6.30.0) + +## v6.29.0 + +Date: 2025-01-30 + +### Minor Changes + +- Provide the request `signal` as a parameter to `patchRoutesOnNavigation` ([#12900](https://github.com/remix-run/react-router/pull/12900)) + - This can be used to abort any manifest fetches if the in-flight navigation/fetcher is aborted + +### Patch Changes + +- Do not log v7 deprecation warnings in production builds ([#12794](https://github.com/remix-run/react-router/pull/12794)) +- Properly bubble headers when throwing a `data()` result ([#12845](https://github.com/remix-run/react-router/pull/12845)) +- Optimize route matching by skipping redundant `matchRoutes` calls when possible ([#12169](https://github.com/remix-run/react-router/pull/12169)) +- Strip search parameters from `patchRoutesOnNavigation` `path` param for fetcher calls ([#12899](https://github.com/remix-run/react-router/pull/12899)) + +**Full Changelog**: [`v6.28.2...v6.29.0`](https://github.com/remix-run/react-router/compare/react-router@6.28.2...react-router@6.29.0) + +## v6.28.2 + +Date: 2025-01-16 + +### Patch Changes + +- Fix manual fetcher `key` usage when not opted into `future.v7_fetcherPersist` ([#12674](https://github.com/remix-run/react-router/pull/12674)) +- Fix issue with fetcher data cleanup in the data layer on fetcher unmount ([#12674](https://github.com/remix-run/react-router/pull/12674)) + +**Full Changelog**: [`v6.28.1...v6.28.2`](https://github.com/remix-run/react-router/compare/react-router@6.28.1...react-router@6.28.2) + +## v6.28.1 + +Date: 2024-12-20 + +### Patch Changes + +- Allow users to opt out of v7 deprecation warnings by setting flags to `false` ([#12441](https://github.com/remix-run/react-router/pull/12441)) + +**Full Changelog**: [`v6.28.0...v6.28.1`](https://github.com/remix-run/react-router/compare/react-router@6.28.0...react-router@6.28.1) + +## v6.28.0 + +Date: 2024-11-06 + +### What's Changed + +- In preparation for v7 we've added deprecation warnings for any future flags that you have not yet opted into. Please use the flags to better prepare for eventually upgrading to v7. + +### Minor Changes + +- Log deprecation warnings for v7 flags ([#11750](https://github.com/remix-run/react-router/pull/11750)) + - Add deprecation warnings to `json`/`defer` in favor of returning raw objects + - These methods will be removed in React Router v7 + +### Patch Changes + +- Update JSDoc URLs for new website structure (add /v6/ segment) ([#12141](https://github.com/remix-run/react-router/pull/12141)) + +**Full Changelog**: [`v6.27.0...v6.28.0`](https://github.com/remix-run/react-router/compare/react-router@6.27.0...react-router@6.28.0) + +## v6.27.0 + +Date: 2024-10-11 + +### What's Changed + +#### Stabilized APIs + +This release stabilizes a handful of "unstable" APIs in preparation for the [pending](https://x.com/remix_run/status/1841926034868077009) React Router v7 release (see [these](https://remix.run/blog/merging-remix-and-react-router) [posts](https://remix.run/blog/incremental-path-to-react-19) for more info): + +- `unstable_dataStrategy` โ†’ `dataStrategy` (`createBrowserRouter` and friends) ([Docs](https://reactrouter.com/v6/routers/create-browser-router#optsdatastrategy)) +- `unstable_patchRoutesOnNavigation` โ†’ `patchRoutesOnNavigation` (`createBrowserRouter` and friends) ([Docs](https://reactrouter.com/v6/routers/create-browser-router#optspatchroutesonnavigation)) +- `unstable_flushSync` โ†’ `flushSync` (`useSubmit`, `fetcher.load`, `fetcher.submit`) ([Docs](https://reactrouter.com/v6/hooks/use-submit#optionsflushsync)) +- `unstable_viewTransition` โ†’ `viewTransition` (``, `
`, `useNavigate`, `useSubmit`) ([Docs](https://reactrouter.com/v6/components/link#viewtransition)) + +### Minor Changes + +- Stabilize the `unstable_flushSync` option for navigations and fetchers ([#11989](https://github.com/remix-run/react-router/pull/11989)) +- Stabilize the `unstable_viewTransition` option for navigations and the corresponding `unstable_useViewTransitionState` hook ([#11989](https://github.com/remix-run/react-router/pull/11989)) +- Stabilize `unstable_dataStrategy` ([#11974](https://github.com/remix-run/react-router/pull/11974)) +- Stabilize `unstable_patchRoutesOnNavigation` ([#11973](https://github.com/remix-run/react-router/pull/11973)) + - Add new `PatchRoutesOnNavigationFunctionArgs` type for convenience ([#11967](https://github.com/remix-run/react-router/pull/11967)) + +### Patch Changes + +- Fix bug when submitting to the current contextual route (parent route with an index child) when an `?index` param already exists from a prior submission ([#12003](https://github.com/remix-run/react-router/pull/12003)) +- Fix `useFormAction` bug - when removing `?index` param it would not keep other non-Remix `index` params ([#12003](https://github.com/remix-run/react-router/pull/12003)) +- Fix bug with fetchers not persisting `preventScrollReset` through redirects during concurrent fetches ([#11999](https://github.com/remix-run/react-router/pull/11999)) +- Avoid unnecessary `console.error` on fetcher abort due to back-to-back revalidation calls ([#12050](https://github.com/remix-run/react-router/pull/12050)) +- Fix bugs with `partialHydration` when hydrating with errors ([#12070](https://github.com/remix-run/react-router/pull/12070)) +- Remove internal cache to fix issues with interrupted `patchRoutesOnNavigation` calls ([#12055](https://github.com/remix-run/react-router/pull/12055)) + - โš ๏ธ This may be a breaking change if you were relying on this behavior in the `unstable_` API + - We used to cache in-progress calls to `patchRoutesOnNavigation` internally so that multiple navigations with the same start/end would only execute the function once and use the same promise + - However, this approach was at odds with `patch` short circuiting if a navigation was interrupted (and the `request.signal` aborted) since the first invocation's `patch` would no-op + - This cache also made some assumptions as to what a valid cache key might be - and is oblivious to any other application-state changes that may have occurred + - So, the cache has been removed because in _most_ cases, repeated calls to something like `import()` for async routes will already be cached automatically - and if not it's easy enough for users to implement this cache in userland +- Remove internal `discoveredRoutes` FIFO queue from `unstable_patchRoutesOnNavigation` ([#11977](https://github.com/remix-run/react-router/pull/11977)) + - โš ๏ธ This may be a breaking change if you were relying on this behavior in the `unstable_` API + - This was originally implemented as an optimization but it proved to be a bit too limiting + - If you need this optimization you can implement your own cache inside `patchRoutesOnNavigation` +- Fix types for `RouteObject` within `PatchRoutesOnNavigationFunction`'s `patch` method so it doesn't expect agnostic route objects passed to `patch` ([#11967](https://github.com/remix-run/react-router/pull/11967)) +- Expose errors thrown from `patchRoutesOnNavigation` directly to `useRouteError` instead of wrapping them in a 400 `ErrorResponse` instance ([#12111](https://github.com/remix-run/react-router/pull/12111)) + +**Full Changelog**: [`v6.26.2...v6.27.0`](https://github.com/remix-run/react-router/compare/react-router@6.26.2...react-router@6.27.0) + +## v6.26.2 + +Date: 2024-09-09 + +### Patch Changes + +- Update the `unstable_dataStrategy` API to allow for more advanced implementations ([#11943](https://github.com/remix-run/react-router/pull/11943)) + - โš ๏ธ If you have already adopted `unstable_dataStrategy`, please review carefully as this includes breaking changes to this API + - Rename `unstable_HandlerResult` to `unstable_DataStrategyResult` + - Change the return signature of `unstable_dataStrategy` from a parallel array of `unstable_DataStrategyResult[]` (parallel to `matches`) to a key/value object of `routeId => unstable_DataStrategyResult` + - This allows more advanced control over revalidation behavior because you can opt-into or out-of revalidating data that may not have been revalidated by default (via `match.shouldLoad`) + - You should now return/throw a result from your `handlerOverride` instead of returning a `DataStrategyResult` + - The return value (or thrown error) from your `handlerOverride` will be wrapped up into a `DataStrategyResult` and returned fromm `match.resolve` + - Therefore, if you are aggregating the results of `match.resolve()` into a final results object you should not need to think about the `DataStrategyResult` type + - If you are manually filling your results object from within your `handlerOverride`, then you will need to assign a `DataStrategyResult` as the value so React Router knows if it's a successful execution or an error (see examples in the documentation for details) + - Added a new `fetcherKey` parameter to `unstable_dataStrategy` to allow differentiation from navigational and fetcher calls +- Preserve opted-in view transitions through redirects ([#11925](https://github.com/remix-run/react-router/pull/11925)) +- Preserve pending view transitions through a router revalidation call ([#11917](https://github.com/remix-run/react-router/pull/11917)) +- Fix blocker usage when `blocker.proceed` is called quickly/synchronously ([#11930](https://github.com/remix-run/react-router/pull/11930)) + +**Full Changelog**: [`v6.26.1...v6.26.2`](https://github.com/remix-run/react-router/compare/react-router@6.26.1...react-router@6.26.2) + +## v6.26.1 + +Date: 2024-08-15 + +### Patch Changes + +- Rename `unstable_patchRoutesOnMiss` to `unstable_patchRoutesOnNavigation` to match new behavior ([#11888](https://github.com/remix-run/react-router/pull/11888)) +- Update `unstable_patchRoutesOnNavigation` logic so that we call the method when we match routes with dynamic param or splat segments in case there exists a higher-scoring static route that we've not yet discovered ([#11883](https://github.com/remix-run/react-router/pull/11883)) + - We also now leverage an internal FIFO queue of previous paths we've already called `unstable_patchRoutesOnNavigation` against so that we don't re-call on subsequent navigations to the same path + +**Full Changelog**: [`v6.26.0...v6.26.1`](https://github.com/remix-run/react-router/compare/react-router@6.26.0...react-router@6.26.1) + +## v6.26.0 + +Date: 2024-08-01 + +### Minor Changes + +- Add a new `replace(url, init?)` alternative to `redirect(url, init?)` that performs a `history.replaceState` instead of a `history.pushState` on client-side navigation redirects ([#11811](https://github.com/remix-run/react-router/pull/11811)) +- Add a new `unstable_data()` API for usage with Remix Single Fetch ([#11836](https://github.com/remix-run/react-router/pull/11836)) + - This API is not intended for direct usage in React Router SPA applications + - It is primarily intended for usage with `createStaticHandler.query()` to allow loaders/actions to return arbitrary data along with custom `status`/`headers` without forcing the serialization of data into a `Response` instance + - This allows for more advanced serialization tactics via `unstable_dataStrategy` such as serializing via `turbo-stream` in Remix Single Fetch + - โš ๏ธ This removes the `status` field from `HandlerResult` + - If you need to return a specific `status` from `unstable_dataStrategy` you should instead do so via `unstable_data()` + +### Patch Changes + +- Fix internal cleanup of interrupted fetchers to avoid invalid revalidations on navigations ([#11839](https://github.com/remix-run/react-router/pull/11839)) +- Fix initial hydration behavior when using `future.v7_partialHydration` along with `unstable_patchRoutesOnMiss` ([#11838](https://github.com/remix-run/react-router/pull/11838)) + - During initial hydration, `router.state.matches` will now include any partial matches so that we can render ancestor `HydrateFallback` components + +**Full Changelog**: [`v6.25.1...v6.26.0`](https://github.com/remix-run/react-router/compare/react-router@6.25.1...react-router@6.26.0) + +## v6.25.1 + +Date: 2024-07-17 + +### Patch Changes + +- Memoize some `RouterProvider` internals to reduce unnecessary re-renders ([#11803](https://github.com/remix-run/react-router/pull/11803)) + +**Full Changelog**: [`v6.25.0...v6.25.1`](https://github.com/remix-run/react-router/compare/react-router@6.25.0...react-router@6.25.1) + +## v6.25.0 + +Date: 2024-07-16 + +### What's Changed + +#### Stabilized `v7_skipActionErrorRevalidation` + +This release stabilizes the `future.unstable_skipActionErrorRevalidation` flag into [`future.v7_skipActionErrorRevalidation`](https://reactrouter.com/v6/upgrading/future#v7_skipactionstatusrevalidation) in preparation for the upcoming React Router v7 release. + +- When this flag is enabled, actions that return/throw a `4xx/5xx` `Response` will not trigger a revalidation by default +- This also stabilizes `shouldRevalidate`'s `unstable_actionStatus` parameter to `actionStatus` + +### Minor Changes + +- Stabilize `future.unstable_skipActionErrorRevalidation` as `future.v7_skipActionErrorRevalidation` ([#11769](https://github.com/remix-run/react-router/pull/11769)) + +### Patch Changes + +- Fix regression and properly decode paths inside `useMatch` so matches/params reflect decoded params ([#11789](https://github.com/remix-run/react-router/pull/11789)) +- Fix bubbling of errors thrown from `unstable_patchRoutesOnMiss` ([#11786](https://github.com/remix-run/react-router/pull/11786)) +- Fix hydration in SSR apps using `unstable_patchRoutesOnMiss` that matched a splat route on the server ([#11790](https://github.com/remix-run/react-router/pull/11790)) + +**Full Changelog**: [`v6.24.1...v6.25.0`](https://github.com/remix-run/react-router/compare/react-router@6.24.1...react-router@6.25.0) + +## v6.24.1 + +Date: 2024-07-03 + +### Patch Changes + +- Remove `polyfill.io` reference from warning message because the domain was sold and has since been determined to serve malware ([#11741](https://github.com/remix-run/react-router/pull/11741)) + - See https://sansec.io/research/polyfill-supply-chain-attack +- Export `NavLinkRenderProps` type for easier typing of custom `NavLink` callback ([#11553](https://github.com/remix-run/react-router/pull/11553)) +- When using `future.v7_relativeSplatPath`, properly resolve relative paths in splat routes that are children of pathless routes ([#11633](https://github.com/remix-run/react-router/pull/11633)) +- Fog of War (unstable): Trigger a new `router.routes` identity/reflow during route patching ([#11740](https://github.com/remix-run/react-router/pull/11740)) +- Fog of War (unstable): Fix initial matching when a splat route matches ([#11759](https://github.com/remix-run/react-router/pull/11759)) + +**Full Changelog**: [`v6.24.0...v6.24.1`](https://github.com/remix-run/react-router/compare/react-router@6.24.0...react-router@6.24.1) + +## v6.24.0 + +Date: 2024-06-24 + +### What's Changed + +#### Lazy Route Discovery (a.k.a. "Fog of War") + +We're really excited to release our new API for "Lazy Route Discovery" in `v6.24.0`! For some background information, please check out the original [RFC](https://github.com/remix-run/react-router/discussions/11113). The **tl;dr;** is that ever since we introduced the Data APIs in v6.4 via ``, we've been a little bummed that one of the tradeoffs was the lack of a compelling code-splitting story mirroring what we had in the ``/`` apps. We took a baby-step towards improving that story with `route.lazy` in `v6.9.0`, but with `v6.24.0` we've gone the rest of the way. + +With "Fog of War", you can now load portions of the route tree lazily via the new `unstable_patchRoutesOnMiss` option passed to `createBrowserRouter` (and it's memory/hash counterparts). This gives you a way to hook into spots where React Router is unable to match a given path and patch new routes into the route tree during the navigation (or fetcher call). + +Here's a very small example, but please refer to the [documentation](https://reactrouter.com/v6/routers/create-browser-router#optsunstable_patchroutesonmiss) for more information and use cases: + +```js +const router = createBrowserRouter( + [ + { + id: "root", + path: "/", + Component: RootComponent, + }, + ], + { + async unstable_patchRoutesOnMiss({ path, patch }) { + if (path === "/a") { + // Load the `a` route (`{ path: 'a', Component: A }`) + let route = await getARoute(); + // Patch the `a` route in as a new child of the `root` route + patch("root", [route]); + } + }, + }, +); +``` + +### Minor Changes + +- Add support for Lazy Route Discovery (a.k.a. "Fog of War") ([#11626](https://github.com/remix-run/react-router/pull/11626)) + +### Patch Changes + +- Fix `fetcher.submit` types - remove incorrect `navigate`/`fetcherKey`/`unstable_viewTransition` options because they are only relevant for `useSubmit` ([#11631](https://github.com/remix-run/react-router/pull/11631)) +- Allow falsy `location.state` values passed to `` ([#11495](https://github.com/remix-run/react-router/pull/11495)) + +**Full Changelog**: [`v6.23.1...v6.24.0`](https://github.com/remix-run/react-router/compare/react-router@6.23.1...react-router@6.24.0) + +## v6.23.1 + +Date: 2024-05-10 + +### Patch Changes + +- Allow `undefined` to be resolved through `` ([#11513](https://github.com/remix-run/react-router/pull/11513)) +- Add defensive `document` check when checking for `document.startViewTransition` availability ([#11544](https://github.com/remix-run/react-router/pull/11544)) +- Change the `react-router-dom/server` import back to `react-router-dom` instead of `index.ts` ([#11514](https://github.com/remix-run/react-router/pull/11514)) +- `@remix-run/router` - Support `unstable_dataStrategy` on `staticHandler.queryRoute` ([#11515](https://github.com/remix-run/react-router/pull/11515)) + +**Full Changelog**: [`v6.23.0...v6.23.1`](https://github.com/remix-run/react-router/compare/react-router@6.23.0...react-router@6.23.1) + +## v6.23.0 + +Date: 2024-04-23 + +### What's Changed + +#### Data Strategy (unstable) + +The new `unstable_dataStrategy` API is a low-level API designed for advanced use-cases where you need to take control over the data strategy for your `loader`/`action` functions. The default implementation is today's behavior, to fetch all loaders in parallel, but this option allows users to implement more advanced data flows including Remix ["Single Fetch"](https://remix.run/docs/guides/single-fetch), user-land middleware/context APIs, automatic loader caching, and more. Please see the [docs](https://reactrouter.com/v6/routers/create-browser-router#unstable_datastrategy) for more information. + +**Note:** This is a low-level API intended for advanced use-cases. This overrides React Router's internal handling of `loader`/`action` execution, and if done incorrectly will break your app code. Please use with caution and perform the appropriate testing. + +#### Skip Action Error Revalidation (unstable) + +Currently, all active `loader`'s revalidate after any `action` submission, regardless of the `action` result. However, in the majority of cases a `4xx`/`5xx` response from an `action` means that no data was actually changed and the revalidation is unnecessary. We've introduced a new `future.unstable_skipActionErrorRevalidation` flag that changes the behavior here, and we plan to make this the default in future version of React Router. + +With this flag enabled, `action`'s that return/throw a `4xx`/`5xx` response status will no longer automatically revalidate. If you need to revalidate after a `4xx`/`5xx` result with this flag enabled, you can still do that via returning `true` from `shouldRevalidate` - which now also receives a new `unstable_actionStatus` argument alongside `actionResult` so you can make decision based on the status of the `action` response without having to encode it into the action data. + +### Minor Changes + +- Add a new `unstable_dataStrategy` configuration option ([#11098](https://github.com/remix-run/react-router/pull/11098), [#11377](https://github.com/remix-run/react-router/pull/11377)) +- `@remix-run/router` - Add a new `future.unstable_skipActionRevalidation` future flag ([#11098](https://github.com/remix-run/react-router/pull/11098)) +- `@remix-run/router` - SSR: Added a new `skipLoaderErrorBubbling` options to the `staticHandler.query` method to disable error bubbling by the static handler for use in Remix's Single Fetch implementation ([#11098](https://github.com/remix-run/react-router/pull/11098), ([#11377](https://github.com/remix-run/react-router/pull/11377))) + +**Full Changelog**: [`v6.22.3...v6.23.0`](https://github.com/remix-run/react-router/compare/react-router@6.22.3...react-router@6.23.0) + +## v6.22.3 + +Date: 2024-03-07 + +### Patch Changes + +- Fix a `future.v7_partialHydration` bug that would re-run loaders below the boundary on hydration if SSR loader errors bubbled to a parent boundary ([#11324](https://github.com/remix-run/react-router/pull/11324)) +- Fix a `future.v7_partialHydration` bug that would consider the router uninitialized if a route did not have a loader ([#11325](https://github.com/remix-run/react-router/pull/11325)) + +**Full Changelog**: [`v6.22.2...v6.22.3`](https://github.com/remix-run/react-router/compare/react-router@6.22.2...react-router@6.22.3) + +## v6.22.2 + +Date: 2024-02-28 + +### Patch Changes + +- Preserve hydrated errors during partial hydration runs ([#11305](https://github.com/remix-run/react-router/pull/11305)) + +**Full Changelog**: [`v6.22.1...v6.22.2`](https://github.com/remix-run/react-router/compare/react-router@6.22.1...react-router@6.22.2) + +## v6.22.1 + +Date: 2024-02-16 + +### Patch Changes + +- Fix encoding/decoding issues with pre-encoded dynamic parameter values ([#11199](https://github.com/remix-run/react-router/pull/11199)) + +**Full Changelog**: [`v6.22.0...v6.22.1`](https://github.com/remix-run/react-router/compare/react-router@6.22.0...react-router@6.22.1) + +## v6.22.0 + +Date: 2024-02-01 + +### What's Changed + +#### Core Web Vitals Technology Report Flag + +In 2021, the HTTP Archive launched the [Core Web Vitals Technology Report dashboard](https://discuss.httparchive.org/t/new-dashboard-the-core-web-vitals-technology-report/2178): + +> By combining the powers of real-user experiences in the Chrome UX Report 26 (CrUX) dataset with web technology detections in HTTP Archive 30, we can get a glimpse into how architectural decisions like choices of CMS platform or JavaScript framework play a role in sitesโ€™ CWV performance. + +They use a tool called [`wappalyzer`](https://github.com/HTTPArchive/wappalyzer) to identify what technologies a given website is using by looking for certain scripts, global JS variables, or other identifying characteristics. For example, for Remix applications, they [look for the global `__remixContext`](https://github.com/HTTPArchive/wappalyzer/blob/c2a24ee7c2d07bf9c521f02584ae2dcf603ac0b7/src/technologies/r.json#L1328) variable to identify that a website is using Remix. + +It was brought to our attention that React Router was unable to be reliably identified because there are no identifying global aspects. They are currently [looking for external scripts with `react-router`](https://github.com/HTTPArchive/wappalyzer/blob/c2a24ee7c2d07bf9c521f02584ae2dcf603ac0b7/src/technologies/r.json#L637) in the name. This will identify sites using React Router from a CDN such as `unpkg` - but it will miss the **vast** majority of sites that are installing React Router from the npm registry and bundling it into their JS files. This results in [drastically under-reporting](https://lookerstudio.google.com/s/pixHkNmGbN4) the usage of React Router on the web. + +Starting with version `6.22.0`, sites using `react-router-dom` will begin adding a `window.__reactRouterVersion` variable that will be set to a string value of the SemVer major version number (i.e., `window.__reactRouterVersion = "6";`) so that they can be properly identified. + +### Minor Changes + +- Include a `window.__reactRouterVersion` for CWV Report detection ([#11222](https://github.com/remix-run/react-router/pull/11222)) +- Add a `createStaticHandler` `future.v7_throwAbortReason` flag to throw `request.signal.reason` (defaults to a `DOMException`) when a request is aborted instead of an `Error` such as `new Error("query() call aborted: GET /path")` ([#11104](https://github.com/remix-run/react-router/pull/11104)) + - Please note that `DOMException` was added in Node v17 so you will not get a `DOMException` on Node 16 and below. + +### Patch Changes + +- Respect the `ErrorResponse` status code if passed to `getStaticContextFormError` ([#11213](https://github.com/remix-run/react-router/pull/11213)) + +**Full Changelog**: [`v6.21.3...v6.22.0`](https://github.com/remix-run/react-router/compare/react-router@6.21.3...react-router@6.22.0) + +## v6.21.3 + +Date: 2024-01-18 + +### Patch Changes + +- Fix `NavLink` `isPending` when a `basename` is used ([#11195](https://github.com/remix-run/react-router/pull/11195)) +- Remove leftover `unstable_` prefix from `Blocker`/`BlockerFunction` types ([#11187](https://github.com/remix-run/react-router/pull/11187)) + +**Full Changelog**: [`v6.21.2...v6.21.3`](https://github.com/remix-run/react-router/compare/react-router@6.21.2...react-router@6.21.3) + +## v6.21.2 + +Date: 2024-01-11 + +### Patch Changes + +- Leverage `useId` for internal fetcher keys when available ([#11166](https://github.com/remix-run/react-router/pull/11166)) +- Fix bug where dashes were not picked up in dynamic parameter names ([#11160](https://github.com/remix-run/react-router/pull/11160)) +- Do not attempt to deserialize empty JSON responses ([#11164](https://github.com/remix-run/react-router/pull/11164)) + +**Full Changelog**: [`v6.21.1...v6.21.2`](https://github.com/remix-run/react-router/compare/react-router@6.21.1...react-router@6.21.2) + +## v6.21.1 + +Date: 2023-12-21 + +### Patch Changes + +- Fix bug with `route.lazy` not working correctly on initial SPA load when `v7_partialHydration` is specified ([#11121](https://github.com/remix-run/react-router/pull/11121)) +- Fix bug preventing revalidation from occurring for persisted fetchers unmounted during the `submitting` phase ([#11102](https://github.com/remix-run/react-router/pull/11102)) +- De-dup relative path logic in `resolveTo` ([#11097](https://github.com/remix-run/react-router/pull/11097)) + +**Full Changelog**: [`v6.21.0...v6.21.1`](https://github.com/remix-run/react-router/compare/react-router@6.21.0...react-router@6.21.1) + +## v6.21.0 + +Date: 2023-12-13 + +### What's Changed + +#### `future.v7_relativeSplatPath` + +We fixed a splat route path-resolution bug in `6.19.0`, but later determined a large number of applications were relying on the buggy behavior, so we reverted the fix in `6.20.1` (see [#10983](https://github.com/remix-run/react-router/issues/10983), [#11052](https://github.com/remix-run/react-router/issues/11052), [#11078](https://github.com/remix-run/react-router/issues/11078)). + +The buggy behavior is that the default behavior when resolving relative paths inside a splat route would _ignore_ any splat (`*`) portion of the current route path. When the future flag is enabled, splat portions are included in relative path logic within splat routes. + +For more information, please refer to the [`useResolvedPath` docs](https://reactrouter.com/v6/hooks/use-resolved-path#splat-paths) and/or the [detailed changelog entry](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md#6210). + +#### Partial Hydration + +We added a new `future.v7_partialHydration` future flag for the `@remix-run/router` that enables partial hydration of a data router when Server-Side Rendering. This allows you to provide `hydrationData.loaderData` that has values for _some_ initially matched route loaders, but not all. When this flag is enabled, the router will call `loader` functions for routes that do not have hydration loader data during `router.initialize()`, and it will render down to the deepest provided `HydrateFallback` (up to the first route without hydration data) while it executes the unhydrated routes. ([#11033](https://github.com/remix-run/react-router/pull/11033)) + +### Minor Changes + +- Add a new `future.v7_relativeSplatPath` flag to implement a breaking bug fix to relative routing when inside a splat route. ([#11087](https://github.com/remix-run/react-router/pull/11087)) +- Add a new `future.v7_partialHydration` future flag that enables partial hydration of a data router when Server-Side Rendering ([#11033](https://github.com/remix-run/react-router/pull/11033)) + +### Patch Changes + +- Properly handle falsy error values in `ErrorBoundary`'s ([#11071](https://github.com/remix-run/react-router/pull/11071)) +- Catch and bubble errors thrown when trying to unwrap responses from `loader`/`action` functions ([#11061](https://github.com/remix-run/react-router/pull/11061)) +- Fix `relative="path"` issue when rendering `Link`/`NavLink` outside of matched routes ([#11062](https://github.com/remix-run/react-router/pull/11062)) + +**Full Changelog**: [`v6.20.1...v6.21.0`](https://github.com/remix-run/react-router/compare/react-router@6.20.1...react-router@6.21.0) + +## v6.20.1 + +Date: 2023-12-01 + +### Patch Changes + +- Revert the `useResolvedPath` fix for splat routes due to a large number of applications that were relying on the buggy behavior (see [#11052](https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329)) ([#11078](https://github.com/remix-run/react-router/pull/11078)) + - We plan to re-introduce this fix behind a future flag in the next minor version (see [this comment](https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329)) + - This fix was included in versions `6.19.0` and `6.20.0`. If you are upgrading from `6.18.0` or earlier, you would not have been impacted by this fix. + +**Full Changelog**: [`v6.20.0...v6.20.1`](https://github.com/remix-run/react-router/compare/react-router@6.20.0...react-router@6.20.1) + +## v6.20.0 + +Date: 2023-11-22 + +> [!WARNING] +> Please use version `6.20.1` or later instead of `6.20.0`. We discovered that a large number of apps were relying on buggy behavior that was fixed in this release ([#11045](https://github.com/remix-run/react-router/pull/11045)). We reverted the fix in `6.20.1` and will be re-introducing it behind a future flag in a subsequent release. See [#11052](https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329) for more details. + +### Minor Changes + +- Export the `PathParam` type from the public API ([#10719](https://github.com/remix-run/react-router/pull/10719)) + +### Patch Changes + +- Do not revalidate unmounted fetchers when `v7_fetcherPersist` is enabled ([#11044](https://github.com/remix-run/react-router/pull/11044)) +- Fix bug with `resolveTo` path resolution in splat routes ([#11045](https://github.com/remix-run/react-router/pull/11045)) + - This is a follow up to [#10983](https://github.com/remix-run/react-router/pull/10983) to handle the few other code paths using `getPathContributingMatches` + - This removes the `UNSAFE_getPathContributingMatches` export from `@remix-run/router` since we no longer need this in the `react-router`/`react-router-dom` layers + +**Full Changelog**: [`v6.19.0...v6.20.0`](https://github.com/remix-run/react-router/compare/react-router@6.19.0...react-router@6.20.0) + +## v6.19.0 + +Date: 2023-11-16 + +> [!WARNING] +> Please use version `6.20.1` or later instead of `6.19.0`. We discovered that a large number of apps were relying on buggy behavior that was fixed in this release ([#10983](https://github.com/remix-run/react-router/pull/10983)). We reverted the fix in `6.20.1` and will be re-introducing it behind a future flag in a subsequent release. See [#11052](https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329) for more details. + +### What's Changed + +#### `unstable_flushSync` API + +This release brings a new `unstable_flushSync` option to the imperative APIs (`useSubmit`, `useNavigate`, `fetcher.submit`, `fetcher.load`) to let users opt-into synchronous DOM updates for pending/optimistic UI. + +```js +function handleClick() { + submit(data, { flushSync: true }); + // Everything is flushed to the DOM so you can focus/scroll to your pending/optimistic UI + setFocusAndOrScrollToNewlyAddedThing(); +} +``` + +### Minor Changes + +- Add `unstable_flushSync` option to `useNavigate`/`useSubmit`/`fetcher.load`/`fetcher.submit` to opt-out of `React.startTransition` and into `ReactDOM.flushSync` for state updates ([#11005](https://github.com/remix-run/react-router/pull/11005)) +- Remove the `unstable_` prefix from the [`useBlocker`](https://reactrouter.com/v6/hooks/use-blocker) hook as it's been in use for enough time that we are confident in the API ([#10991](https://github.com/remix-run/react-router/pull/10991)) + - We do not plan to remove the prefix from `unstable_usePrompt` due to differences in how browsers handle `window.confirm` that prevent React Router from guaranteeing consistent/correct behavior + +### Patch Changes + +- Fix `useActionData` so it returns proper contextual action data and not _any_ action data in the tree ([#11023](https://github.com/remix-run/react-router/pull/11023)) +- Fix bug in `useResolvedPath` that would cause `useResolvedPath(".")` in a splat route to lose the splat portion of the URL path. ([#10983](https://github.com/remix-run/react-router/pull/10983)) + - โš ๏ธ This fixes a quite long-standing bug specifically for `"."` paths inside a splat route which incorrectly dropped the splat portion of the URL. If you are relative routing via `"."` inside a splat route in your application you should double check that your logic is not relying on this buggy behavior and update accordingly. +- Fix issue where a changing fetcher `key` in a `useFetcher` that remains mounted wasn't getting picked up ([#11009](https://github.com/remix-run/react-router/pull/11009)) +- Fix `useFormAction` which was incorrectly inheriting the `?index` query param from child route `action` submissions ([#11025](https://github.com/remix-run/react-router/pull/11025)) +- Fix `NavLink` `active` logic when `to` location has a trailing slash ([#10734](https://github.com/remix-run/react-router/pull/10734)) +- Fix types so `unstable_usePrompt` can accept a `BlockerFunction` in addition to a `boolean` ([#10991](https://github.com/remix-run/react-router/pull/10991)) +- Fix `relative="path"` bug where relative path calculations started from the full location pathname, instead of from the current contextual route pathname. ([#11006](https://github.com/remix-run/react-router/pull/11006)) + + ```jsx + + }> + + + ; + + function Component() { + return ( + <> + {/* This is now correctly relative to /a/b, not /a/b/c */} + + + + ); + } + ``` + +**Full Changelog**: [`6.18.0...6.19.0`](https://github.com/remix-run/react-router/compare/react-router@6.18.0...react-router@6.19.0) + +## v6.18.0 + +Date: 2023-10-31 + +### What's Changed + +#### New Fetcher APIs + +Per this [RFC](https://github.com/remix-run/remix/discussions/7698), we've introduced some new APIs that give you more granular control over your fetcher behaviors. + +- You may now specify your own fetcher identifier via `useFetcher({ key: string })`, which allows you to access the same fetcher instance from different components in your application without prop-drilling +- Fetcher keys are now exposed on the fetchers returned from `useFetchers` so that they can be looked up by `key` +- `Form` and `useSubmit` now support optional `navigate`/`fetcherKey` props/params to allow kicking off a fetcher submission under the hood with an optionally user-specified `key` + - `` + - `submit(data, { method: "post", navigate: false, fetcherKey: "my-key" })` + - Invoking a fetcher in this way is ephemeral and stateless + - If you need to access the state of one of these fetchers, you will need to leverage `useFetchers()` or `useFetcher({ key })` to look it up elsewhere + +#### Persistence Future Flag (`future.v7_fetcherPersist`) + +Per the same [RFC](https://github.com/remix-run/remix/discussions/7698) as above, we've introduced a new `future.v7_fetcherPersist` flag that allows you to opt-into the new fetcher persistence/cleanup behavior. Instead of being immediately cleaned up on unmount, fetchers will persist until they return to an `idle` state. This makes pending/optimistic UI _much_ easier in scenarios where the originating fetcher needs to unmount. + +- This is sort of a long-standing bug fix as the `useFetchers()` API was always supposed to only reflect **in-flight** fetcher information for pending/optimistic UI -- it was not intended to reflect fetcher data or hang onto fetchers after they returned to an `idle` state +- Keep an eye out for the following specific behavioral changes when opting into this flag and check your app for compatibility: + - Fetchers that complete _while still mounted_ will no longer appear in `useFetchers()` after completion - they served no purpose in there since you can access the data via `useFetcher().data` + - Fetchers that previously unmounted _while in-flight_ will not be immediately aborted and will instead be cleaned up once they return to an `idle` state + - They will remain exposed via `useFetchers` while in-flight so you can still access pending/optimistic data after unmount + - If a fetcher is no longer mounted when it completes, then it's result will not be post processed - e.g., redirects will not be followed and errors will not bubble up in the UI + - However, if a fetcher was re-mounted elsewhere in the tree using the same `key`, then it's result will be processed, even if the originating fetcher was unmounted + +### Minor Changes + +- Add fetcher `key` APIs and `navigate=false` options ([#10960](https://github.com/remix-run/react-router/pull/10960)) +- Add `future.v7_fetcherPersist` flag ([#10962](https://github.com/remix-run/react-router/pull/10962)) +- Add support for optional path segments in `matchPath` ([#10768](https://github.com/remix-run/react-router/pull/10768)) + +### Patch Changes + +- Fix the `future` prop on `BrowserRouter`, `HashRouter` and `MemoryRouter` so that it accepts a `Partial` instead of requiring all flags to be included ([#10962](https://github.com/remix-run/react-router/pull/10962)) +- Fix `router.getFetcher`/`router.deleteFetcher` type definitions which incorrectly specified `key` as an optional parameter ([#10960](https://github.com/remix-run/react-router/pull/10960)) + +**Full Changelog**: [`6.17.0...6.18.0`](https://github.com/remix-run/react-router/compare/react-router@6.17.0...react-router@6.18.0) + +## v6.17.0 + +Date: 2023-10-16 + +### What's Changed + +#### View Transitions ๐Ÿš€ + +We're excited to release experimental support for the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) in React Router! You can now trigger navigational DOM updates to be wrapped in `document.startViewTransition` to enable CSS animated transitions on SPA navigations in your application. + +The simplest approach to enabling a View Transition in your React Router app is via the new [``](https://reactrouter.com/v6/components/link#unstable_viewtransition) prop. This will cause the navigation DOM update to be wrapped in `document.startViewTransition` which will enable transitions for the DOM update. Without any additional CSS styles, you'll get a basic cross-fade animation for your page. + +If you need to apply more fine-grained styles for your animations, you can leverage the [`unstable_useViewTransitionState`](https://reactrouter.com/v6/hooks/use-view-transition-state) hook which will tell you when a transition is in progress and you can use that to apply classes or styles: + +```jsx +function ImageLink(to, src, alt) { + const isTransitioning = unstable_useViewTransitionState(to); + return ( + + {alt} + + ); +} +``` + +You can also use the [``](https://reactrouter.com/v6/components/nav-link#unstable_viewtransition) shorthand which will manage the hook usage for you and automatically add a `transitioning` class to the `` during the transition: + +```css +a.transitioning img { + view-transition-name: "image-expand"; +} +``` + +```jsx + + {alt} + +``` + +For an example usage of View Transitions, check out [our fork](https://github.com/brophdawg11/react-router-records) of the awesome [Astro Records](https://github.com/Charca/astro-records) demo. + +For more information on using the View Transitions API, please refer to the [Smooth and simple transitions with the View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions/) guide from the Google Chrome team. + +### Minor Changes + +- Add support for view transitions ([#10916](https://github.com/remix-run/react-router/pull/10916)) + +### Patch Changes + +- Log a warning and fail gracefully in `ScrollRestoration` when `sessionStorage` is unavailable ([#10848](https://github.com/remix-run/react-router/pull/10848)) +- Fix `RouterProvider` `future` prop type to be a `Partial` so that not all flags must be specified ([#10900](https://github.com/remix-run/react-router/pull/10900)) +- Allow 404 detection to leverage root route error boundary if path contains a URL segment ([#10852](https://github.com/remix-run/react-router/pull/10852)) +- Fix `ErrorResponse` type to avoid leaking internal field ([#10876](https://github.com/remix-run/react-router/pull/10876)) + +**Full Changelog**: [`6.16.0...6.17.0`](https://github.com/remix-run/react-router/compare/react-router@6.16.0...react-router@6.17.0) + +## v6.16.0 + +Date: 2023-09-13 + +### Minor Changes + +- In order to move towards stricter TypeScript support in the future, we're aiming to replace current usages of `any` with `unknown` on exposed typings for user-provided data. To do this in Remix v2 without introducing breaking changes in React Router v6, we have added generics to a number of shared types. These continue to default to `any` in React Router and are overridden with `unknown` in Remix. In React Router v7 we plan to move these to `unknown` as a breaking change. ([#10843](https://github.com/remix-run/react-router/pull/10843)) + - `Location` now accepts a generic for the `location.state` value + - `ActionFunctionArgs`/`ActionFunction`/`LoaderFunctionArgs`/`LoaderFunction` now accept a generic for the `context` parameter (only used in SSR usages via `createStaticHandler`) + - The return type of `useMatches` (now exported as `UIMatch`) accepts generics for `match.data` and `match.handle` - both of which were already set to `unknown` +- Move the `@private` class export `ErrorResponse` to an `UNSAFE_ErrorResponseImpl` export since it is an implementation detail and there should be no construction of `ErrorResponse` instances in userland. This frees us up to export a `type ErrorResponse` which correlates to an instance of the class via `InstanceType`. Userland code should only ever be using `ErrorResponse` as a type and should be type-narrowing via `isRouteErrorResponse`. ([#10811](https://github.com/remix-run/react-router/pull/10811)) +- Export `ShouldRevalidateFunctionArgs` interface ([#10797](https://github.com/remix-run/react-router/pull/10797)) +- Removed private/internal APIs only required for the Remix v1 backwards compatibility layer and no longer needed in Remix v2 (`_isFetchActionRedirect`, `_hasFetcherDoneAnything`) ([#10715](https://github.com/remix-run/react-router/pull/10715)) + +### Patch Changes + +- Properly encode rendered URIs in server rendering to avoid hydration errors ([#10769](https://github.com/remix-run/react-router/pull/10769)) +- Add method/url to error message on aborted `query`/`queryRoute` calls ([#10793](https://github.com/remix-run/react-router/pull/10793)) +- Fix a race-condition with loader/action-thrown errors on `route.lazy` routes ([#10778](https://github.com/remix-run/react-router/pull/10778)) +- Fix type for `actionResult` on the arguments object passed to `shouldRevalidate` ([#10779](https://github.com/remix-run/react-router/pull/10779)) + +**Full Changelog**: [`v6.15.0...v6.16.0`](https://github.com/remix-run/react-router/compare/react-router@6.15.0...react-router@6.16.0) + +## v6.15.0 + +Date: 2023-08-10 + +### Minor Changes + +- Add's a new `redirectDocument()` function which allows users to specify that a redirect from a `loader`/`action` should trigger a document reload (via `window.location`) instead of attempting to navigate to the redirected location via React Router ([#10705](https://github.com/remix-run/react-router/pull/10705)) + +### Patch Changes + +- Ensure `useRevalidator` is referentially stable across re-renders if revalidations are not actively occurring ([#10707](https://github.com/remix-run/react-router/pull/10707)) +- Ensure hash history always includes a leading slash on hash pathnames ([#10753](https://github.com/remix-run/react-router/pull/10753)) +- Fixes an edge-case affecting web extensions in Firefox that use `URLSearchParams` and the `useSearchParams` hook ([#10620](https://github.com/remix-run/react-router/pull/10620)) +- Reorder effects in `unstable_usePrompt` to avoid throwing an exception if the prompt is unblocked and a navigation is performed synchronously ([#10687](https://github.com/remix-run/react-router/pull/10687), [#10718](https://github.com/remix-run/react-router/pull/10718)) +- SSR: Do not include hash in `useFormAction()` for unspecified actions since it cannot be determined on the server and causes hydration issues ([#10758](https://github.com/remix-run/react-router/pull/10758)) +- SSR: Fix an issue in `queryRoute` that was not always identifying thrown `Response` instances ([#10717](https://github.com/remix-run/react-router/pull/10717)) +- `react-router-native`: Update `@ungap/url-search-params` dependency from `^0.1.4` to `^0.2.2` ([#10590](https://github.com/remix-run/react-router/pull/10590)) + +**Full Changelog**: [`v6.14.2...v6.15.0`](https://github.com/remix-run/react-router/compare/react-router@6.14.2...react-router@6.15.0) + +## v6.14.2 + +Date: 2023-07-17 + +### Patch Changes + +- Add missing `` prop to populate `history.state` on submission navigations ([#10630](https://github.com/remix-run/react-router/pull/10630)) +- Trigger an error if a `defer` promise resolves/rejects with `undefined` in order to match the behavior of loaders and actions which must return a value or `null` ([#10690](https://github.com/remix-run/react-router/pull/10690)) +- Properly handle fetcher redirects interrupted by normal navigations ([#10674](https://github.com/remix-run/react-router/pull/10674)) +- Initial-load fetchers should not automatically revalidate on GET navigations ([#10688](https://github.com/remix-run/react-router/pull/10688)) +- Properly decode element id when emulating hash scrolling via `` ([#10682](https://github.com/remix-run/react-router/pull/10682)) +- Typescript: Enhance the return type of `Route.lazy` to prohibit returning an empty object ([#10634](https://github.com/remix-run/react-router/pull/10634)) +- SSR: Support proper hydration of `Error` subclasses such as `ReferenceError`/`TypeError` ([#10633](https://github.com/remix-run/react-router/pull/10633)) + +**Full Changelog**: [`v6.14.1...v6.14.2`](https://github.com/remix-run/react-router/compare/react-router@6.14.1...react-router@6.14.2) + +## v6.14.1 + +Date: 2023-06-30 + +### Patch Changes + +- Fix loop in `unstable_useBlocker` when used with an unstable blocker function ([#10652](https://github.com/remix-run/react-router/pull/10652)) +- Fix issues with reused blockers on subsequent navigations ([#10656](https://github.com/remix-run/react-router/pull/10656)) +- Updated dependencies: + - `@remix-run/router@1.7.1` + +**Full Changelog**: [`v6.14.0...v6.14.1`](https://github.com/remix-run/react-router/compare/react-router@6.14.0...react-router@6.14.1) + +## v6.14.0 + +Date: 2023-06-23 + +### What's Changed + +#### JSON/Text Submissions + +`6.14.0` adds support for JSON and Text submissions via `useSubmit`/`fetcher.submit` since it's not always convenient to have to serialize into `FormData` if you're working in a client-side SPA. To opt-into these encodings you just need to specify the proper `formEncType`: + +**Opt-into `application/json` encoding:** + +```js +function Component() { + let navigation = useNavigation(); + let submit = useSubmit(); + submit({ key: "value" }, { method: "post", encType: "application/json" }); + // navigation.formEncType => "application/json" + // navigation.json => { key: "value" } +} + +async function action({ request }) { + // request.headers.get("Content-Type") => "application/json" + // await request.json() => { key: "value" } +} +``` + +**Opt-into `text/plain` encoding:** + +```js +function Component() { + let navigation = useNavigation(); + let submit = useSubmit(); + submit("Text submission", { method: "post", encType: "text/plain" }); + // navigation.formEncType => "text/plain" + // navigation.text => "Text submission" +} + +async function action({ request }) { + // request.headers.get("Content-Type") => "text/plain" + // await request.text() => "Text submission" +} +``` + +**โš ๏ธ Default Behavior Will Change in v7** + +Please note that to avoid a breaking change, the default behavior will still encode a simple key/value JSON object into a `FormData` instance: + +```jsx +function Component() { + let navigation = useNavigation(); + let submit = useSubmit(); + submit({ key: "value" }, { method: "post" }); + // navigation.formEncType => "application/x-www-form-urlencoded" + // navigation.formData => FormData instance +} + +async function action({ request }) { + // request.headers.get("Content-Type") => "application/x-www-form-urlencoded" + // await request.formData() => FormData instance +} +``` + +This behavior will likely change in v7 so it's best to make any JSON object submissions explicit with `formEncType: "application/x-www-form-urlencoded"` or `formEncType: "application/json"` to ease your eventual v7 migration path. + +### Minor Changes + +- Add support for `application/json` and `text/plain` encodings for `useSubmit`/`fetcher.submit`. To reflect these additional types, `useNavigation`/`useFetcher` now also contain `navigation.json`/`navigation.text` and `fetcher.json`/`fetcher.text` which include the json/text submission if applicable. ([#10413](https://github.com/remix-run/react-router/pull/10413)) + +### Patch Changes + +- When submitting a form from a `submitter` element, prefer the built-in `new FormData(form, submitter)` instead of the previous manual approach in modern browsers (those that support the new `submitter` parameter) ([#9865](https://github.com/remix-run/react-router/pull/9865)) + - For browsers that don't support it, we continue to just append the submit button's entry to the end, and we also add rudimentary support for `type="image"` buttons + - If developers want full spec-compliant support for legacy browsers, they can use the `formdata-submitter-polyfill` +- Call `window.history.pushState/replaceState` _before_ updating React Router state (instead of after) so that `window.location` matches `useLocation` during synchronous React 17 rendering ([#10448](https://github.com/remix-run/react-router/pull/10448)) + - โš ๏ธ Note: generally apps should not be relying on `window.location` and should always reference `useLocation` when possible, as `window.location` will not be in sync 100% of the time (due to `popstate` events, concurrent mode, etc.) +- Avoid calling `shouldRevalidate` for fetchers that have not yet completed a data load ([#10623](https://github.com/remix-run/react-router/pull/10623)) +- Strip `basename` from the `location` provided to `` to match the `useLocation` behavior ([#10550](https://github.com/remix-run/react-router/pull/10550)) +- Strip `basename` from locations provided to `unstable_useBlocker` functions to match the `useLocation` behavior ([#10573](https://github.com/remix-run/react-router/pull/10573)) +- Fix `unstable_useBlocker` key issues in `StrictMode` ([#10573](https://github.com/remix-run/react-router/pull/10573)) +- Fix `generatePath` when passed a numeric `0` value parameter ([#10612](https://github.com/remix-run/react-router/pull/10612)) +- Fix `tsc --skipLibCheck:false` issues on React 17 ([#10622](https://github.com/remix-run/react-router/pull/10622)) +- Upgrade `typescript` to 5.1 ([#10581](https://github.com/remix-run/react-router/pull/10581)) + +**Full Changelog**: [`v6.13.0...v6.14.0`](https://github.com/remix-run/react-router/compare/react-router@6.13.0...react-router@6.14.0) + +## v6.13.0 + +Date: 2023-06-14 + +### What's Changed + +`6.13.0` is really a patch release in spirit but comes with a SemVer minor bump since we added a new future flag. + +#### `future.v7_startTransition` + +The **tl;dr;** is that `6.13.0` is the same as [`6.12.0`](https://github.com/remix-run/react-router/releases/tag/react-router%406.12.0) bue we've moved the usage of `React.startTransition` behind an opt-in `future.v7_startTransition` [future flag](https://reactrouter.com/v6/guides/api-development-strategy) because we found that there are applications in the wild that are currently using `Suspense` in ways that are incompatible with `React.startTransition`. + +Therefore, in `6.13.0` the default behavior will no longer leverage `React.startTransition`: + +```jsx + + {/*...*/} + + + +``` + +If you wish to enable `React.startTransition`, pass the future flag to your router component: + +```jsx + + {/*...*/} + + + +``` + +We recommend folks adopt this flag sooner rather than later to be better compatible with React concurrent mode, but if you run into issues you can continue without the use of `React.startTransition` until v7. Issues usually boil down to creating net-new promises during the render cycle, so if you run into issues when opting into `React.startTransition`, you should either lift your promise creation out of the render cycle or put it behind a `useMemo`. + +### Minor Changes + +- Move `React.startTransition` usage behinds a future flag ([#10596](https://github.com/remix-run/react-router/pull/10596)) + +### Patch Changes + +- Work around webpack/terser `React.startTransition` minification bug in production mode ([#10588](https://github.com/remix-run/react-router/pull/10588)) + +**Full Changelog**: [`v6.12.1...v6.13.0`](https://github.com/remix-run/react-router/compare/react-router@6.12.1...react-router@6.13.0) + +## v6.12.1 + +Date: 2023-06-08 + +> [!WARNING] +> Please use version `6.13.0` or later instead of `6.12.0`/`6.12.1`. These versions suffered from some Webpack build/minification issues resulting failed builds or invalid minified code in your production bundles. See [#10569](https://github.com/remix-run/react-router/pull/10569) and [#10579](https://github.com/remix-run/react-router/issues/10579) for more details. + +### Patch Changes + +- Adjust feature detection of `React.startTransition` to fix webpack + react 17 compilation error ([#10569](https://github.com/remix-run/react-router/pull/10569)) + +**Full Changelog**: [`v6.12.0...v6.12.1`](https://github.com/remix-run/react-router/compare/react-router@6.12.0...react-router@6.12.1) + +## v6.12.0 + +Date: 2023-06-06 + +> [!WARNING] +> Please use version `6.13.0` or later instead of `6.12.0`/`6.12.1`. These versions suffered from some Webpack build/minification issues resulting failed builds or invalid minified code in your production bundles. See [#10569](https://github.com/remix-run/react-router/pull/10569) and [#10579](https://github.com/remix-run/react-router/issues/10579) for more details. + +### What's Changed + +#### `React.startTransition` support + +With `6.12.0` we've added better support for suspending components by wrapping the internal router state updates in [`React.startTransition`](https://react.dev/reference/react/startTransition). This means that, for example, if one of your components in a destination route suspends and you have not provided a [`Suspense`](https://react.dev/reference/react/Suspense) boundary to show a fallback, React will delay the rendering of the new UI and show the old UI until that asynchronous operation resolves. This could be useful for waiting for things such as waiting for images or CSS files to load (and technically, yes, you could use it for data loading but we'd still recommend using loaders for that ๐Ÿ˜€). For a quick overview of this usage, check out [Ryan's demo on Twitter](https://twitter.com/remix_run/status/1658976420767604736). + +### Minor Changes + +- Wrap internal router state updates with `React.startTransition` ([#10438](https://github.com/remix-run/react-router/pull/10438)) + +### Patch Changes + +- Allow fetcher revalidations to complete if submitting fetcher is deleted ([#10535](https://github.com/remix-run/react-router/pull/10535)) +- Re-throw `DOMException` (`DataCloneError`) when attempting to perform a `PUSH` navigation with non-serializable state. ([#10427](https://github.com/remix-run/react-router/pull/10427)) +- Ensure revalidations happen when hash is present ([#10516](https://github.com/remix-run/react-router/pull/10516)) +- Upgrade `jest` and `jsdom` ([#10453](https://github.com/remix-run/react-router/pull/10453)) +- Updated dependencies: + - `@remix-run/router@1.6.3` ([Changelog](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#163)) + +**Full Changelog**: [`v6.11.2...v6.12.0`](https://github.com/remix-run/react-router/compare/react-router@6.11.2...react-router@6.12.0) + +## v6.11.2 + +Date: 2023-05-17 + +### Patch Changes + +- Fix `basename` duplication in descendant `` inside a `` ([#10492](https://github.com/remix-run/react-router/pull/10492)) +- Fix bug where initial data load would not kick off when hash is present ([#10493](https://github.com/remix-run/react-router/pull/10493)) +- Export `SetURLSearchParams` type ([#10444](https://github.com/remix-run/react-router/pull/10444)) +- Fix Remix HMR-driven error boundaries by properly reconstructing new routes and `manifest` in `_internalSetRoutes` ([#10437](https://github.com/remix-run/react-router/pull/10437)) + +**Full Changelog**: [`v6.11.1...v6.11.2`](https://github.com/remix-run/react-router/compare/react-router@6.11.1...react-router@6.11.2) + +## v6.11.1 + +Date: 2023-05-03 + +### Patch Changes + +- Fix usage of `Component` API within descendant `` ([#10434](https://github.com/remix-run/react-router/pull/10434)) +- Fix bug when calling `useNavigate` from `` inside a `` ([#10432](https://github.com/remix-run/react-router/pull/10432)) +- Fix usage of `` in strict mode when using a data router ([#10435](https://github.com/remix-run/react-router/pull/10435)) +- Fix `basename` handling when navigating without a path ([#10433](https://github.com/remix-run/react-router/pull/10433)) +- "Same hash" navigations no longer re-run loaders to match browser behavior (i.e. `/path#hash -> /path#hash`) ([#10408](https://github.com/remix-run/react-router/pull/10408)) + +**Full Changelog**: [`v6.11.0...v6.11.1`](https://github.com/remix-run/react-router/compare/react-router@6.11.0...react-router@6.11.1) + +## v6.11.0 + +Date: 2023-04-28 + +### Minor Changes + +- Enable `basename` support in `useFetcher` ([#10336](https://github.com/remix-run/react-router/pull/10336)) + - If you were previously working around this issue by manually prepending the `basename` then you will need to remove the manually prepended `basename` from your `fetcher` calls (`fetcher.load('/basename/route') -> fetcher.load('/route')`) +- Updated dependencies: + - `@remix-run/router@1.6.0` ([Changelog](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#160)) + +### Patch Changes + +- When using a `RouterProvider`, `useNavigate`/`useSubmit`/`fetcher.submit` are now stable across location changes, since we can handle relative routing via the `@remix-run/router` instance and get rid of our dependence on `useLocation()` ([#10336](https://github.com/remix-run/react-router/pull/10336)) + - When using `BrowserRouter`, these hooks remain unstable across location changes because they still rely on `useLocation()` +- Fetchers should no longer revalidate on search params changes or routing to the same URL, and will only revalidate on `action` submissions or `router.revalidate` calls ([#10344](https://github.com/remix-run/react-router/pull/10344)) +- Fix inadvertent re-renders when using `Component` instead of `element` on a route definition ([#10287](https://github.com/remix-run/react-router/pull/10287)) +- Fail gracefully on `` and other invalid URL values ([#10367](https://github.com/remix-run/react-router/pull/10367)) +- Switched from `useSyncExternalStore` to `useState` for internal `@remix-run/router` router state syncing in ``. We found some [subtle bugs](https://codesandbox.io/s/use-sync-external-store-loop-9g7b81) where router state updates got propagated _before_ other normal `useState` updates, which could lead to foot guns in `useEffect` calls. ([#10377](https://github.com/remix-run/react-router/pull/10377), [#10409](https://github.com/remix-run/react-router/pull/10409)) +- Log loader/action errors caught by the default error boundary to the console in dev for easier stack trace evaluation ([#10286](https://github.com/remix-run/react-router/pull/10286)) +- Fix bug preventing rendering of descendant `` when `RouterProvider` errors existed ([#10374](https://github.com/remix-run/react-router/pull/10374)) +- Fix detection of `useNavigate` in the render cycle by setting the `activeRef` in a layout effect, allowing the `navigate` function to be passed to child components and called in a `useEffect` there ([#10394](https://github.com/remix-run/react-router/pull/10394)) +- Allow `useRevalidator()` to resolve a loader-driven error boundary scenario ([#10369](https://github.com/remix-run/react-router/pull/10369)) +- Enhance `LoaderFunction`/`ActionFunction` return type to prevent `undefined` from being a valid return value ([#10267](https://github.com/remix-run/react-router/pull/10267)) +- Ensure proper 404 error on `fetcher.load` call to a route without a `loader` ([#10345](https://github.com/remix-run/react-router/pull/10345)) +- Decouple `AbortController` usage between revalidating fetchers and the thing that triggered them such that the unmount/deletion of a revalidating fetcher doesn't impact the ongoing triggering navigation/revalidation ([#10271](https://github.com/remix-run/react-router/pull/10271)) + +**Full Changelog**: [`v6.10.0...v6.11.0`](https://github.com/remix-run/react-router/compare/react-router@6.10.0...react-router@6.11.0) + +## v6.10.0 + +Date: 2023-03-29 + +### What's Changed + +We recently published a post over on the Remix Blog titled ["Future Proofing Your Remix App"](https://remix.run/blog/future-flags) that goes through our strategy to ensure smooth upgrades for your Remix and React Router apps going forward. React Router `6.10.0` adds support for these flags (for data routers) which you can specify when you create your router: + +```js +const router = createBrowserRouter(routes, { + future: { + // specify future flags here + }, +}); +``` + +You can also check out the docs [here](https://reactrouter.com/en/dev/guides/api-development-strategy) and [here](https://reactrouter.com/en/dev/routers/create-browser-router#future). + +### Minor Changes + +#### `future.v7_normalizeFormMethod` + +The first future flag being introduced is `future.v7_normalizeFormMethod` which will normalize the exposed `useNavigation()/useFetcher()` `formMethod` fields as uppercase HTTP methods to align with the `fetch()` (and some Remix) behavior. ([#10207](https://github.com/remix-run/react-router/pull/10207)) + +- When `future.v7_normalizeFormMethod` is unspecified or set to `false` (default v6 behavior), + - `useNavigation().formMethod` is lowercase + - `useFetcher().formMethod` is lowercase +- When `future.v7_normalizeFormMethod === true`: + - `useNavigation().formMethod` is UPPERCASE + - `useFetcher().formMethod` is UPPERCASE + +### Patch Changes + +- Fix `createStaticHandler` to also check for `ErrorBoundary` on routes in addition to `errorElement` ([#10190](https://github.com/remix-run/react-router/pull/10190)) +- Fix route ID generation when using Fragments in `createRoutesFromElements` ([#10193](https://github.com/remix-run/react-router/pull/10193)) +- Provide fetcher submission to `shouldRevalidate` if the fetcher action redirects ([#10208](https://github.com/remix-run/react-router/pull/10208)) +- Properly handle `lazy()` errors during router initialization ([#10201](https://github.com/remix-run/react-router/pull/10201)) +- Remove `instanceof` check for `DeferredData` to be resilient to ESM/CJS boundaries in SSR bundling scenarios ([#10247](https://github.com/remix-run/react-router/pull/10247)) +- Update to latest `@remix-run/web-fetch@4.3.3` ([#10216](https://github.com/remix-run/react-router/pull/10216)) + +**Full Changelog**: [`v6.9.0...v6.10.0`](https://github.com/remix-run/react-router/compare/react-router@6.9.0...react-router@6.10.0) + +## v6.9.0 + +Date: 2023-03-10 + +### What's Changed + +#### `Component`/`ErrorBoundary` route properties + +React Router now supports an alternative way to define your route `element` and `errorElement` fields as React Components instead of React Elements. You can instead pass a React Component to the new `Component` and `ErrorBoundary` fields if you choose. There is no functional difference between the two, so use whichever approach you prefer ๐Ÿ˜€. You shouldn't be defining both, but if you do `Component`/`ErrorBoundary` will "win" + +**Example JSON Syntax** + +```jsx +// Both of these work the same: +const elementRoutes = [{ + path: '/', + element: , + errorElement: , +}] + +const componentRoutes = [{ + path: '/', + Component: Home, + ErrorBoundary: HomeError, +}] + +function Home() { ... } +function HomeError() { ... } +``` + +**Example JSX Syntax** + +```jsx +// Both of these work the same: +const elementRoutes = createRoutesFromElements( + } errorElement={ } /> +); + +const componentRoutes = createRoutesFromElements( + +); + +function Home() { ... } +function HomeError() { ... } +``` + +#### Introducing Lazy Route Modules + +In order to keep your application bundles small and support code-splitting of your routes, we've introduced a new `lazy()` route property. This is an async function that resolves the non-route-matching portions of your route definition (`loader`, `action`, `element`/`Component`, `errorElement`/`ErrorBoundary`, `shouldRevalidate`, `handle`). + +Lazy routes are resolved on initial load and during the `loading` or `submitting` phase of a navigation or fetcher call. You cannot lazily define route-matching properties (`path`, `index`, `children`) since we only execute your lazy route functions after we've matched known routes. + +Your `lazy` functions will typically return the result of a dynamic import. + +```jsx +// In this example, we assume most folks land on the homepage so we include that +// in our critical-path bundle, but then we lazily load modules for /a and /b so +// they don't load until the user navigates to those routes +let routes = createRoutesFromElements( + }> + } /> + import("./a")} /> + import("./b")} /> + , +); +``` + +Then in your lazy route modules, export the properties you want defined for the route: + +```jsx +export async function loader({ request }) { + let data = await fetchData(request); + return json(data); +} + +// Export a `Component` directly instead of needing to create a React Element from it +export function Component() { + let data = useLoaderData(); + + return ( + <> +

You made it!

+

{data}

+ + ); +} + +// Export an `ErrorBoundary` directly instead of needing to create a React Element from it +export function ErrorBoundary() { + let error = useRouteError(); + return isRouteErrorResponse(error) ? ( +

+ {error.status} {error.statusText} +

+ ) : ( +

{error.message || error}

+ ); +} +``` + +An example of this in action can be found in the [`examples/lazy-loading-router-provider`](https://github.com/remix-run/react-router/tree/main/examples/lazy-loading-router-provider) directory of the repository. For more info, check out the [`lazy` docs](https://reactrouter.com/v6/route/lazy). + +๐Ÿ™Œ Huge thanks to @rossipedia for the [Initial Proposal](https://github.com/remix-run/react-router/discussions/9826) and [POC Implementation](https://github.com/remix-run/react-router/pull/9830). + +### Minor Changes + +- Add support for `route.Component`/`route.ErrorBoundary` properties ([#10045](https://github.com/remix-run/react-router/pull/10045)) +- Add support for `route.lazy` ([#10045](https://github.com/remix-run/react-router/pull/10045)) + +### Patch Changes + +- Improve memoization for context providers to avoid unnecessary re-renders ([#9983](https://github.com/remix-run/react-router/pull/9983)) +- Fix `generatePath` incorrectly applying parameters in some cases ([#10078](https://github.com/remix-run/react-router/pull/10078)) +- `[react-router-dom-v5-compat]` Add missed data router API re-exports ([#10171](https://github.com/remix-run/react-router/pull/10171)) + +**Full Changelog**: [`v6.8.2...v6.9.0`](https://github.com/remix-run/react-router/compare/react-router@6.8.2...react-router@6.9.0) + +## v6.8.2 + +Date: 2023-02-27 + +### Patch Changes + +- Treat same-origin absolute URLs in `` as external if they are outside of the router `basename` ([#10135](https://github.com/remix-run/react-router/pull/10135)) +- Correctly perform a hard redirect for same-origin absolute URLs outside of the router `basename` ([#10076](https://github.com/remix-run/react-router/pull/10076)) +- Fix SSR of absolute `` urls ([#10112](https://github.com/remix-run/react-router/pull/10112)) +- Properly escape HTML characters in `StaticRouterProvider` serialized hydration data ([#10068](https://github.com/remix-run/react-router/pull/10068)) +- Fix `useBlocker` to return `IDLE_BLOCKER` during SSR ([#10046](https://github.com/remix-run/react-router/pull/10046)) +- Ensure status code and headers are maintained for `defer` loader responses in `createStaticHandler`'s `query()` method ([#10077](https://github.com/remix-run/react-router/pull/10077)) +- Change `invariant` to an `UNSAFE_invariant` export since it's only intended for internal use ([#10066](https://github.com/remix-run/react-router/pull/10066)) + +**Full Changelog**: [`v6.8.1...v6.8.2`](https://github.com/remix-run/react-router/compare/react-router@6.8.1...react-router@6.8.2) + +## v6.8.1 + +Date: 2023-02-06 + +### Patch Changes + +- Remove inaccurate console warning for POP navigations and update active blocker logic ([#10030](https://github.com/remix-run/react-router/pull/10030)) +- Only check for differing origin on absolute URL redirects ([#10033](https://github.com/remix-run/react-router/pull/10033)) +- Improved absolute url detection in `Link` component (now also supports `mailto:` urls) ([#9994](https://github.com/remix-run/react-router/pull/9994)) +- Fix partial object (search or hash only) pathnames losing current path value ([#10029](https://github.com/remix-run/react-router/pull/10029)) + +**Full Changelog**: [`v6.8.0...v6.8.1`](https://github.com/remix-run/react-router/compare/react-router@6.8.0...react-router@6.8.1) + +## v6.8.0 + +Date: 2023-01-26 + +### Minor Changes + +Support absolute URLs in ``. If the URL is for the current origin, it will still do a client-side navigation. If the URL is for a different origin then it will do a fresh document request for the new origin. ([#9900](https://github.com/remix-run/react-router/pull/9900)) + +```tsx + {/* Document request */} + {/* Document request */} + {/* Client-side navigation */} +``` + +### Patch Changes + +- Fixes 2 separate issues for revalidating fetcher `shouldRevalidate` calls ([#9948](https://github.com/remix-run/react-router/pull/9948)) + - The `shouldRevalidate` function was only being called for _explicit_ revalidation scenarios (after a mutation, manual `useRevalidator` call, or an `X-Remix-Revalidate` header used for cookie setting in Remix). It was not properly being called on _implicit_ revalidation scenarios that also apply to navigation `loader` revalidation, such as a change in search params or clicking a link for the page we're already on. It's now correctly called in those additional scenarios. + - The parameters being passed were incorrect and inconsistent with one another since the `current*`/`next*` parameters reflected the static `fetcher.load` URL (and thus were identical). Instead, they should have reflected the navigation that triggered the revalidation (as the `form*` parameters did). These parameters now correctly reflect the triggering navigation. +- Fix bug with search params removal via `useSearchParams` ([#9969](https://github.com/remix-run/react-router/pull/9969)) +- Respect `preventScrollReset` on `` ([#9963](https://github.com/remix-run/react-router/pull/9963)) +- Fix navigation for hash routers on manual URL changes ([#9980](https://github.com/remix-run/react-router/pull/9980)) +- Use `pagehide` instead of `beforeunload` for ``. This has better cross-browser support, specifically on Mobile Safari. ([#9945](https://github.com/remix-run/react-router/pull/9945)) +- Do not short circuit on hash change only mutation submissions ([#9944](https://github.com/remix-run/react-router/pull/9944)) +- Remove `instanceof` check from `isRouteErrorResponse` to avoid bundling issues on the server ([#9930](https://github.com/remix-run/react-router/pull/9930)) +- Detect when a `defer` call only contains critical data and remove the `AbortController` ([#9965](https://github.com/remix-run/react-router/pull/9965)) +- Send the name as the value when url-encoding `File` `FormData` entries ([#9867](https://github.com/remix-run/react-router/pull/9867)) +- `react-router-dom-v5-compat` - Fix SSR `useLayoutEffect` `console.error` when using `CompatRouter` ([#9820](https://github.com/remix-run/react-router/pull/9820)) + +**Full Changelog**: [`v6.7.0...v6.8.0`](https://github.com/remix-run/react-router/compare/react-router@6.7.0...react-router@6.8.0) + +## v6.7.0 + +Date: 2023-01-18 + +### Minor Changes + +- Add `unstable_useBlocker`/`unstable_usePrompt` hooks for blocking navigations within the app's location origin ([#9709](https://github.com/remix-run/react-router/pull/9709), [#9932](https://github.com/remix-run/react-router/pull/9932)) +- Add `preventScrollReset` prop to `` ([#9886](https://github.com/remix-run/react-router/pull/9886)) + +### Patch Changes + +- Added pass-through event listener options argument to `useBeforeUnload` ([#9709](https://github.com/remix-run/react-router/pull/9709)) +- Fix `generatePath` when optional params are present ([#9764](https://github.com/remix-run/react-router/pull/9764)) +- Update `` to accept `ReactNode` as children function return result ([#9896](https://github.com/remix-run/react-router/pull/9896)) +- Improved absolute redirect url detection in actions/loaders ([#9829](https://github.com/remix-run/react-router/pull/9829)) +- Fix URL creation with memory histories ([#9814](https://github.com/remix-run/react-router/pull/9814)) +- Fix scroll reset if a submission redirects ([#9886](https://github.com/remix-run/react-router/pull/9886)) +- Fix 404 bug with same-origin absolute redirects ([#9913](https://github.com/remix-run/react-router/pull/9913)) +- Streamline `jsdom` bug workaround in tests ([#9824](https://github.com/remix-run/react-router/pull/9824)) + +**Full Changelog**: [`v6.6.2...v6.7.0`](https://github.com/remix-run/react-router/compare/react-router@6.6.2...react-router@6.7.0) + +## v6.6.2 + +Date: 2023-01-09 + +### Patch Changes + +- Ensure `useId` consistency during SSR ([#9805](https://github.com/remix-run/react-router/pull/9805)) + +**Full Changelog**: [`v6.6.1...v6.6.2`](https://github.com/remix-run/react-router/compare/react-router@6.6.1...react-router@6.6.2) + +## v6.6.1 + +Date: 2022-12-23 + +### Patch Changes + +- Include submission info in `shouldRevalidate` on action redirects ([#9777](https://github.com/remix-run/react-router/pull/9777), [#9782](https://github.com/remix-run/react-router/pull/9782)) +- Reset `actionData` on action redirect to current location ([#9772](https://github.com/remix-run/react-router/pull/9772)) + +**Full Changelog**: [`v6.6.0...v6.6.1`](https://github.com/remix-run/react-router/compare/react-router@6.6.0...react-router@6.6.1) + +## v6.6.0 + +Date: 2022-12-21 + +### What's Changed + +This minor release is primarily to stabilize our SSR APIs for Data Routers now that we've wired up the new `RouterProvider` in Remix as part of the [React Router-ing Remix](https://remix.run/blog/react-routering-remix) work. + +### Minor Changes + +- Remove `unstable_` prefix from `createStaticHandler`/`createStaticRouter`/`StaticRouterProvider` ([#9738](https://github.com/remix-run/react-router/pull/9738)) +- Add `useBeforeUnload()` hook ([#9664](https://github.com/remix-run/react-router/pull/9664)) + +### Patch Changes + +- Support uppercase `` and `useSubmit` method values ([#9664](https://github.com/remix-run/react-router/pull/9664)) +- Fix `
+ + {blocker.state === "blocked" ? ( +
+

You have unsaved changes!

+ + +

+ ) : blocker.state === "proceeding" ? ( +

Navigating away with unsaved changes...

+ ) : null} + + ); +} +``` + +The `blocker` received by the user would be either `unblocked`, `blocked`, or `proceeding`: + +- `unblocked` is the normal idle state +- `blocked` means the user tried to navigate and the blocker function returned `true` and the navigation was blocked. When in a `blocked` state the blocker would expose `proceed`/`reset` functions: + - `blocker.proceed()` would allow the blocked navigation to happen (and thus lose unsaved changes). This proceed navigation would _not_ re-run the blocker function. + - `blocker.reset()` would reset the blocker back to `unblocked` and remain on the current page +- `proceeding` indicates the navigation from `blocker.proceed()` is in-progress - and essentially reflects the non-`idle` `navigation.state` during that navigation + +Other navigations and/or interruptions to proceeding navigations would reset the blocker back to an unblocked state. + +~We will not provide a `usePrompt` implementation, however it would be somewhat trivial to implement that on top of `useBlocker` in userland.~ + +We decided in the end to include a `usePrompt` even though it's got more broken edge cases than `useBlocker`: + +- It's only a handful of lines of code +- It's more similar to what we had in v5 +- We don't know for sure how many folks were using this in v5, since the github commenters are not a complete sample +- It has a lower barrier to implement than a custom modal UI +- We plan to document that it breaks in more cases, in weird ways, and even differently across browsers. + +### Blocker State Diagram + +```mermaid +graph TD; + Unblocked -->|navigate| A{shouldBlock?}; + A -->|false| Unblocked; + A -->|true| Blocked; + Blocked -->|blocker.proceed| Proceeding; + Blocked -->|Unblocked Navigation| Unblocked; + Blocked -->|blocker.reset| Unblocked; + Proceeding -->|Navigation Complete| Unblocked; + Proceeding -->|Navigation Interrupted| Unblocked; +``` + +### Open Questions + +- Initial implementation is for data-router usage (6.4+). We still need to back-port to 6.3 and earlier to help folks migrate from `v5 -> v6 BrowserRouter -> v6 RouterProvider` + - We decided that this can just be net-new 6.4+ API. A v5 app should be able to migrate to a 6.4+ `RouterProvider` just as easily as a 6.3 `BrowserRouter` +- We should probably pass the `historyAction`/`location` of the active navigation to `shouldBlock()` similar to how v5 did it. Should we also pass the submission (`formMethod`, `formData`, etc.)? + - For now we landed on calling the blocker function with `{ currentLocation, nextLocation, historyAction }` to align naming loosely with `shouldRevalidate`. Can always extend that API ion the future if needed (with form submission info). +- I think since we are not providing `usePrompt`, we should accept a `beforeUnload:boolean` option to add cross-navigation handling in an opt-in fashion. + - `beforeUnload` is also unreliable because it does not prevent the user from doing additional back/forward navigations ao this is not included out of the box and can be implemented in user-land. diff --git a/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md b/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md new file mode 100644 index 0000000000..35a6ec0a36 --- /dev/null +++ b/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md @@ -0,0 +1,113 @@ +# Use `npm` to manage NPM dependencies for Deno projects + +Date: 2022-05-10 + +Status: accepted + +## Context + +Deno has three ways to manage dependencies: + +1. Inlined URL imports: `import {...} from "https://deno.land/x/blah"` +2. [deps.ts](https://deno.land/manual/examples/manage_dependencies) +3. [Import maps](https://deno.land/manual/linking_to_external_code/import_maps) + +Additionally, NPM packages can be accessed as Deno modules via [Deno-friendly CDNs](https://deno.land/manual/node/cdns#deno-friendly-cdns) like https://esm.sh. + +Remix has some requirements around dependencies: + +- Remix treeshakes dependencies that are free of side-effects. +- Remix sets the environment (dev/prod/test) across all code, including dependencies, at runtime via the `NODE_ENV` environment variable. +- Remix depends on some NPM packages that should be specified as peer dependencies (notably, `react` and `react-dom`). + +### Treeshaking + +To optimize bundle size, Remix [treeshakes](https://esbuild.github.io/api/#tree-shaking) your app's code and dependencies. +This also helps to separate browser code and server code. + +Under the hood, the Remix compiler uses [esbuild](https://esbuild.github.io). +Like other bundlers, `esbuild` uses [`sideEffects` in `package.json` to determine when it is safe to eliminate unused imports](https://esbuild.github.io/api/#conditionally-injecting-a-file). + +Unfortunately, URL imports do not have a standard mechanism for marking packages as side-effect free. + +### Setting dev/prod/test environment + +Deno-friendly CDNs set the environment via a query parameter (e.g. `?dev`), not via an environment variable. +That means changing environment requires changing the URL import in the source code. +While you could use multiple import maps (`dev.json`, `prod.json`, etc...) to workaround this, import maps have other limitations: + +- standard tooling for managing import maps is not available +- import maps are not composeable, so any dependencies that use import maps must be manually accounted for + +### Specifying peer dependencies + +Even if import maps were perfected, CDNs compile each dependency in isolation. +That means that specifying peer dependencies becomes tedious and error-prone as the user needs to: + +- determine which dependencies themselves depend on `react` (or other similar peer dependency), even if indirectly. +- manually figure out which `react` version works across _all_ of these dependencies +- set that version for `react` as a query parameter in _all_ of the URLs for the identified dependencies + +If any dependencies change (added, removed, version change), +the user must repeat all of these steps again. + +## Decision + +### Use `npm` to manage NPM dependencies for Deno + +Do not use Deno-friendly CDNs for NPM dependencies in Remix projects using Deno. + +Use `npm` and `node_modules/` to manage NPM dependencies like `react` for Remix projects, even when using Deno with Remix. + +Deno module dependencies (e.g. from `https://deno.land`) can still be managed via URL imports. + +### Allow URL imports + +Remix will preserve any URL imports in the built bundles as external dependencies, +letting your browser runtime and server runtime handle them accordingly. +That means that you may: + +- use URL imports for the browser +- use URL imports for the server, if your server runtime supports it + +For example, Node will throw errors for URL imports, while Deno will resolve URL imports as normal. + +### Do not support import maps + +Remix will not yet support import maps. + +## Consequences + +- URL imports will not be treeshaken. +- Users can specify environment via the `NODE_ENV` environment variable at runtime. +- Users won't have to do error-prone, manual dependency resolution. + +### VS Code type hints + +Users may configure an import map for the [Deno extension for VS Code](denoland.vscode-deno) to enable type hints for NPM-managed dependencies within their Deno editor: + +`.vscode/resolve_npm_imports_in_deno.json` + +```json +{ + "// This import map is used solely for the denoland.vscode-deno extension.": "", + "// Remix does not support import maps.": "", + "// Dependency management is done through `npm` and `node_modules/` instead.": "", + "// Deno-only dependencies may be imported via URL imports (without using import maps).": "", + + "imports": { + "react": "https://esm.sh/react@18.0.0", + "react-dom": "https://esm.sh/react-dom@18.0.0", + "react-dom/server": "https://esm.sh/react-dom@18.0.0/server" + } +} +``` + +`.vscode/settings.json` + +```json +{ + "deno.enable": true, + "deno.importMap": "./.vscode/resolve_npm_imports_in_deno.json" +} +``` diff --git a/decisions/0002-do-not-clone-request.md b/decisions/0002-do-not-clone-request.md new file mode 100644 index 0000000000..30f599f7f0 --- /dev/null +++ b/decisions/0002-do-not-clone-request.md @@ -0,0 +1,19 @@ +# Do not clone request + +Date: 2022-05-13 + +Status: accepted + +## Context + +To allow multiple loaders / actions to read the body of a request, we have been cloning the request before forwarding it to user-code. This is not the best thing to do as some runtimes will begin buffering the body to allow for multiple consumers. It also goes against "the platform" that states a request body should only be consumed once. + +## Decision + +Do not clone requests before they are passed to user-code (actions, handleDocumentRequest, handleDataRequest), and remove body from request passed to loaders. Loaders should be thought of as a "GET" / "HEAD" request handler. These request methods are not allowed to have a body, therefore you should not be reading it in your Remix loader function. + +## Consequences + +Loaders always receive a null body for the request. + +If you are reading the request body in both an action and handleDocumentRequest or handleDataRequest this will now fail as the body will have already been read. If you wish to continue reading the request body in multiple places for a single request against recommendations, consider using `.clone()` before reading it; just know this comes with tradeoffs. diff --git a/decisions/0002-lazy-route-modules.md b/decisions/0002-lazy-route-modules.md new file mode 100644 index 0000000000..2978903c82 --- /dev/null +++ b/decisions/0002-lazy-route-modules.md @@ -0,0 +1,248 @@ +# Lazy Route Modules + +Date: 2023-02-21 + +Status: accepted + +## Context + +In a data-aware React Router application (``), the router needs to be aware of the route tree ahead of time so it can match routes and execute loaders/actions _prior_ to rendering the destination route. This is different than in non-data-aware React Router applications (``) where you could nest `` sub-tree anywhere in your application, and compose together `` and `React.lazy()` to dynamically load "new" portions of your routing tree as the user navigated through the application. The downside of this approach in `BrowserRouter` is that it's a render-then-fetch cycle which produces network waterfalls and nested spinners, two things that we're aiming to eliminate in `RouterProvider` applications. + +There were ways to [manually code-split][manually-code-split] in a `RouterProvider` application but they can be a bit verbose and tedious to do manually. As a result of this DX, we received a [Remix Route Modules Proposal][proposal] from the community along with a [POC implementation][poc] (thanks `@rossipedia` ๐Ÿ™Œ). + +## Original POC + +The original POC idea was to implement this in user-land where `element`/`errorElement` would be transformed into `React.lazy()` calls and `loader`/`action` would load the module and then execute the `loader`/`action`: + +```js +// Assuming route.module is a function returning a Remix-style route module +let Component = React.lazy(route.module); +route.element = ; +route.loader = async (args) => { + const { loader } = await route.module(); + return typeof loader === "function" ? loader(args) : null; +}; +``` + +This approach got us pretty far but suffered from some limitations being done in user-land since it did not have access to some router internals to make for a more seamless integration. Namely, it _had_ to put every possible property onto a route since it couldn't know ahead of time whether the route module would resolve with the matching property. For example, will `import('./route')` return an `errorElement`? Who knows! + +To combat this, a `route.use` property was considered which would allow the user to define the exports of the module: + +```js +const route = { + path: "/", + module: () => import("./route"), + use: ["loader", "element"], +}; +``` + +This wasn't ideal since it introduced a tight coupling of the file contents and the route definitions. + +Furthermore, since the goal of `RouterProvider` is to reduce spinners, it felt incorrect to automatically introduce `React.lazy` and thus expect Suspense boundaries for elements that we expected to be fully fetched _prior_ to rendering the destination route. + +## Decision + +Given what we learned from the original POC, we felt we could do this a bit leaner with an implementation inside the router. Data router apps already have an asynchronous pre-render flow where we could hook in and run this logic. A few advantages of doing this inside of the router include: + +- We can load at a more specific spot internal to the router +- We can access the navigation `AbortSignal` in case the `lazy()` call gets interrupted +- We can also load once and update the internal route definition so subsequent navigations don't have a repeated `lazy()` call +- We don't have issue with knowing whether or not an `errorElement` exists since we will have updated the route prior to updating any UI state + +This proved to work out quite well as we did our own POC so we went with this approach in the end. Now, any time we enter a `submitting`/`loading` state we first check for a `route.lazy` definition and resolve that promise first and update the internal route definition with the result. + +The resulting API looks like this, assuming you want to load your homepage in the main bundle, but lazily load the code for the `/about` route. Note we're using the new `Component` API introduced along with this work. + +```jsx +// app.jsx +const router = createBrowserRouter([ + { + path: "/", + Component: Layout, + children: [ + { + index: true, + Component: Home, + }, + { + path: "about", + lazy: () => import("./about"), + }, + ], + }, +]); +``` + +And then your `about.jsx` file would export the properties to be lazily defined on the route: + +```jsx +// about.jsx +export function loader() { ... } + +export function Component() { ... } +``` + +## Choices + +Here's a few choices we made along the way: + +### Immutable Route Properties + +A route has 3 types of fields defined on it: + +- Path matching properties: `path`, `index`, `caseSensitive` and `children` + - While not strictly used for matching, `id` is also considered static since it is needed up-front to uniquely identify all defined routes +- Data loading properties: `loader`, `action`, `hasErrorBoundary`, `shouldRevalidate` +- Rendering properties: `handle` and the framework-aware `element`/`errorElement`/`Component`/`ErrorBoundary` + +The `route.lazy()` method is focused on lazy-loading the data loading and rendering properties, but cannot update the path matching properties because we have to path match _first_ before we can even identify which matched routes include a `lazy()` function. Therefore, we do not allow path matching route keys to be updated by `lazy()`, and will log a warning if you return one of those properties from your lazy() method. + +## Static Route Properties + +Similar to how you cannot override any immutable path-matching properties, you also cannot override any statically defined data-loading or rendering properties (and will log the a console warning if you attempt to). This allows you to statically define aspects that you don't need (or wish) to lazy load. Two potential use-cases her might be: + +1. Using a small statically-defined `loader`/`action` which just hits an API endpoint to load/submit data. + - In fact this is an interesting option we've optimized React Router to detect this and call any statically defined loader/action handlers in parallel with `lazy` (since `lazy` will be unable to update the `loader`/`action` anyway!). This will provide the ability to obtain the most-optimal parallelization of loading your component in parallel with your data fetches. +2. Re-using a common statically-defined `ErrorBoundary` across multiple routes + +### Addition of route `Component` and `ErrorBoundary` fields + +In React Router v6, routes define `element` properties because it allows static prop passing as well as fitting nicely in the JSX render-tree-defined route trees: + +```jsx + + + } /> + + +``` + +However, in a React Router 6.4+ landscape when using `RouterProvider`, routes are defined statically up-front to enable data-loading, so using element feels arguably a bit awkward outside of a JSX tree: + +```js +const routes = [ + { + path: "/", + element: , + }, +]; +``` + +It also means that you cannot easily use hooks inline, and have to add a level of indirection to access hooks. + +This gets a bit more awkward with the introduction of `lazy()` since your file now has to export a root-level JSX element: + +```jsx +// home.jsx +export const element = + +function Homepage() { ... } +``` + +In reality, what we want in this "static route definition" landscape is just the component for the Route: + +```js +const routes = [ + { + path: "/", + Component: Homepage, + }, +]; +``` + +This has a number of advantages in that we can now use inline component functions to access hooks, provide props, etc. And we also simplify the exports of a `lazy()` route module: + +```jsx +const routes = [ + { + path: "/", + // You can include just the component + Component: Homepage, + }, + { + path: "/a", + // Or you can inline your component and pass props + Component: () => , + }, + { + path: "/b", + // And even use use hooks without indirection ๐Ÿ’ฅ + Component: () => { + let data = useLoaderData(); + return ; + }, + }, +]; +``` + +So in the end, the work for `lazy()` introduced support for `route.Component` and `route.ErrorBoundary`, which can be statically or lazily defined. They will take precedence over `element`/`errorElement` if both happen to be defined, but for now both are acceptable ways to define routes. We think we'll be expanding the `Component` API in the future for stronger type-safety since we can pass it inferred-type `loaderData` etc. so in the future that _may_ become the preferred API. + +### Interruptions + +Previously when a link was clicked or a form was submitted, since we had the `action`/`loader` defined statically up-front, they were immediately executed and there was no chance for an interruption _before calling the handler_. Now that we've introduced the concept of `lazy()` there is a period of time prior to executing the handler where the user could interrupt the navigation by clicking to a new location. In order to keep behavior consistent with lazily-loaded routes and statically defined routes, if a `lazy()` function is interrupted React Router _will still call the returned handler_. As always, the user can leverage `request.signal.aborted` inside the handler to short-circuit on interruption if desired. + +This is important because `lazy()` is only ever run once in an application session. Once lazy has completed it updates the route in place, and all subsequent navigations to that route use the now-statically-defined properties. Without this behavior, routes would behave differently on the _first_ navigation versus _subsequent_ navigations which could introduce subtle and hard-to-track-down bugs. + +Additionally, since `lazy()` functions are intended to return a static definition of route `loader`/`element`/etc. - if multiple navigations happen to the same route in parallel, the first `lazy()` call to resolve will "win" and update the route, and the returned values from any other `lazy()` executions will be ignored. This should not be much of an issue in practice though as modern bundlers latch onto the same promise for repeated calls to `import()` so in those cases the first call will still "win". + +### Error Handling + +If an error is thrown by `lazy()` we catch that in the same logic as if the error was thrown by the `action`/`loader` and bubble it to the nearest `errorElement`. + +## Consequences + +Not so much as a consequence, but more of limitation - we still require the routing tree up-front for the most efficient data-loading. This means that we can't _yet_ support quite the same nested `` use-cases as before (particularly with respect to microfrontends), but we have ideas for how to solve that as an extension of this concept in the future. + +Another slightly edge-case concept we discovered is that in DIY SSR applications using `createStaticHandler` and `StaticRouterProvider`, it's possible to server-render a lazy route and send up its hydration data. But then we may _not_ have those routes loaded in our client-side hydration: + +```jsx +const routes = [{ + path: '/', + lazy: () => import("./route"), +}] +let router = createBrowserRouter(routes, { + hydrationData: window.__hydrationData, +}); + +// โš ๏ธ At this point, the router has the data but not the route definition! + +ReactDOM.hydrateRoot( + document.getElementById("app")!, + +); +``` + +In the above example, we've server-rendered our `/` route and therefore we _don't_ want to render a `fallbackElement` since we already have the SSR'd content, and the router doesn't need to "initialize" because we've provided the data in `hydrationData`. However, if we're hydrating into a route that includes `lazy`, then we _do_ need to initialize that lazy route. + +The real solution for this is to do what Remix does and know your matched routes and preload their modules ahead of time and hydrate with synchronous route definitions. This is a non-trivial process through so it's not expected that every DIY SSR use-case will handle it. Instead, the router will not be initialized until any initially matched lazy routes are loaded, and therefore we need to delay the hydration or our `RouterProvider`. + +The recommended way to do this is to manually match routes against the initial location and load/update any lazy routes before creating your router: + +```jsx +// Determine if any of the initial routes are lazy +let lazyMatches = matchRoutes(routes, window.location)?.filter( + (m) => m.route.lazy +); + +// Load the lazy matches and update the routes before creating your router +// so we can hydrate the SSR-rendered content synchronously +if (lazyMatches && lazyMatches.length > 0) { + await Promise.all( + lazyMatches.map(async (m) => { + let routeModule = await m.route.lazy!(); + Object.assign(m.route, { ...routeModule, lazy: undefined }); + }) + ); +} + +// Create router and hydrate +let router = createBrowserRouter(routes) +ReactDOM.hydrateRoot( + document.getElementById("app")!, + +); +``` + +[manually-code-split]: https://www.infoxicator.com/en/react-router-6-4-code-splitting +[proposal]: https://github.com/remix-run/react-router/discussions/9826 +[poc]: https://github.com/remix-run/react-router/pull/9830 diff --git a/decisions/0003-data-strategy.md b/decisions/0003-data-strategy.md new file mode 100644 index 0000000000..cb8884363e --- /dev/null +++ b/decisions/0003-data-strategy.md @@ -0,0 +1,420 @@ +# Data Strategy + +Date: 2024-01-31 + +Status: accepted + +## Context + +In order to implement "Single Fetch" in Remix ([Issue][single-fetch-issue], [RFC][single-fetch-rfc]), we need to expose some level of control over the internal data fetching behaviors of the `@remix-run/router`. This way, while React Router will run loaders in parallel by default, Remix can opt-into making a single fetch call to the server for all loaders. + +## Decisions + +### `dataStrategy` + +To achieve the above, we propose to add an optional `dataStrategy` config which can be passed in by the application. The idea is that `dataStrategy` will accept an array of `matches` to load and will return a parallel array of results for those matches. + +```js +function dataStrategy(arg: DataStrategyFunctionArgs): DataResult[]; + +interface DataStrategyFunctionArgs + extends DataFunctionArgs { + matches: AgnosticDataStrategyMatch[]; +} + +interface DataFunctionArgs { + request: Request; + params: Params; + context?: Context; +} +``` + +There's a [comment][responsibilities-comment] here from Jacob which does a good job of outlining the current responsibilities, but basically React Router in it's current state handles 4 aspects when it comes to executing loaders for a given URL - `dataStrategy` is largely intended to handle step 3: + +1. Match routes for URL +2. Determine what routes to load (via `shouldRevalidate`) +3. Call `loader` functions in parallel +4. Decode Responses + +### Inputs + +The primary input is `matches`, since the user needs to know what routes match and eed to have loaders executed. We also wanted to provide a way for the user to call the "default" internal behavior so they could easily change from parallel to sequential without having to re-invent the wheel and manually call loaders, decode responses, etc. The first idea for this API was to pass a `defaultStrategy(match)` parameter so they could call that per-match: + +```js +function dataStrategy({ matches }) { + // Call in parallel + return Promise.all(matches.map(m => defaultStrategy((m)))); + + // Call sequentially + let results = [] + for (let match of matches) { + results.push(await defaultStrategy(match)) + } + return results; +} +``` + +โš ๏ธ `defaultStrategy` was eliminated in favor of `match.resolve`. + +We also originally intended to expose a `type: 'loader' | 'action`' field as a way to presumably let them call `match.route.loader`/`match.route.action` directly - but we have since decided against that with the `match.resolve` API. + +โš ๏ธ `type` was eliminated in favor of `match.resolve`. + +`dataStrategy` is control _when_ handlers are called, not _how_. RR is in charge of calling them with the right parameters. + +### Outputs + +Originally, we planned on making the `DataResult` API public, which is a union of the different types of results (`SuccessResult`, `ErrorResult`, `RedirectResult`, `DeferResult`). However, as we kept evolving and did some internal refactoring to separate calling loaders from decoding results - we realized that all we really need is a simpler `HandlerResult`: + +```ts +interface HandlerResult { + type: ResultType.success | ResultType.error; + result: any; +} +``` + +If the user returns us one of those per-match, we can internally convert it to a `DataResult`. + +- If `result` is a `Response` then we can handle unwrapping the data and processing any redirects (may produce a `SuccessResult`, `ErrorResult`, or `RedirectResult`) +- If `result` is a `DeferredData` instance, convert to `DeferResult` +- If result is anything else we don't touch the data, it's either a `SuccessResult` or `ErrorResult` based on `type` + - This is important because it's lets the end user opt into a different decoding strategy of their choice. If they return us a Response, we decode it. If not, we don't touch it. + +### Decoding Responses + +Initially, we intended for `dataStrategy` to handle (3), and considered an optional `decodeResponse` API for (4) - but we decided that the decoding of responses was a small enough undertaking using standard Fetch APIs (i.e., `res.json`) that it didn't warrant a custom property - and they could just call those APIs directly. The `defaultStrategy` parameter would handle performing 3 the normal way that RR would. + +โš ๏ธ `decodeResponse` is made obsolete by `HandlerResult` + +### Handling `route.lazy` + +There's a nuanced step we missed in our sequential steps above. If a route was +using `route.lazy`, we may need to load the route before we can execute the `loader`. There's two options here: + +1. We pre-execute all `route.lazy` methods before calling `dataStrategy` +2. We let `dataStrategy` execute them accordingly + +(1) has a pretty glaring perf issue in that it blocks _any_ loaders from running until _all_ `route.lazy`'s have resolved. So if route A is super small but has a slow loader, and route B is large but has a fast loader: + +``` +|-- route a lazy --> |-- route a loader --------------->| +|-- route b lazy ------------------------>|-- route b loader --> | +``` + +This is no bueno. Instead, we want option (2) where the users can run these sequentially per-route - and "loading the route" is just part of the "loading the data" step + +``` +|-- route a lazy -->|-- route a loader ---------------> | +|-- route b lazy ------------------------>|-- route b loader -->| +``` + +Therefore, we're introducing the concept of a `DataStrategyMatch` which is just like a `RouteMatch` but the `match.route` field is a `Promise`. We'll kick off the executions of route.lazy and then you can wait for them to complete prior to calling the loader: + +```js +function dataStrategy({ matches, defaultStrategy }) { + return Promise.all( + matches.map((m) => match.route.then((route) => route.loader(/* ... */))), + ); +} +``` + +There are also statically defined properties that live outside of lazy, so those are extended right onto `match.route`. This allows you to define loaders statically and run them in parallel with `route.lazy`: + +```js +function dataStrategy({ matches, defaultStrategy }) { + // matches[0].route => Promise + // matches[0].route.id => string + // matches[0].route.index => boolean + // matches[0].route.path => string +} +``` + +โš ๏ธ This match.route as a function API was removed in favor of `match.resolve` + +### Handling `shouldRevalidate` behavior + +We considered how to handle `shouldRevalidate` behavior. There's sort of 2 basic approaches: + +1. We pre-filter and only hand the user `matchesToLoad` +2. We hand the user all matches and let them filter + - This would probably also require a new `defaultShouldRevalidate(match) => boolean` parameter passed to `dataStrategy` + +I _think_ (1) is preferred to keep the API at a minimum and avoid leaking into _other_ ways to opt-out of revalidation. We already have an API for that so let's lean into it. + +Additionally, another big con of (2) is that if we want to let them make revalidation decisions inside `dataStrategy` - we need to expose all of the information required for that (`currentUrl`, `currentParams`, `nextUrl`, `nextParams`, `submission` info, `actionResult`, etc.) - the API becomes a mess. + +Therefore we are aiming to stick with one and let `shouldRevalidate` be the only way to opt-out of revalidation. + +### Handling actions and fetchers + +Thus far, we've been mostly concerned with how to handle navigational loaders where they are multiple matched routes and loaders to run. But what about actions and fetchers where we only run a handler for a single leaf match? The quick answer to this is to just send a single-length array with the match in question: + +```js +// loaders +let matchesToLoad = getMatchesToLoad(request, matches); +let results = await dataStrategy({ + request, + params, + matches: matchesToLoad, + type: "loader", + defaultStrategy, +}); + +// action +let actionMatch = getTargetMatch(request, matches); +let actionResults = await dataStrategy({ + request, + params, + matches: [actionMatch], + type: "action", + defaultStrategy, +}); +let actionResult = actionResults[0]; + +// fetcher loader/action +let fetcherMatch = getTargetMatch(request, matches); +let fetcherResults = await dataStrategy({ + request, + params, + matches: [fetcherMatch], + type: "loader", // or "action" + defaultStrategy, +}); +let fetcherResult = fetcherResults[0]; +``` + +This way, the user's implementation can just always operate on the `matches` array and it'll work for all use cases. + +```js +// Sample strategy to run sequentially +async function dataStrategy({ request, params, matches, type }) { + let results = []; + for (let match of matches) { + let result = await match.route[type]({ request, params }); + result.push(result); + } + return results; +} +``` + +### What about middlewares? + +As we thought more and more about this API, it became clear that the concept of "process data for a route" (step 3 above) was not necessarily limited to the `loader`/`action` and that there are data-related APIs on the horizon such as `middleware` and `context` that would also fall under the `dataStrategy` umbrella! In fact, a well-implemented `dataStrategy` could alleviate the need for first-class APIs - even if only initially. Early adopters could use `dataStrategy` to implement their own middlewares and we could see which patterns rise to the top and adopt them as first class `route.middleware` or whatever. + +So how would middleware work? The general idea is that middleware runs sequentially top-down prior to the loaders running. And if you bring `context` into the equation - they also run top down and middlewares/loaders/actions receive the context from their level and above in the tree - but they do not "see" any context from below them in the tree. + +A user-land implementation turns out not to be too bad assuming routes define `middleware`/`context` on `handle`: + +```js +// Assume routes look like this: +let route = { + id: "parent", + path: "/parent", + loader: () => {}, + handle: { + // context can provide multiple keyed contexts + context: { + parent: () => ({ id: "parent" }), + }, + // middleware receives context as an argument + middleware(context) { + context.parent.whatever = "PARENT MIDDLEWARE"; + }, + }, +}; + +async function dataStrategy({ request, params, matches, type }) { + // Run context/middleware sequentially + let contexts = {}; + for (let match of matches) { + if (m.route.handle?.context) { + for (let [id, ctx] of Object.entries(m.route.handle.context)) { + contexts[key] = ctx(); + } + } + if (m.route.handle?.middleware) { + m.route.handle.middleware(context); + } + } + + // Run loaders in parallel (or run the solo action) + return Promise.all( + matches.map(async (m, i) => { + // Only expose contexts from this level and above + let context = matches.slice(0, i + 1).reduce((acc, m) => { + Object.keys(m.route.handle?.context).forEach((k) => { + acc[k] = contexts[k]; + }); + return acc; + }, {}); + try { + return { + type: ResultType.data, + data: await m.route[type]?.({ request, params, context });, + }; + } catch (error) { + return { + type: ResultType.error, + error, + }; + } + }) + ); +} +``` + +โŒ Nope - this doesn't actually work! + +Remember above where we decided to _pre-filter_ the matches based on `shouldRevalidate`? That breaks any concept of middleware since even if we don't intend to load a route, we need to run middleware on all parents before the loader. So we _must_ expose at least the `matches` at or above that level in the tree - and more likely _all_ matches to `dataStrategy` if it's to be able to implement middleware. + +And then, once we expose _multiple_ matches - we need to tell the user if they're supposed to actually run the handlers on those matches or only on a leaf/target match. + +I think there's a few options here: + +**Option 1 - `routeMatches` and `handlerMatches`** + +We could add a second array of the "full" set of matches for the route and then middleware would operate on that set, and handlers would operate on the filtered set (renamed to `handlerMatches`) here. This still preserves the pre-filtering and keeps `shouldRevalidate` logic out of `dataStrategy`. + +```js +async function dataStrategy({ request, params, routeMatches, handlerMatches, type }) { + // Run context/middleware sequentially + let contexts = {}; + for (let match of routeMatches) { ... } + + // Run loaders in parallel + return Promise.all( + handlerMatches.map(async (m, i) => { ... }) + ); +} +``` + +**Option 2 - new field on `DataStrategyMatch`** + +Since we're already introducing a concept of a `DataStrategyMatch` to handle `route.lazy`, we could lean into that and expose something on those matches that indicate if they need to have their handler run or not? + +```js +// Inside React Router, assume navigate from /a/ -> /b and we don't need to +// re-run the root loader +let dataStrategyMatches = [{ + route: { id: 'root', loader() {}, ... } + runHandler: false // determined via shouldRevalidate +}, { + route: { id: 'b', loader() {}, ... } + runHandler: true // determined via shouldRevalidate +}] +``` + +Then, the user could use this to differentiate between middlewares and handlers: + +```js +async function dataStrategy({ request, params, matches, type }) { + // Run context/middleware sequentially + let contexts = {}; + for (let match of matches) { ... } + + // Run loaders in parallel + let matchesToLoad = matches.filter(m => m.runHandler); + return Promise.all( + matchesToLoad.map(async (m, i) => { ... }) + ); +} +``` + +**Option 3 - new function on `DataStrategyMatch`** + +Extending on the idea above - it all started to feel super leaky and full of implementation-details. Why are users manually filtering? Or manually passing parameters to loaders/actions? Using a `type` field to know which to call? Waiting on a `match.route` Promise before calling the loader? + +That's wayyyy to many rough edges for us to document and users to get wrong (rightfully so!). + +Why can't we just do it all? Let's wrap _all_ of that up into a single `match.resolve()` function that: + +- Waits for `route.lazy` to resolve (if needed) +- No-ops if the route isn't supposed to revalidate + - Open question here if we return the _current_ data from these no-ops or return `undefined`? + - We decided _not_ to expose this data for now since we don't have a good use case +- Knows whether to call the `loader` or the `action` +- Allows users to pass _additional_ params to loaders/actions for middleware/context use cases. + +```js +// Simplest case - call all loaders in parallel just like current behavior +function dataStrategy({ matches }) { + // No more type, defaultStrategy, or match.route promise APIs! + return Promise.all(matches.map(match => { + // resolve `route.lazy` if needed and call loader/action + return m.resolve(); + }); +} + +// More advanced case - call loader sequentially passing a context through +async function dataStrategy({ matches }) { + let ctx = {}; + let results = []; + for (let match of matches) { + // You can pass a "handlerOverride" function to resolve giving you control + // over how/if to call the handler. The argument passed to `handler` will + // be passed as the second argument to your `loader`/`action`: + // function loader({ request }, ctx) {...} + let result = await m.resolve((handler) => { + return handler(ctx); + }); + results.push(result); + }); + return results; +} + +// More performant case leveraging a middleware type abstraction which lets loaders +// still run in parallel after sequential middlewares: +function dataStrategy({ matches }) { + // Can implement middleware as above since you now get all matches + let context = runMiddlewares(matches); + + // Call all loaders in parallel (no params to pass) but you _can_ pass you + // own argument to `resolve` and it will come in as `loader({ request }, handlerArg)` + // So you can send middleware context through to loaders/actions + return Promise.all(matches.map(match => { + return m.resolve(context); + }); + + // Note we don't do any filtering above - if a match doesn't need to load, + // `match.resolve` is no-op. Just like `serverLoader` is a no-op in `clientLoader` + // when it doesn't need to run +} + +// Advanced case - single-fetch type approach +// More advanced case - call loader sequentially passing a context through +async function dataStrategy({ matches }) { + let singleFetchData = await makeSingleFetchCall() + // Assume we get back: + // { data: { [routeId]: unknown }, errors: { [routeId]: unknown } } + let results = []; + for (let match of matches) { + // Don't even call the handler since we have the data we need from single fetch + let result = await m.resolve(() => { + if (singleFetchData.errors?.[m.route.id]) { + return { + type: 'error', + result: singleFetchData.errors?.[m.route.id] + } + } + return { + type: 'data', + result: singleFetchData.data?.[m.route.id] + } + }); + results.push(result); + }); + return results; +} +``` + +## Status codes + +Initially, we thought we could just let the `handlerOverride`return or throw and then internally we could convert the returned/thrown valuer into a `HandlerResult`. However, this didn't work for the `unstable_skipActionRevalidation` behavior we wanted to implement with Single Fetch. + +If users returned normal Response's it would be fine, since we could decode the response internally and also know the status. However, if user's wanted to do custom response decoding (i.e., use `turbo-stream` like we did in single fetch) then there was no way to return/throw data _and the status code from the response_ without introducing something like the `ErrorResponse` API which holds a status and data. We decided to make `HandlerResult` public API and put an optional `status` field on it. + +This means that if you just call resolve with no `handlerOverride` you never need to know about `HandlerResult`. If you do pass a `handlerOverride`, then you need to return a proper HandlerResult with `type:"data"|"error"`. + +[single-fetch-issue]: https://github.com/remix-run/remix/issues/7641 +[single-fetch-rfc]: https://github.com/remix-run/remix/discussions/7640 +[responsibilities-comment]: https://github.com/remix-run/remix/issues/7641#issuecomment-1836635069 diff --git a/decisions/0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md b/decisions/0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md new file mode 100644 index 0000000000..8394bc2a2e --- /dev/null +++ b/decisions/0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md @@ -0,0 +1,230 @@ +# Infer types for `useLoaderData` and `useActionData` from `loader` and `action` via generics + +Date: 2022-07-11 + +Status: Superseded by [#0012](./0012-type-inference.md) + +## Context + +Goal: End-to-end type safety for `useLoaderData` and `useActionData` with great Developer Experience (DX) + +Related discussions: + +- [remix-run/remix#1254](https://github.com/remix-run/remix/pull/1254) +- [remix-run/remix#3276](https://github.com/remix-run/remix/pull/3276) + +--- + +In Remix v1.6.4, types for both `useLoaderData` and `useActionData` are parameterized with a generic: + +```tsx +type MyLoaderData = { + /* ... */ +}; +type MyActionData = { + /* ... */ +}; + +export default function Route() { + const loaderData = useLoaderData(); + const actionData = useActionData(); + return
{/* ... */}
; +} +``` + +For end-to-end type safety, it is then the user's responsibility to make sure that `loader` and `action` also use the same type in the `json` generic: + +```ts +export const loader: LoaderFunction = () => { + return json({ + /* ... */ + }); +}; + +export const action: ActionFunction = () => { + return json({ + /* ... */ + }); +}; +``` + +### Diving into `useLoaderData`'s and `useActionData`'s generics + +Tracing through the `@remix-run/react` source code (v1.6.4), you'll find that `useLoaderData` returns an `any` type that is implicitly type cast to whatever type gets passed into the `useLoaderData` generic: + +```ts +// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L1370 +export function useLoaderData(): T { + return useRemixRouteContext().data; // +} + +// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L73 +function useRemixRouteContext(): RemixRouteContextType { + /* ... */ +} + +// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L56 +interface RemixRouteContextType { + data: AppData; + id: string; +} + +// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/data.ts#L4 +export type AppData = any; +``` + +Boiling this down, the code looks like: + +```ts +let data: any; + +// somewhere else, `loader` gets called an sets `data` to some value + +function useLoaderData(): T { + return data; // <-- Typescript casts this `any` to `T` +} +``` + +`useLoaderData` isn't basing its return type on how `data` was set (i.e. the return value of `loader`) nor is it validating the data. +It's just blindly casting `data` to whatever the user passed in for the generic `T`. + +### Issues with current approach + +The developer experience is subpar. +Users are required to write redundant code for the data types that could have been inferred from the arguments to `json`. +Changes to the data shape require changing _both_ the declared `type` or `interface` as well as the argument to `json`. + +Additionally, the current approach encourages users to pass the same type to `json` with the `loader` and to `useLoaderData`, but **this is a footgun**! +`json` can accept data types like `Date` that are JSON serializable, but `useLoaderData` will return the _serialized_ type: + +```ts +type MyLoaderData = { + birthday: Date; +}; + +export const loader: LoaderFunction = () => { + return json({ birthday: new Date("February 15, 1992") }); +}; + +export default function Route() { + const { birthday } = useLoaderData(); + // ^ `useLoaderData` tricks Typescript into thinking this is a `Date`, when in fact it's a `string`! +} +``` + +Again, the same goes for `useActionData`. + +### Solution criteria + +- Return type of `useLoaderData` and `useActionData` should somehow be inferred from `loader` and `action`, not blindly type cast +- Return type of `loader` and `action` should be inferred + - Necessarily, return type of `json` should be inferred from its input +- No module side-effects (so higher-order functions like `makeLoader` is definitely a no). +- `json` should allow everything that `JSON.stringify` allows. +- `json` should allow only what `JSON.stringify` allows. +- `useLoaderData` should not return anything that `JSON.parse` can't return. + +### Key insight: `loader` and `action` are an _implicit_ inputs + +While there's been interest in inferring the types for `useLoaderData` based on `loader`, there was [hesitance to use a Typescript generic to do so](https://github.com/remix-run/remix/pull/3276#issuecomment-1164764821). +Typescript generics are apt for specifying or inferring types for _inputs_, not for blindly type casting output types. + +A key factor in the decision was identifying that `loader` and `action` are _implicit_ inputs of `useLoaderData` and `useActionData`. + +In other words, if `loader` and `useLoaderData` were guaranteed to run in the same process (and not cross the network), then we could write `useLoaderData(loader)`, specifying `loader` as an explicit input for `useLoaderData`. + +```ts +// _conceptually_ `loader` is an input for `useLoaderData` +function useLoaderData(loader: Loader) { + /*...*/ +} +``` + +Though `loader` and `useLoaderData` exist together in the same file at development-time, `loader` does not exist at runtime in the browser. +Without the `loader` argument to infer types from, `useLoaderData` needs a way to learn about `loader`'s type at compile-time. + +Additionally, `loader` and `useLoaderData` are both managed by Remix across the network. +While it's true that Remix doesn't "own" the network in the strictest sense, having `useLoaderData` return data that does not correspond to its `loader` is an exceedingly rare edge-case. + +Same goes for `useActionData`. + +--- + +A similar case is how [Prisma](https://www.prisma.io/) infers types from database schemas available at runtime, even though there are (exceedingly rare) edge-cases where that database schema _could_ be mutated after compile-time but before run-time. + +## Decision + +Explicitly provide type of the implicit `loader` input for `useLoaderData` and then infer the return type for `useLoaderData`. +Do the same for `action` and `useActionData`. + +```ts +export const loader = async (args: LoaderArgs) => { + // ... + return json(/*...*/); +}; + +export default function Route() { + const data = useLoaderData(); + // ... +} +``` + +Additionally, the inferred return type for `useLoaderData` will only include serializable (JSON) types. + +### Return `unknown` when generic is omitted + +Omitting the generic for `useLoaderData` or `useActionData` results in `any` being returned. +This hides potential type errors from the user. +Instead, we'll change the return type to `unknown`. + +```ts +type MyLoaderData = { + /*...*/ +}; + +export default function Route() { + const data = useLoaderData(); + // ^? unknown +} +``` + +Note: Since this would be a breaking change, changing the return type to `unknown` will be slated for v2. + +### Deprecate non-inferred types via generics + +Passing in a non-inferred type for `useLoaderData` is hiding an unsafe type cast. +Using the `useLoaderData` in this way will be deprecated in favor of an explicit type cast that clearly communicates the assumptions being made: + +```ts +type MyLoaderData = { + /*...*/ +}; + +export default function Route() { + const dataGeneric = useLoaderData(); // <-- will be deprecated + const dataCast = useLoaderData() as MyLoaderData; // <- use this instead +} +``` + +## Consequences + +- Users can continue to provide non-inferred types by type casting the result of `useLoaderData` or `useActionData` +- Users can opt-in to inferred types by using `typeof loader` or `typeof action` at the generic for `useLoaderData` or `useActionData`. +- Return types for `loader` and `action` will be the sources-of-truth for the types inferred for `useLoaderData` and `useActionData`. +- Users do not need to write redundant code to align types across the network +- Return type of `useLoaderData` and `useActionData` will correspond to the JSON _serialized_ types from `json` calls in `loader` and `action`, eliminating a class of errors. +- `LoaderFunction` and `ActionFunction` should not be used when opting into type inference as they override the inferred return types.[^1] + +๐Ÿšจ Users who opt-in to inferred types **MUST** return a `TypedResponse` from `json` and **MUST NOT** return a bare object: + +```ts +const loader = () => { + // NO + return { hello: "world" }; + + // YES + return json({ hello: "world" }); +}; +``` + +[^1]: The proposed `satisfies` operator for Typescript would let `LoaderFunction` and `ActionFunction` enforce function types while preserving the narrower inferred return type: https://github.com/microsoft/TypeScript/issues/47920 diff --git a/decisions/0004-streaming-apis.md b/decisions/0004-streaming-apis.md new file mode 100644 index 0000000000..5677a48e36 --- /dev/null +++ b/decisions/0004-streaming-apis.md @@ -0,0 +1,193 @@ +--- +title: Remix (and React Router) Streaming APIs +--- + +# Title + +Date: 2022-07-27 + +Status: accepted + +## Context + +Remix aims to provide first-class support for React 18's streaming capabilities. Throughout the development process we went through many iterations and naming schemes around the APIs we plan to build into Remix to support streaming, so this document aims to lay out the final names we chose and the reasons behind it. + +It's also worth nothing that even in a single-page-application without SSR-streaming, the same concepts still apply so these decisions were made with React Router 6.4.0 in mind as well - which will support the same Data APIs from Remix. + +## Decision + +Streaming in Remix can be thought of as having 3 touch points with corresponding APIs: + +1. _Initiating_ a streamed response in your `loader` can be done by returning a `defer(object)` call from your `loader` in which some of the keys on `object` are `Promise` instances +2. _Accessing_ a streamed response from `useLoaderData` + 1. No new APIs here - when you return a `defer()` response from your loader, you'll get `Promise` values inside your `useLoaderData` object ๐Ÿ‘Œ +3. _Rendering_ a streamed value (with fallback and error handling) in your component + 1. You can render a `Promise` from `useLoaderData()` with the `` component + 2. `` accepts an `errorElement` prop to handle error UI + 3. `` should be wrapped with a `` component to handle your loading UI + +## Details + +In the spirit of `#useThePlatform` we've chosen to leverage the `Promise` API to represent these "eventually available" values. When Remix receives a `defer()` response back from a `loader`, it needs to serialize that `Promise` over the network to the client application (prompting Jacob to coin the phrase [_"promise teleportation over the network"_][promise teleportation] ๐Ÿ”ฅ). + +### Initiating + +In order to initiate a streamed response in your `loader`, you can use the `defer()` utility which accepts a JSON object with `Promise` values from your `loader`. + +```tsx +export async function loader() { + return defer({ + // Await this, don't stream + critical: await fetchCriticalData(), + // Don't await this - stream it! + lazy: fetchLazyData(), + }); +} +``` + +By not using `await` on `fetchLazyData()` Remix knows that this value is not ready yet _but eventually will be_ and therefore Remix will leverage a streamed HTTP response allowing it to send up the resolved/rejected value when available. Essentially serializing/teleporting that Promise over the network via a streamed HTTP response. + +Just like `json()`, the `defer()` will accept a second optional `responseInit` param that lets you customize the resulting `Response` (i.e., in case you need to set custom headers). + +The name `defer` was settled on as a corollary to ` +``` + +--- + +[$link]: https://www.youtube.com/watch?v=dQw4w9WgXcQ +[createbrowserrouter]: ./routers/create-browser-router diff --git a/docs/explanation/README b/docs/explanation/README new file mode 100644 index 0000000000..cdb5e70286 --- /dev/null +++ b/docs/explanation/README @@ -0,0 +1,5 @@ +Explanations: + +- Theoretical Knowledge +- Understanding Oriented +- Useful when we're studying diff --git a/docs/explanation/backend-for-frontend.md b/docs/explanation/backend-for-frontend.md new file mode 100644 index 0000000000..252b66b51a --- /dev/null +++ b/docs/explanation/backend-for-frontend.md @@ -0,0 +1,50 @@ +--- +title: Backend For Frontend +--- + +# Backend For Frontend + +[MODES: framework] + +
+
+ +While React Router can serve as your fullstack application, it also fits perfectly into the "Backend for Frontend" architecture. + +The BFF strategy employs a web server with a job scoped to serving the frontend web app and connecting it to the services it needs: your database, mailer, job queues, existing backend APIs (REST, GraphQL), etc. Instead of your UI integrating directly from the browser to these services, it connects to the BFF, and the BFF connects to your services. + +Mature apps already have a lot of backend application code in Ruby, Elixir, PHP, etc., and there's no reason to justify migrating it all to a server-side JavaScript runtime just to get the benefits of React Router. Instead, you can use your React Router app as a backend for your frontend. + +You can use `fetch` right from your loaders and actions to your backend. + +```tsx lines=[7,13,17] +import escapeHtml from "escape-html"; + +export async function loader() { + const apiUrl = "https://api.example.com/some-data.json"; + const res = await fetch(apiUrl, { + headers: { + Authorization: `Bearer ${process.env.API_TOKEN}`, + }, + }); + + const data = await res.json(); + + const prunedData = data.map((record) => { + return { + id: record.id, + title: record.title, + formattedBody: escapeHtml(record.content), + }; + }); + return { prunedData }; +} +``` + +There are several benefits of this approach vs. fetching directly from the browser. The highlighted lines above show how you can: + +1. Simplify third-party integrations and keep tokens and secrets out of client bundles +2. Prune the data down to send less kB over the network, speeding up your app significantly +3. Move a lot of code from browser bundles to the server, like `escapeHtml`, which speeds up your app. Additionally, moving code to the server usually makes your code easier to maintain since server-side code doesn't have to worry about UI states for async operations + +Again, React Router can be used as your only server by talking directly to the database and other services with server-side JavaScript APIs, but it also works perfectly as a backend for your frontend. Go ahead and keep your existing API server for application logic and let React Router connect the UI to it. diff --git a/docs/explanation/code-splitting.md b/docs/explanation/code-splitting.md new file mode 100644 index 0000000000..e5425dec20 --- /dev/null +++ b/docs/explanation/code-splitting.md @@ -0,0 +1,61 @@ +--- +title: Automatic Code Splitting +--- + +# Automatic Code Splitting + +[MODES: framework] + +
+
+ +When using React Router's framework features, your application is automatically code split to improve the performance of initial load times when users visit your application. + +## Code Splitting by Route + +Consider this simple route config: + +```tsx filename=app/routes.ts +import { + type RouteConfig, + route, +} from "@react-router/dev/routes"; + +export default [ + route("/contact", "./contact.tsx"), + route("/about", "./about.tsx"), +] satisfies RouteConfig; +``` + +Instead of bundling all routes into a single giant build, the modules referenced (`contact.tsx` and `about.tsx`) become entry points to the bundler. + +Because these entry points are coupled to URL segments, React Router knows just from a URL which bundles are needed in the browser, and more importantly, which are not. + +If the user visits `"/about"` then the bundles for `about.tsx` will be loaded but not `contact.tsx`. This drastically reduces the JavaScript footprint for initial page loads and speeds up your application. + +## Removal of Server Code + +Any server-only [Route Module APIs][route-module] will be removed from the bundles. Consider this route module: + +```tsx +export async function loader() { + return { message: "hello" }; +} + +export async function action() { + console.log(Date.now()); + return { ok: true }; +} + +export async function headers() { + return { "Cache-Control": "max-age=300" }; +} + +export default function Component({ loaderData }) { + return
{loaderData.message}
; +} +``` + +After building for the browser, only the `Component` will still be in the bundle, so you can use server-only code in the other module exports. + +[route-module]: ../../start/framework/route-module diff --git a/docs/explanation/concurrency.md b/docs/explanation/concurrency.md new file mode 100644 index 0000000000..cfa8695d53 --- /dev/null +++ b/docs/explanation/concurrency.md @@ -0,0 +1,135 @@ +--- +title: Network Concurrency Management +--- + +# Network Concurrency Management + +[MODES: framework, data] + +
+
+ +When building web applications, managing network requests can be a daunting task. The challenges of ensuring up-to-date data and handling simultaneous requests often lead to complex logic in the application to deal with interruptions and race conditions. React Router simplifies this process by automating network management while mirroring and expanding upon the intuitive behavior of web browsers. + +To help understand how React Router handles concurrency, it's important to remember that after `form` submissions, React Router will fetch fresh data from the `loader`s. This is called revalidation. + +## Natural Alignment with Browser Behavior + +React Router's handling of network concurrency is heavily inspired by the default behavior of web browsers when processing documents. + +### Link Navigation + +**Browser Behavior**: When you click on a link in a browser and then click on another before the page transition completes, the browser prioritizes the most recent `action`. It cancels the initial request, focusing solely on the latest link clicked. + +**React Router Behavior**: React Router manages client-side navigation the same way. When a link is clicked within a React Router application, it initiates fetch requests for each `loader` tied to the target URL. If another navigation interrupts the initial navigation, React Router cancels the previous fetch requests, ensuring that only the latest requests proceed. + +### Form Submission + +**Browser Behavior**: If you initiate a form submission in a browser and then quickly submit another form again, the browser disregards the first submission, processing only the latest one. + +**React Router Behavior**: React Router mimics this behavior when working with forms. If a form is submitted and another submission occurs before the first completes, React Router cancels the original fetch requests. It then waits for the latest submission to complete before triggering page revalidation again. + +## Concurrent Submissions and Revalidation + +While standard browsers are limited to one request at a time for navigations and form submissions, React Router elevates this behavior. Unlike navigation, with [`useFetcher`][use_fetcher] multiple requests can be in flight simultaneously. + +React Router is designed to handle multiple form submissions to server `action`s and concurrent revalidation requests efficiently. It ensures that as soon as new data is available, the state is updated promptly. However, React Router also safeguards against potential pitfalls by refraining from committing stale data when other `action`s introduce race conditions. + +For instance, if three form submissions are in progress, and one completes, React Router updates the UI with that data immediately without waiting for the other two so that the UI remains responsive and dynamic. As the remaining submissions finalize, React Router continues to update the UI, ensuring that the most recent data is displayed. + +Using this key: + +- `|`: Submission begins +- โœ“: Action complete, data revalidation begins +- โœ…: Revalidated data is committed to the UI +- โŒ: Request cancelled + +We can visualize this scenario in the following diagram: + +```text +submission 1: |----โœ“-----โœ… +submission 2: |-----โœ“-----โœ… +submission 3: |-----โœ“-----โœ… +``` + +However, if a subsequent submission's revalidation completes before an earlier one, React Router discards the earlier data, ensuring that only the most up-to-date information is reflected in the UI: + +```text +submission 1: |----โœ“---------โŒ +submission 2: |-----โœ“-----โœ… +submission 3: |-----โœ“-----โœ… +``` + +Because the revalidation from submission (2) started later and landed earlier than submission (1), the requests from submission (1) are canceled and only the data from submission (2) is committed to the UI. It was requested later, so it's more likely to contain the updated values from both (1) and (2). + +## Potential for Stale Data + +It's unlikely your users will ever experience this, but there are still chances for the user to see stale data in very rare conditions with inconsistent infrastructure. Even though React Router cancels requests for stale data, they will still end up making it to the server. Canceling a request in the browser simply releases browser resources for that request; it can't "catch up" and stop it from getting to the server. In extremely rare conditions, a canceled request may change data after the interrupting `action`'s revalidation lands. Consider this diagram: + +```text + ๐Ÿ‘‡ interruption with new submission +|----โŒ----------------------โœ“ + |-------โœ“-----โœ… + ๐Ÿ‘† + initial request reaches the server + after the interrupting submission + has completed revalidation +``` + +The user is now looking at different data than what is on the server. Note that this problem is both extremely rare and exists with default browser behavior, too. The chance of the initial request reaching the server later than both the submission and revalidation of the second is unexpected on any network and server infrastructure. If this is a concern with your infrastructure, you can send timestamps with your form submissions and write server logic to ignore stale submissions. + +## Example + +In UI components like comboboxes, each keystroke can trigger a network request. Managing such rapid, consecutive requests can be tricky, especially when ensuring that the displayed results match the most recent query. However, with React Router, this challenge is automatically handled, ensuring that users see the correct results without developers having to micromanage the network. + +```tsx filename=app/pages/city-search.tsx +export async function loader({ request }) { + const { searchParams } = new URL(request.url); + const cities = await searchCities(searchParams.get("q")); + return cities; +} + +export function CitySearchCombobox() { + const fetcher = useFetcher(); + + return ( + + + + // submit the form onChange to get the list of cities + fetcher.submit(event.target.form) + } + /> + + {/* render with the loader's data */} + {fetcher.data ? ( + + {fetcher.data.length > 0 ? ( + + {fetcher.data.map((city) => ( + + ))} + + ) : ( + No results found + )} + + ) : null} + + + ); +} +``` + +All the application needs to know is how to query the data and how to render it. React Router handles the network. + +## Conclusion + +React Router offers developers an intuitive, browser-based approach to managing network requests. By mirroring browser behaviors and enhancing them where needed, it simplifies the complexities of concurrency, revalidation, and potential race conditions. Whether you're building a simple webpage or a sophisticated web application, React Router ensures that your user interactions are smooth, reliable, and always up to date. + +[use_fetcher]: ../api/hooks/useFetcher diff --git a/docs/explanation/form-vs-fetcher.md b/docs/explanation/form-vs-fetcher.md new file mode 100644 index 0000000000..691b3d9000 --- /dev/null +++ b/docs/explanation/form-vs-fetcher.md @@ -0,0 +1,292 @@ +--- +title: Form vs. fetcher +--- + +# Form vs. fetcher + +[MODES: framework, data] + +## Overview + +Developing in React Router offers a rich set of tools that can sometimes overlap in functionality, creating a sense of ambiguity for newcomers. The key to effective development in React Router is understanding the nuances and appropriate use cases for each tool. This document seeks to provide clarity on when and why to use specific APIs. + +## APIs in Focus + +- [`
`][form-component] +- [`useFetcher`][use-fetcher] +- [`useNavigation`][use-navigation] + +Understanding the distinctions and intersections of these APIs is vital for efficient and effective React Router development. + +## URL Considerations + +The primary criterion when choosing among these tools is whether you want the URL to change or not: + +- **URL Change Desired**: When navigating or transitioning between pages, or after certain actions like creating or deleting records. This ensures that the user's browser history accurately reflects their journey through your application. + - **Expected Behavior**: In many cases, when users hit the back button, they should be taken to the previous page. Other times the history entry may be replaced but the URL change is important nonetheless. + +- **No URL Change Desired**: For actions that don't significantly change the context or primary content of the current view. This might include updating individual fields or minor data manipulations that don't warrant a new URL or page reload. This also applies to loading data with fetchers for things like popovers, combo boxes, etc. + +### When the URL Should Change + +These actions typically reflect significant changes to the user's context or state: + +- **Creating a New Record**: After creating a new record, it's common to redirect users to a page dedicated to that new record, where they can view or further modify it. + +- **Deleting a Record**: If a user is on a page dedicated to a specific record and decides to delete it, the logical next step is to redirect them to a general page, such as a list of all records. + +For these cases, developers should consider using a combination of [``][form-component] and [`useNavigation`][use-navigation]. These tools can be coordinated to handle form submission, invoke specific actions, retrieve action-related data through component props, and manage navigation respectively. + +### When the URL Shouldn't Change + +These actions are generally more subtle and don't require a context switch for the user: + +- **Updating a Single Field**: Maybe a user wants to change the name of an item in a list or update a specific property of a record. This action is minor and doesn't necessitate a new page or URL. + +- **Deleting a Record from a List**: In a list view, if a user deletes an item, they likely expect to remain on the list view, with that item no longer in the list. + +- **Creating a Record in a List View**: When adding a new item to a list, it often makes sense for the user to remain in that context, seeing their new item added to the list without a full page transition. + +- **Loading Data for a Popover or Combobox**: When loading data for a popover or combobox, the user's context remains unchanged. The data is loaded in the background and displayed in a small, self-contained UI element. + +For such actions, [`useFetcher`][use-fetcher] is the go-to API. It's versatile, combining functionalities of these APIs, and is perfectly suited for tasks where the URL should remain unchanged. + +## API Comparison + +As you can see, the two sets of APIs have a lot of similarities: + +| Navigation/URL API | Fetcher API | +| ----------------------------- | -------------------- | +| `` | `` | +| `actionData` (component prop) | `fetcher.data` | +| `navigation.state` | `fetcher.state` | +| `navigation.formAction` | `fetcher.formAction` | +| `navigation.formData` | `fetcher.formData` | + +## Examples + +### Creating a New Record + +```tsx filename=app/pages/new-recipe.tsx lines=[16,23-24,29] +import { + Form, + redirect, + useNavigation, +} from "react-router"; +import type { Route } from "./+types/new-recipe"; + +export async function action({ + request, +}: Route.ActionArgs) { + const formData = await request.formData(); + const errors = await validateRecipeFormData(formData); + if (errors) { + return { errors }; + } + const recipe = await db.recipes.create(formData); + return redirect(`/recipes/${recipe.id}`); +} + +export function NewRecipe({ + actionData, +}: Route.ComponentProps) { + const { errors } = actionData || {}; + const navigation = useNavigation(); + const isSubmitting = + navigation.formAction === "/recipes/new"; + + return ( + + +