diff --git a/.eslintignore b/.eslintignore index 26b8a0ae29..49a812d5b4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,9 +3,18 @@ node_modules/ pnpm-lock.yaml /docs/api examples/**/dist/ -/integration/helpers/vite-plugin-cloudflare-template/worker-configuration.d.ts +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 diff --git a/.eslintrc b/.eslintrc index 61400497f8..2e0c041bd5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,6 +18,78 @@ "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/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6b8e6fe5b9..4ef4cbaa8e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,32 +8,31 @@ body: value: | Thank you for helping to improve React Router! - ## Option 1: Submit a PR with a failing test + 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 and submit a pull request with a failing bug report test! - - ## Option 2: Continue filling out this form + 3. Follow the instructions to create a failing bug report test + 4. Link to your forked branch with the failing test in this issue - If you'd rather open a GitHub issue, here are other ways to share a reproduction (ordered from most helpful to least): + ### Option 2 - Create a **minimal** reproduction - ๐Ÿฅ‡ Link to a [StackBlitz](https://reactrouter.com/new) environment - - ๐Ÿฅˆ Link to a GitHub repository - - ๐Ÿฅ‰ Description of project including template, config files, `package.json` scripts, etc. + - ๐Ÿฅˆ 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: dropdown - id: mode - attributes: - label: I'm using React Router as a... - description: See https://reactrouter.com/home for explanation - options: - - library - - framework - validations: - required: true - type: textarea id: reproduction attributes: diff --git a/.github/workflows/close-feature-pr.yml b/.github/workflows/close-feature-pr.yml new file mode 100644 index 0000000000..9dac3185c7 --- /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: + 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..ed616c6458 --- /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@v4 + 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 index 929df876e0..8398b6eada 100644 --- a/.github/workflows/deduplicate-lock-file.yml +++ b/.github/workflows/deduplicate-lock-file.yml @@ -5,20 +5,22 @@ on: branches: - dev paths: - - ./pnpm-lock.yaml + - pnpm-lock.yaml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - format: + dedupe: if: github.repository == 'remix-run/react-router' runs-on: ubuntu-latest steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 + with: + token: ${{ secrets.FORMAT_PAT }} - name: ๐Ÿ“ฆ Setup pnpm uses: pnpm/action-setup@v4 @@ -34,13 +36,14 @@ jobs: - name: ๐Ÿ’ช Commit run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" + 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 commit -m "chore: deduplicate \`pnpm-lock.yaml\`" git push - echo "๐Ÿ’ฟ https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)" + 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..6430bcfb06 --- /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@v4 + 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 index cb7b558534..80094bc6a4 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -17,7 +17,7 @@ jobs: steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.FORMAT_PAT }} diff --git a/.github/workflows/integration-full.yml b/.github/workflows/integration-full.yml index fd2f79bfa9..cb1d064ee2 100644 --- a/.github/workflows/integration-full.yml +++ b/.github/workflows/integration-full.yml @@ -40,7 +40,7 @@ jobs: uses: ./.github/workflows/shared-integration.yml with: os: "windows-latest" - node_version: "[20, 22]" + node_version: "[22]" browser: '["msedge"]' integration-macos: diff --git a/.github/workflows/integration-pr-windows-macos.yml b/.github/workflows/integration-pr-windows-macos.yml index a36aef144d..f3d12c7681 100644 --- a/.github/workflows/integration-pr-windows-macos.yml +++ b/.github/workflows/integration-pr-windows-macos.yml @@ -32,7 +32,7 @@ jobs: os: "windows-latest" node_version: "[22]" browser: '["msedge"]' - timeout: 60 + timeout: 120 integration-webkit: name: "๐Ÿ‘€ Integration Test" @@ -42,3 +42,4 @@ jobs: os: "macos-latest" node_version: "[22]" browser: '["webkit"]' + timeout: 40 diff --git a/.github/workflows/release-comments.yml b/.github/workflows/release-comments.yml index 4ba2af6e93..1b37a5771f 100644 --- a/.github/workflows/release-comments.yml +++ b/.github/workflows/release-comments.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: ๐Ÿ“ Comment on related issues and pull requests - uses: remix-run/release-comment-action@v0.4.1 + uses: remix-run/release-comment-action@v0.4.2 with: DIRECTORY_TO_CHECK: "./packages" PACKAGE_NAME: "react-router" diff --git a/.github/workflows/release-experimental.yml b/.github/workflows/release-experimental.yml index 0a9437dc89..4ebdd0636a 100644 --- a/.github/workflows/release-experimental.yml +++ b/.github/workflows/release-experimental.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.event.inputs.branch }} # checkout using a custom token so that we can push later on diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 62d5db855b..64419f5a2f 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -29,7 +29,7 @@ jobs: NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }} steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: dev # checkout using a custom token so that we can push later on diff --git a/.github/workflows/release-stage-2-alpha.yml b/.github/workflows/release-stage-2-alpha.yml new file mode 100644 index 0000000000..85e41296d4 --- /dev/null +++ b/.github/workflows/release-stage-2-alpha.yml @@ -0,0 +1,86 @@ +name: ๐Ÿงช Check Alpha Release + +on: + pull_request: + types: [labeled] + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +env: + CI: true + +jobs: + alpha-release: + name: ๐Ÿงช Check Alpha Release + if: github.repository == 'remix-run/react-router' && github.event.label.name == 'alpha-release' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ“„ Log Info + run: | + echo "Label: ${{ github.event.label.name }}" + echo "Branch: ${{ github.event.pull_request.head.ref }}" + echo "SHA: ${{ github.event.pull_request.head.sha }}" + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Log Git Info + run: | + git log -n 1 + git status + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: โคด๏ธ Update version + id: 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} + echo "version=${NEXT_VERSION}" >> "$GITHUB_OUTPUT" + + - name: ๐Ÿ— Build + run: pnpm build + + - name: ๐Ÿ” Setup npm auth + run: | + echo "registry=https://registry.npmjs.org" >> ~/.npmrc + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc + + - name: ๐Ÿš€ Publish + run: pnpm run publish + + - name: ๐Ÿ’ฌ Comment + env: + GH_TOKEN: ${{ github.token }} + run: | + LATEST_RELEASE_SHA=$(gh release list --limit 1 --json tagName --jq ".[0].tagName") + BASE_SHA=$(echo ${{ github.event.pull_request.base.sha }} | cut -c1-7) + COMMAND="git log --pretty=oneline ${LATEST_RELEASE_SHA}..${BASE_SHA}" + echo -e \ + "[Alpha release](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#stage-2--alpha) \ + created: \`${{ steps.version.outputs.version }}\`\n\n \ + โš ๏ธ **Note:** This release was created from the \`HEAD\` of this branch so it \ + may contain commits that have landed in \`dev\` but have not been released yet \ + depending on when this branch was created. You can run the following command \ + to see the commits that may not have been released yet:\n\n \ + \`\`\`bash\n \ + ${COMMAND}\n \ + \`\`\`" \ + | gh pr comment ${{ github.event.pull_request.number }} --body-file - + gh pr edit ${{ github.event.pull_request.number }} --remove-label ${{ github.event.label.name }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c65a6d930..d4284459ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,9 +21,13 @@ jobs: 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@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -51,7 +55,9 @@ jobs: # publish to npm. - name: ๐Ÿš€ PR / Publish id: changesets - uses: changesets/action@v1 + # 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" @@ -60,6 +66,7 @@ jobs: createGithubReleases: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_CONFIG_PROVENANCE: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} find_package_version: @@ -71,7 +78,7 @@ jobs: package_version: ${{ steps.find_package_version.outputs.package_version }} steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: ๐Ÿ“ฆ Setup pnpm uses: pnpm/action-setup@v4 @@ -94,4 +101,7 @@ jobs: name: ๐Ÿ“ Comment on related issues and pull requests if: github.repository == 'remix-run/react-router' && github.ref_name != 'release-v6' && needs.find_package_version.outputs.package_version != '' needs: [release, find_package_version] + permissions: + issues: write # enable commenting on released issues + pull-requests: write # enable commenting on released pull requests uses: ./.github/workflows/release-comments.yml diff --git a/.github/workflows/shared-build.yml b/.github/workflows/shared-build.yml index 6de9e0c61f..49c9c41ca2 100644 --- a/.github/workflows/shared-build.yml +++ b/.github/workflows/shared-build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: ๐Ÿ“ฆ Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/shared-integration.yml b/.github/workflows/shared-integration.yml index e7adb0dd47..62da5c5a35 100644 --- a/.github/workflows/shared-integration.yml +++ b/.github/workflows/shared-integration.yml @@ -21,7 +21,7 @@ on: timeout: required: false type: number - default: 30 + default: 40 env: CI: true @@ -38,7 +38,7 @@ jobs: runs-on: ${{ inputs.os }} steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: ๐Ÿ“ฆ Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/support.yml b/.github/workflows/support.yml index 8a4cf0c8c2..be506d9b36 100644 --- a/.github/workflows/support.yml +++ b/.github/workflows/support.yml @@ -11,7 +11,7 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/support-requests@v3 + - uses: dessant/support-requests@v4 with: issue-comment: > :wave: @{issue-author}, we use the issue tracker exclusively for bug reports diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af709fdaaa..12588257c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: steps: - name: โฌ‡๏ธ Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: ๐Ÿ“ฆ Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.gitignore b/.gitignore index 6e4cc73bf4..53e9a5baf1 100644 --- a/.gitignore +++ b/.gitignore @@ -27,9 +27,11 @@ node_modules/ .wireit .eslintcache +.parcel-cache .tmp tsup.config.bundled_*.mjs build.utils.d.ts +worker-configuration.d.ts /.env /NOTES.md diff --git a/.nvmrc b/.nvmrc index 2edeafb09d..8fdd954df9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 \ No newline at end of file +22 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e020d67eb..92acf7ff40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,75 +13,106 @@ We manage release notes in this file instead of the paginated Github Releases Pa Table of Contents - [React Router Releases](#react-router-releases) - - [v7.6.0](#v760) - - [What's Changed](#whats-changed) - - [`routeDiscovery` Config Option](#routediscovery-config-option) - - [Automatic Types for Future Flags](#automatic-types-for-future-flags) - - [Minor Changes](#minor-changes) + - [v7.8.2](#v782) - [Patch Changes](#patch-changes) - [Unstable Changes](#unstable-changes) - - [Changes by Package](#changes-by-package) - - [v7.5.3](#v753) + - [v7.8.1](#v781) - [Patch Changes](#patch-changes-1) - - [v7.5.2](#v752) - - [Security Notice](#security-notice) + - [Unstable Changes](#unstable-changes-1) + - [v7.8.0](#v780) + - [What's Changed](#whats-changed) + - [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) - [Patch Changes](#patch-changes-2) - - [v7.5.1](#v751) + - [Unstable Changes](#unstable-changes-2) + - [Changes by Package](#changes-by-package) + - [v7.7.1](#v771) - [Patch Changes](#patch-changes-3) - - [Unstable Changes](#unstable-changes-1) - - [v7.5.0](#v750) + - [Unstable Changes](#unstable-changes-3) + - [v7.7.0](#v770) - [What's Changed](#whats-changed-1) - - [`route.lazy` Object API](#routelazy-object-api) + - [Unstable RSC APIs](#unstable-rsc-apis) - [Minor Changes](#minor-changes-1) - [Patch Changes](#patch-changes-4) - - [Unstable Changes](#unstable-changes-2) + - [Unstable Changes](#unstable-changes-4) - [Changes by Package](#changes-by-package-1) - - [v7.4.1](#v741) - - [Security Notice](#security-notice-1) + - [v7.6.3](#v763) - [Patch Changes](#patch-changes-5) - - [Unstable Changes](#unstable-changes-3) - - [v7.4.0](#v740) - - [Minor Changes](#minor-changes-2) + - [v7.6.2](#v762) - [Patch Changes](#patch-changes-6) - - [Unstable Changes](#unstable-changes-4) - - [Changes by Package](#changes-by-package-2) - - [v7.3.0](#v730) - - [Minor Changes](#minor-changes-3) + - [v7.6.1](#v761) - [Patch Changes](#patch-changes-7) - [Unstable Changes](#unstable-changes-5) + - [v7.6.0](#v760) + - [What's Changed](#whats-changed-2) + - [`routeDiscovery` Config Option](#routediscovery-config-option) + - [Automatic Types for Future Flags](#automatic-types-for-future-flags) + - [Minor Changes](#minor-changes-2) + - [Patch Changes](#patch-changes-8) + - [Unstable Changes](#unstable-changes-6) + - [Changes by Package](#changes-by-package-2) + - [v7.5.3](#v753) + - [Patch Changes](#patch-changes-9) + - [v7.5.2](#v752) + - [Security Notice](#security-notice) + - [Patch Changes](#patch-changes-10) + - [v7.5.1](#v751) + - [Patch Changes](#patch-changes-11) + - [Unstable Changes](#unstable-changes-7) + - [v7.5.0](#v750) + - [What's Changed](#whats-changed-3) + - [`route.lazy` Object API](#routelazy-object-api) + - [Minor Changes](#minor-changes-3) + - [Patch Changes](#patch-changes-12) + - [Unstable Changes](#unstable-changes-8) + - [Changes by Package](#changes-by-package-3) + - [v7.4.1](#v741) + - [Security Notice](#security-notice-1) + - [Patch Changes](#patch-changes-13) + - [Unstable Changes](#unstable-changes-9) + - [v7.4.0](#v740) + - [Minor Changes](#minor-changes-4) + - [Patch Changes](#patch-changes-14) + - [Unstable Changes](#unstable-changes-10) + - [Changes by Package](#changes-by-package-4) + - [v7.3.0](#v730) + - [Minor Changes](#minor-changes-5) + - [Patch Changes](#patch-changes-15) + - [Unstable Changes](#unstable-changes-11) - [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-3) + - [Changes by Package](#changes-by-package-5) - [v7.2.0](#v720) - - [What's Changed](#whats-changed-2) + - [What's Changed](#whats-changed-4) - [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-4) - - [Patch Changes](#patch-changes-8) - - [Unstable Changes](#unstable-changes-6) + - [Minor Changes](#minor-changes-6) + - [Patch Changes](#patch-changes-16) + - [Unstable Changes](#unstable-changes-12) - [Split Route Modules (unstable)](#split-route-modules-unstable) - - [Changes by Package](#changes-by-package-4) + - [Changes by Package](#changes-by-package-6) - [v7.1.5](#v715) - - [Patch Changes](#patch-changes-9) + - [Patch Changes](#patch-changes-17) - [v7.1.4](#v714) - - [Patch Changes](#patch-changes-10) + - [Patch Changes](#patch-changes-18) - [v7.1.3](#v713) - - [Patch Changes](#patch-changes-11) + - [Patch Changes](#patch-changes-19) - [v7.1.2](#v712) - - [Patch Changes](#patch-changes-12) + - [Patch Changes](#patch-changes-20) - [v7.1.1](#v711) - - [Patch Changes](#patch-changes-13) + - [Patch Changes](#patch-changes-21) - [v7.1.0](#v710) - - [Minor Changes](#minor-changes-5) - - [Patch Changes](#patch-changes-14) - - [Changes by Package](#changes-by-package-5) + - [Minor Changes](#minor-changes-7) + - [Patch Changes](#patch-changes-22) + - [Changes by Package](#changes-by-package-7) - [v7.0.2](#v702) - - [Patch Changes](#patch-changes-15) + - [Patch Changes](#patch-changes-23) - [v7.0.1](#v701) - - [Patch Changes](#patch-changes-16) + - [Patch Changes](#patch-changes-24) - [v7.0.0](#v700) - [Breaking Changes](#breaking-changes) - [Package Restructuring](#package-restructuring) @@ -93,204 +124,206 @@ We manage release notes in this file instead of the paginated Github Releases Pa - [Exposed Router Promises](#exposed-router-promises) - [Other Notable Changes](#other-notable-changes) - [`routes.ts`](#routests) - - [Typesafety improvements](#typesafety-improvements) + - [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-6) - - [Patch Changes](#patch-changes-17) - - [Changes by Package](#changes-by-package-6) + - [Minor Changes](#minor-changes-8) + - [Patch Changes](#patch-changes-25) + - [Changes by Package](#changes-by-package-8) - [React Router v6 Releases](#react-router-v6-releases) + - [v6.30.1](#v6301) + - [Patch Changes](#patch-changes-26) - [v6.30.0](#v6300) - - [Minor Changes](#minor-changes-7) - - [Patch Changes](#patch-changes-18) + - [Minor Changes](#minor-changes-9) + - [Patch Changes](#patch-changes-27) - [v6.29.0](#v6290) - - [Minor Changes](#minor-changes-8) - - [Patch Changes](#patch-changes-19) + - [Minor Changes](#minor-changes-10) + - [Patch Changes](#patch-changes-28) - [v6.28.2](#v6282) - - [Patch Changes](#patch-changes-20) + - [Patch Changes](#patch-changes-29) - [v6.28.1](#v6281) - - [Patch Changes](#patch-changes-21) + - [Patch Changes](#patch-changes-30) - [v6.28.0](#v6280) - - [What's Changed](#whats-changed-3) - - [Minor Changes](#minor-changes-9) - - [Patch Changes](#patch-changes-22) + - [What's Changed](#whats-changed-5) + - [Minor Changes](#minor-changes-11) + - [Patch Changes](#patch-changes-31) - [v6.27.0](#v6270) - - [What's Changed](#whats-changed-4) + - [What's Changed](#whats-changed-6) - [Stabilized APIs](#stabilized-apis) - - [Minor Changes](#minor-changes-10) - - [Patch Changes](#patch-changes-23) + - [Minor Changes](#minor-changes-12) + - [Patch Changes](#patch-changes-32) - [v6.26.2](#v6262) - - [Patch Changes](#patch-changes-24) + - [Patch Changes](#patch-changes-33) - [v6.26.1](#v6261) - - [Patch Changes](#patch-changes-25) + - [Patch Changes](#patch-changes-34) - [v6.26.0](#v6260) - - [Minor Changes](#minor-changes-11) - - [Patch Changes](#patch-changes-26) + - [Minor Changes](#minor-changes-13) + - [Patch Changes](#patch-changes-35) - [v6.25.1](#v6251) - - [Patch Changes](#patch-changes-27) + - [Patch Changes](#patch-changes-36) - [v6.25.0](#v6250) - - [What's Changed](#whats-changed-5) + - [What's Changed](#whats-changed-7) - [Stabilized `v7_skipActionErrorRevalidation`](#stabilized-v7_skipactionerrorrevalidation) - - [Minor Changes](#minor-changes-12) - - [Patch Changes](#patch-changes-28) + - [Minor Changes](#minor-changes-14) + - [Patch Changes](#patch-changes-37) - [v6.24.1](#v6241) - - [Patch Changes](#patch-changes-29) + - [Patch Changes](#patch-changes-38) - [v6.24.0](#v6240) - - [What's Changed](#whats-changed-6) + - [What's Changed](#whats-changed-8) - [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war) - - [Minor Changes](#minor-changes-13) - - [Patch Changes](#patch-changes-30) + - [Minor Changes](#minor-changes-15) + - [Patch Changes](#patch-changes-39) - [v6.23.1](#v6231) - - [Patch Changes](#patch-changes-31) + - [Patch Changes](#patch-changes-40) - [v6.23.0](#v6230) - - [What's Changed](#whats-changed-7) + - [What's Changed](#whats-changed-9) - [Data Strategy (unstable)](#data-strategy-unstable) - [Skip Action Error Revalidation (unstable)](#skip-action-error-revalidation-unstable) - - [Minor Changes](#minor-changes-14) + - [Minor Changes](#minor-changes-16) - [v6.22.3](#v6223) - - [Patch Changes](#patch-changes-32) + - [Patch Changes](#patch-changes-41) - [v6.22.2](#v6222) - - [Patch Changes](#patch-changes-33) + - [Patch Changes](#patch-changes-42) - [v6.22.1](#v6221) - - [Patch Changes](#patch-changes-34) + - [Patch Changes](#patch-changes-43) - [v6.22.0](#v6220) - - [What's Changed](#whats-changed-8) + - [What's Changed](#whats-changed-10) - [Core Web Vitals Technology Report Flag](#core-web-vitals-technology-report-flag) - - [Minor Changes](#minor-changes-15) - - [Patch Changes](#patch-changes-35) + - [Minor Changes](#minor-changes-17) + - [Patch Changes](#patch-changes-44) - [v6.21.3](#v6213) - - [Patch Changes](#patch-changes-36) + - [Patch Changes](#patch-changes-45) - [v6.21.2](#v6212) - - [Patch Changes](#patch-changes-37) + - [Patch Changes](#patch-changes-46) - [v6.21.1](#v6211) - - [Patch Changes](#patch-changes-38) + - [Patch Changes](#patch-changes-47) - [v6.21.0](#v6210) - - [What's Changed](#whats-changed-9) + - [What's Changed](#whats-changed-11) - [`future.v7_relativeSplatPath`](#futurev7_relativesplatpath) - [Partial Hydration](#partial-hydration) - - [Minor Changes](#minor-changes-16) - - [Patch Changes](#patch-changes-39) + - [Minor Changes](#minor-changes-18) + - [Patch Changes](#patch-changes-48) - [v6.20.1](#v6201) - - [Patch Changes](#patch-changes-40) + - [Patch Changes](#patch-changes-49) - [v6.20.0](#v6200) - - [Minor Changes](#minor-changes-17) - - [Patch Changes](#patch-changes-41) + - [Minor Changes](#minor-changes-19) + - [Patch Changes](#patch-changes-50) - [v6.19.0](#v6190) - - [What's Changed](#whats-changed-10) + - [What's Changed](#whats-changed-12) - [`unstable_flushSync` API](#unstable_flushsync-api) - - [Minor Changes](#minor-changes-18) - - [Patch Changes](#patch-changes-42) + - [Minor Changes](#minor-changes-20) + - [Patch Changes](#patch-changes-51) - [v6.18.0](#v6180) - - [What's Changed](#whats-changed-11) + - [What's Changed](#whats-changed-13) - [New Fetcher APIs](#new-fetcher-apis) - [Persistence Future Flag (`future.v7_fetcherPersist`)](#persistence-future-flag-futurev7_fetcherpersist) - - [Minor Changes](#minor-changes-19) - - [Patch Changes](#patch-changes-43) + - [Minor Changes](#minor-changes-21) + - [Patch Changes](#patch-changes-52) - [v6.17.0](#v6170) - - [What's Changed](#whats-changed-12) + - [What's Changed](#whats-changed-14) - [View Transitions ๐Ÿš€](#view-transitions-) - - [Minor Changes](#minor-changes-20) - - [Patch Changes](#patch-changes-44) + - [Minor Changes](#minor-changes-22) + - [Patch Changes](#patch-changes-53) - [v6.16.0](#v6160) - - [Minor Changes](#minor-changes-21) - - [Patch Changes](#patch-changes-45) + - [Minor Changes](#minor-changes-23) + - [Patch Changes](#patch-changes-54) - [v6.15.0](#v6150) - - [Minor Changes](#minor-changes-22) - - [Patch Changes](#patch-changes-46) + - [Minor Changes](#minor-changes-24) + - [Patch Changes](#patch-changes-55) - [v6.14.2](#v6142) - - [Patch Changes](#patch-changes-47) + - [Patch Changes](#patch-changes-56) - [v6.14.1](#v6141) - - [Patch Changes](#patch-changes-48) + - [Patch Changes](#patch-changes-57) - [v6.14.0](#v6140) - - [What's Changed](#whats-changed-13) + - [What's Changed](#whats-changed-15) - [JSON/Text Submissions](#jsontext-submissions) - - [Minor Changes](#minor-changes-23) - - [Patch Changes](#patch-changes-49) + - [Minor Changes](#minor-changes-25) + - [Patch Changes](#patch-changes-58) - [v6.13.0](#v6130) - - [What's Changed](#whats-changed-14) + - [What's Changed](#whats-changed-16) - [`future.v7_startTransition`](#futurev7_starttransition) - - [Minor Changes](#minor-changes-24) - - [Patch Changes](#patch-changes-50) + - [Minor Changes](#minor-changes-26) + - [Patch Changes](#patch-changes-59) - [v6.12.1](#v6121) - - [Patch Changes](#patch-changes-51) + - [Patch Changes](#patch-changes-60) - [v6.12.0](#v6120) - - [What's Changed](#whats-changed-15) + - [What's Changed](#whats-changed-17) - [`React.startTransition` support](#reactstarttransition-support) - - [Minor Changes](#minor-changes-25) - - [Patch Changes](#patch-changes-52) + - [Minor Changes](#minor-changes-27) + - [Patch Changes](#patch-changes-61) - [v6.11.2](#v6112) - - [Patch Changes](#patch-changes-53) + - [Patch Changes](#patch-changes-62) - [v6.11.1](#v6111) - - [Patch Changes](#patch-changes-54) + - [Patch Changes](#patch-changes-63) - [v6.11.0](#v6110) - - [Minor Changes](#minor-changes-26) - - [Patch Changes](#patch-changes-55) + - [Minor Changes](#minor-changes-28) + - [Patch Changes](#patch-changes-64) - [v6.10.0](#v6100) - - [What's Changed](#whats-changed-16) - - [Minor Changes](#minor-changes-27) + - [What's Changed](#whats-changed-18) + - [Minor Changes](#minor-changes-29) - [`future.v7_normalizeFormMethod`](#futurev7_normalizeformmethod) - - [Patch Changes](#patch-changes-56) + - [Patch Changes](#patch-changes-65) - [v6.9.0](#v690) - - [What's Changed](#whats-changed-17) + - [What's Changed](#whats-changed-19) - [`Component`/`ErrorBoundary` route properties](#componenterrorboundary-route-properties) - [Introducing Lazy Route Modules](#introducing-lazy-route-modules) - - [Minor Changes](#minor-changes-28) - - [Patch Changes](#patch-changes-57) + - [Minor Changes](#minor-changes-30) + - [Patch Changes](#patch-changes-66) - [v6.8.2](#v682) - - [Patch Changes](#patch-changes-58) + - [Patch Changes](#patch-changes-67) - [v6.8.1](#v681) - - [Patch Changes](#patch-changes-59) + - [Patch Changes](#patch-changes-68) - [v6.8.0](#v680) - - [Minor Changes](#minor-changes-29) - - [Patch Changes](#patch-changes-60) + - [Minor Changes](#minor-changes-31) + - [Patch Changes](#patch-changes-69) - [v6.7.0](#v670) - - [Minor Changes](#minor-changes-30) - - [Patch Changes](#patch-changes-61) + - [Minor Changes](#minor-changes-32) + - [Patch Changes](#patch-changes-70) - [v6.6.2](#v662) - - [Patch Changes](#patch-changes-62) + - [Patch Changes](#patch-changes-71) - [v6.6.1](#v661) - - [Patch Changes](#patch-changes-63) + - [Patch Changes](#patch-changes-72) - [v6.6.0](#v660) - - [What's Changed](#whats-changed-18) - - [Minor Changes](#minor-changes-31) - - [Patch Changes](#patch-changes-64) + - [What's Changed](#whats-changed-20) + - [Minor Changes](#minor-changes-33) + - [Patch Changes](#patch-changes-73) - [v6.5.0](#v650) - - [What's Changed](#whats-changed-19) - - [Minor Changes](#minor-changes-32) - - [Patch Changes](#patch-changes-65) + - [What's Changed](#whats-changed-21) + - [Minor Changes](#minor-changes-34) + - [Patch Changes](#patch-changes-74) - [v6.4.5](#v645) - - [Patch Changes](#patch-changes-66) + - [Patch Changes](#patch-changes-75) - [v6.4.4](#v644) - - [Patch Changes](#patch-changes-67) + - [Patch Changes](#patch-changes-76) - [v6.4.3](#v643) - - [Patch Changes](#patch-changes-68) + - [Patch Changes](#patch-changes-77) - [v6.4.2](#v642) - - [Patch Changes](#patch-changes-69) + - [Patch Changes](#patch-changes-78) - [v6.4.1](#v641) - - [Patch Changes](#patch-changes-70) + - [Patch Changes](#patch-changes-79) - [v6.4.0](#v640) - - [What's Changed](#whats-changed-20) + - [What's Changed](#whats-changed-22) - [Remix Data APIs](#remix-data-apis) - - [Patch Changes](#patch-changes-71) + - [Patch Changes](#patch-changes-80) - [v6.3.0](#v630) - - [Minor Changes](#minor-changes-33) + - [Minor Changes](#minor-changes-35) - [v6.2.2](#v622) - - [Patch Changes](#patch-changes-72) + - [Patch Changes](#patch-changes-81) - [v6.2.1](#v621) - - [Patch Changes](#patch-changes-73) + - [Patch Changes](#patch-changes-82) - [v6.2.0](#v620) - - [Minor Changes](#minor-changes-34) - - [Patch Changes](#patch-changes-74) + - [Minor Changes](#minor-changes-36) + - [Patch Changes](#patch-changes-83) - [v6.1.1](#v611) - - [Patch Changes](#patch-changes-75) + - [Patch Changes](#patch-changes-84) - [v6.1.0](#v610) - - [Minor Changes](#minor-changes-35) - - [Patch Changes](#patch-changes-76) + - [Minor Changes](#minor-changes-37) + - [Patch Changes](#patch-changes-85) - [v6.0.2](#v602) - - [Patch Changes](#patch-changes-77) + - [Patch Changes](#patch-changes-86) - [v6.0.1](#v601) - - [Patch Changes](#patch-changes-78) + - [Patch Changes](#patch-changes-87) - [v6.0.0](#v600) @@ -315,7 +348,6 @@ Date: YYYY-MM-DD โš ๏ธ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ - ### Changes by Package - [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.X.Y/packages/create-react-router/CHANGELOG.md#7XY) @@ -332,6 +364,501 @@ Date: YYYY-MM-DD **Full Changelog**: [`v7.X.Y...v7.X.Y`](https://github.com/remix-run/react-router/compare/react-router@7.X.Y...react-router@7.X.Y) --> +## 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 @@ -404,7 +931,6 @@ Behind the scenes, React Router will generate the corresponding `declare module` - `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 @@ -523,7 +1049,6 @@ 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 @@ -828,7 +1353,7 @@ Here's a simple example of a client-side logging middleware that can be placed o ```tsx const clientLogger: Route.unstable_ClientMiddlewareFunction = async ( { request }, - next + next, ) => { let start = performance.now(); @@ -847,7 +1372,7 @@ For a server-side middleware, the `next` function will return the HTTP `Response ```tsx const serverLogger: Route.unstable_MiddlewareFunction = async ( { request, params, context }, - next + next, ) => { let start = performance.now(); @@ -868,7 +1393,7 @@ You can throw a `redirect` from a middleware to short circuit any remaining proc import { sessionContext } from "../context"; const serverAuth: Route.unstable_MiddlewareFunction = ( { request, params, context }, - next + next, ) => { let session = context.get(sessionContext); let user = session.get("user"); @@ -1121,7 +1646,7 @@ import { MassiveComponent } from "~/components"; export async function clientLoader() { return await fetch("https://example.com/api").then((response) => - response.json() + response.json(), ); } @@ -1156,7 +1681,7 @@ To achieve this optimization, React Router will split the route module into mult ```tsx filename=routes/example.tsx?route-chunk=clientLoader export async function clientLoader() { return await fetch("https://example.com/api").then((response) => - response.json() + response.json(), ); } ``` @@ -1202,7 +1727,7 @@ const shared = () => console.log("hello"); export async function clientLoader() { shared(); return await fetch("https://example.com/api").then((response) => - response.json() + response.json(), ); } @@ -1229,7 +1754,7 @@ import { shared } from "./shared"; export async function clientLoader() { shared(); return await fetch("https://example.com/api").then((response) => - response.json() + response.json(), ); } @@ -1247,7 +1772,7 @@ import { shared } from "./shared"; export async function clientLoader() { shared(); return await fetch("https://example.com/api").then((response) => - response.json() + response.json(), ); } ``` @@ -1641,7 +2166,7 @@ Also note that, if you were using Remix's `routes` option to define config-based +]; ``` -#### Typesafety improvements +#### 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/`. @@ -1846,6 +2371,17 @@ async function fakeGetSlugsFromCms() { # 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 @@ -2102,7 +2638,7 @@ const router = createBrowserRouter( patch("root", [route]); } }, - } + }, ); ``` @@ -2882,7 +3418,7 @@ let routes = createRoutesFromElements( } /> import("./a")} /> import("./b")} /> - + , ); ``` diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000000..cfaf61031f --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,150 @@ +# React Router Open Governance Model + +- [Overview](#overview) +- [Design Goals](#design-goals) +- [Steering Committee](#steering-committee) +- [Bug/Issue Process](#bugissue-process) +- [New Feature Process](#new-feature-process) +- [New Feature Stages](#new-feature-stages) + - [Stage 0 โ€” Proposal](#stage-0--proposal) + - [Stage 1 โ€” Consideration](#stage-1--consideration) + - [Stage 2 โ€” Alpha](#stage-2--alpha) + - [Stage 3 โ€” Beta](#stage-3--beta) + - [Stage 4 โ€” Stabilization](#stage-4--stabilization) + - [Stage 5 โ€” Stable](#stage-5--stable) + +## Overview + +React Router has been around since 2014 largely under the development and oversight of [Michael Jackson](https://x.com/mjackson) and [Ryan Florence](https://x.com/ryanflorence). After the launch of [Remix](https://remix.run/) in 2021, the subsequent creation of the Remix team, and the merging of Remix v2 into React Router v7[^1][^2], the project shifted from a [Founder-Leader](https://www.redhat.com/en/blog/understanding-open-source-governance-models) model to a "Steering Committee" (SC) model that operates on a Request for Comments (RFC) process. + +[^1]: https://remix.run/blog/merging-remix-and-react-router + +[^2]: https://remix.run/blog/incremental-path-to-react-19 + +This document will outline the process in which React Router will continue to evolve and how new features will make their way into the codebase. This is an evergreen document and will be updated as needed to reflect future changes in the process. + +## Design Goals + +The following design goals should be considered when considering RFCs for acceptance: + +- **Less is More**. React Router has gained a _lot_ of functionality in the past years, but with that comes a bunch of new API surface. It's time to hone in on the core functionality and aim to reduce API surface _without sacrificing capabilities_. This may come in multiple forms, such as condensing a few existing APIs into a singular API, or deprecating current APIs in favor of a new React API. +- **Routing and Data Focused.** Focus on core router-integrated/router-centric APIs and avoid adding first-class APIs that can be implemented in user-land +- **Simple Migration Paths.** Major version upgrades don't have to stink. Breaking changes should be implemented behind future flags. Deprecations should be properly marked ahead of time in code and in documentation. Console warnings should be added prior to major releases to nudge developers towards the changes they can begin to make to prep for the upgrade. +- **Lowest Common Mode.** Features are added at the lowest mode possible (`declarative -> data -> framework`) and then leveraged by the higher-level modes. This ensures that the largest number of React Router applications can leverage them. +- **Regular Release Cadence**. Aim for major SemVer releases on a ~yearly basis so application developers can prepare in advance. + +## Steering Committee + +The Steering Committee will be in charge of accepting RFC's for consideration, approving PRs to land features in an "unstable" state, and approving stabilization PRs to land PRs that stabilize features into React Router. + +The SC will initially consist of the Remix team developers: + +- Matt Brophy ([`@brophdawg11`](https://github.com/brophdawg11)) +- Pedro Cattori ([`@pcattori`](https://github.com/pcattori)) +- Mark Dalgleish ([`@markdalgleish`](https://github.com/markdalgleish)) +- Jacob Ebey ([`@jacob-ebey`](https://github.com/jacob-ebey)) +- Brooks Lybrand ([`@brookslybrand`](https://github.com/brookslybrand)) + +In the future, we may add a limited number of heavily involved community members to the SC as well. + +To reduce friction, the SC will primarily operate asynchronously via GitHub, but private and/or public meetings may be scheduled as needed. + +## Bug/Issue Process + +Due to the large number of React Router applications out there, we have to be a bit strict on the process for filing issues to avoid an overload in GitHub. + +- **All** bugs must have a **minimal** and **runnable** reproduction [^3] + - _Minimal_ means that it is not just pointing to a deployed site or a branch in your existing application + - _Runnable_ means that it is a working application where we can see the issue, not just a few snippets of code that need to be manually reassembled into a running application + - The preferred methods for reproductions are: + - **Framework Mode**: [StackBlitz](https://reactrouter.com/new) or a GitHub fork with a failing integration test based on [`bug-report-test.ts`](integration/bug-report-test.ts) + - **Data/Declarative Modes**: [CodeSandbox (TS)](https://codesandbox.io/templates/react-vite-ts) or [CodeSandbox (JS)](https://codesandbox.io/templates/react-vite) + - If StackBlitz/CodeSandbox is not an option, a GitHub repo based on a fresh `npx create-react-router` app is acceptable + - Only in extraordinary circumstances will code snippets or maximal reproductions be accepted +- Issue Review + - Issues not meeting the above criteria will be closed and pointed to this document + - Non-issues (feature requests, usage questions) will also be closed with a link to this document + - The SC will triage issues regularly +- Fixing Issues + - The SC will mark good community issues with an `Accepting PRs` label + - These issues will generally be ones that are likely to have a small surface area fix + - However, anyone can work on any issue, but there's no guarantee the PR will be accepted if the surface area is too large for expedient review by a core team member + +[^3]: https://antfu.me/posts/why-reproductions-are-required + +## New Feature Process + +The process for new features being added to React Router will follow a series of stages loosely based on the [TC39 Process](https://tc39.es/process-document/). It is important to note that entrance into any given stage does not imply that an RFC will proceed any further. The stages will act as a funnel with fewer RFCs making it into later stages such that only the strongest RFCs make it into a React Router release in a stable fashion. + +> [!NOTE] +> Most new community-driven features for React Router will go through all stages. Some features, if trivial or obvious enough, may skip stages and be implemented directly as a stable feature. + +This table gives a high-level overview of the stages, but please see the individual stage sections below for more detailed information on the stages and the process for moving an FC through them. Once a feature reaches Stage 2, it will be added to the [Roadmap](https://github.com/orgs/remix-run/projects/5) where it can be tracked as it moves through the stages. + +| Stage | Name | Entrance Criteria | Purpose | +| ----- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | Proposal | Proposal discussion opened on GitHub | We start with a GitHub Proposal to provide the lowest barrier to RFC submission. Anyone can submit an RFC and community members can review, comment, up-vote without any initial involvement of the SC. | +| 1 | Consideration | Proposal acceptance from 2 SC members | The consideration phase is the first "funnel" for incoming RFCs where the SC can officially express interest in the more popular RFCs. We only require 2 SC members to express interest to move an RFC into the **Consideration** phase to allow for low-friction experimentation of features in the **Alpha** stage. | +| 2 | Alpha | Pull request (PR) opened to implement the feature in an "unstable" state | The **Alpha** stage is the next funnel for RFCs. Once interest has been expressed by the SC in the **Consideration** phase we open the RFC up for a sample PR implementation and a mechanism for community members to alpha test the feature without requiring that anything be shipped in a React Router SemVer release. This stage allows evaluation of the RFC in running applications and consideration of what a practical implementation of the RFC looks like in the code. | +| 3 | Beta | PR approval from 2 SC members indicating their acceptance of the PR for an unstable API | A RFC enters the **Beta** stage once enough members of the SC feel comfortable not only with the code for the beta feature, but have also seen positive feedback from alpha testers that the feature is working as expected. Once an **Alpha** stage PR has enough SC approvals, it will be merged and be included in the next React Router release. | +| 4 | Stabilization | At least 1 month in the Beta stage and PR opened to stabilize the APIs. This PR should also include documentation for the new feature. | The **Stabilization** phase exists to ensure that unstable features are available for enough time for applications to update their React Router version and opt-into beta testing. We don't want to rush features through beta testing so that we have maximal feedback prior to stabilizing a feature. | +| 5 | Stable | PR approval from at least 50% of the SC members indicating their acceptance of the PR for a stable API | A RFC is completed and enters the **Stable** stage once enough members of the SC feel comfortable not only with the code for the stable feature, but have also seen positive feedback from beta testers that the feature is working as expected. Once an **Beta** stage PR has enough SC approvals and has spent the required amount of time in the **Beta** stage, it can be merged and included in the next React Router release. | + +## New Feature Stages + +### Stage 0 โ€” Proposal + +- All new features begin at **Stage 0 โ€” Proposal** when a Request For Comments (RFC) is written up in a GitHub Proposal Discussion +- Anyone can write an RFC, including core team members and community members +- The RFC should outline the use-case for the new feature, why current APIs are insufficient for the use-case, and provide potential API surfaces for the feature +- The proposal should be clear, concise, and provide enough context for the Steering Committee (SC) and community to evaluate its merit +- Community upvotes on the proposal are used as a signal of interest and demand for the SC โ€” higher upvoted issues are more likely to be considered by the SC members +- At this stage, community members may feel free to work on sample implementations in a fork of the repo and provide links in the RFC, but a pull request **should not** be opened until it reaches Stage 1 + +### Stage 1 โ€” Consideration + +- A proposal enters **Stage 1 โ€” Consideration** when 2 SC members indicate interest/support for the idea as a valuable addition to React Router +- Upon entering this stage, a GitHub Issue will be created for the feature and added to the roadmap +- These initial supporting SC members will be the champions for the feature and will be loosely responsible for shepherding the feature through the stages of the RFC process +- At this stage, the proposal is eligible for a sample PR implementation from a core team or community member +- The SC will indicate at this stage if this is a feature open to a community PR or something the core team would prefer to tackle +- All PRs at this stage should implement the feature in an "unstable" fashion (usually using an `unstable_` prefix on the future flag or API) + +### Stage 2 โ€” Alpha + +- A proposal enters **Stage 2 โ€” Alpha** once a PR has been opened implementing the feature in an `unstable_` state +- At this stage, we should open an Issue for the Proposal and add it to the [Roadmap](https://github.com/orgs/remix-run/projects/5) +- At this stage, we are looking for early community testing _before_ merging any work to the React Router repo โ€” so these PRs should provide a mechanism for community members to opt into to alpha testing + - Maintainers can trigger an alpha release from the PR branch by adding the `alpha-release` label, which will kick off an experimental release and comment it back on the PR + - Because the alpha release may contain other work committed to `dev` but not yet released in a stable version, it may not be ideal for testing in all cases + - In these cases, PR authors may also add the contents for a `.patch` file in a comment that folks can use via [patch-package](https://www.npmjs.com/package/patch-package) or [pnpm patch](https://pnpm.io/cli/patch) +- Feedback from alpha testers is considered essential for further progress +- The PR should also contain a changeset documenting the new API for the release notes +- SC members will review and approve the PR via GitHub reviews +- Approval at this stage communicates: + - The feature is valuable for React Router + - The API/code is sufficient for unstable/beta testing, though further iteration may be needed + - Code is not required to be in a final state yet, but it must be coded in such a way so as not to introduce regressions to other areas of the API + - We have seen enough positive feedback from Alpha testers to move the feature along + +### Stage 3 โ€” Beta + +- A proposal enters **Stage 3 โ€” Beta** once it receives **Stage 2 โ€” Alpha** PR approvals from 2 SC members and is merged to `dev` + - An SC member authoring the `unstable_` PR counts as an implicit approval, so in those cases explicit approval is required from 1 additional SC member +- This will include the feature in `nightly` releases and the next normal SemVer release for broader beta testing under the `unstable_` flag + +### Stage 4 โ€” Stabilization + +- A proposal enters **Stage 4 โ€” Stabilization** after a minimum of 1 month in **Stage 3 โ€” Beta** and a PR has been opened to remove the `unstable_` prefixes and stabilize the feature +- Stabilization PRs should add proper documentation for the feature +- SC members will review and approve the PR via GitHub reviews +- Approval at this stage communicates: + - Sufficient community feedback has been received from beta testers to trust the API's design and implementation + - The code is production-quality and well-tested, with no related regressions + - The PR includes documentation for the stable feature + +### Stage 5 โ€” Stable + +- A proposal enters **Stage 5 โ€” Stable** once it receives **Stage 4 โ€” Stabilization** PR approvals from at least 50% of the SC members and is merged to `dev` + - An SC member authoring the stabilization PR counts as an implicit approval +- This will include the stable feature in `nightly` releases and the next normal SemVer release diff --git a/contributors.yml b/contributors.yml index e0df33a8b1..913ae49346 100644 --- a/contributors.yml +++ b/contributors.yml @@ -1,4 +1,6 @@ - 0xEddie +- 3fuyang +- 43081j - aarbi - abdallah-nour - abeadam @@ -16,6 +18,7 @@ - akamfoad - alany411 - alberto +- AlemTuzlak - Aleuck - alexandernanberg - alexanderson1993 @@ -24,6 +27,7 @@ - amitdahan - AmRo045 - amsal +- Andarist - andreasottosson-polestar - andreiduca - antonmontrezor @@ -48,11 +52,14 @@ - BDomzalski - bhbs - bilalk711 +- bjohn465 +- bmsuseluda - bobziroll - bravo-kernel - Brendonovich - briankb - BrianT1414 +- Bricklou - brockross - brookslybrand - brophdawg11 @@ -60,15 +67,19 @@ - bvangraafeiland - camthompson - CanRau +- caprolactam - cassidoo - chaance - chasinhues - chensokheng +- chr33s +- chrille0313 - chrisngobanh - christopherchudzicki - ChristophP - christowiz - clavery +- clonemycode - Cmoen11 - codeape2 - coryhouse @@ -84,10 +95,12 @@ - david-crespo - davidbielik - dcblair +- dchenk - decadentsavant - developit - dgrijuela - DigitalNaut +- DimaAmega - dmitrytarassov - dokeet - doytch @@ -106,6 +119,7 @@ - FilipJirsak - focusotter - foxscotch +- frodi-karlsson - frontsideair - fucancode - fyzhu @@ -114,6 +128,7 @@ - gatzjames - gavriguy - Geist5000 +- GeoffKarnov - gesposito - gianlucca - gijo-varghese @@ -126,10 +141,14 @@ - haivuw - hampelm - harshmangalam +- HelpMe-Pls +- HenriqueLimas - hernanif1 +- hi-ogawa - HK-SHAO - holynewbie - hongji00 +- hoosierhuy - hsbtr - hyesungoh - iamnishanth @@ -138,6 +157,7 @@ - igniscyan - imjordanxd - infoxicator +- ioNihal - IsaiStormBlesed - Isammoc - iskanderbroere @@ -184,6 +204,8 @@ - ken0x0a - kentcdodds - kettanaito +- kigawas +- kilavvy - kiliman - kkirsche - kno-raziel @@ -193,6 +215,7 @@ - KubasuIvanSakwa - KutnerUri - kylegirard +- LadyTsukiko - landisdesign - latin-1 - lazybean @@ -218,17 +241,21 @@ - marc2332 - markdalgleish - markivancho +- markmals +- Marlon-Buckley - maruffahmed - marvinruder - mathpaquette - matmilbury - matt-harro +- matteogauthier - matthewlynch - maximevtush - maxpou - mcansh - MeatSim - MenouerBetty +- Methuselah96 - mfijas - MichaelDeBoey - michal-antczak @@ -240,6 +267,7 @@ - mjackson - mlewando - mm-jpoole +- mobregozo - modex98 - morleytatro - ms10596 @@ -247,16 +275,20 @@ - mtendekuyokwa19 - mtliendo - namoscato +- nanianlisao - ned-park - nenene3 +- ngbrown - nichtsam - nikeee - nilubisan - Nismit - nnhjs - noisypigeon +- nowells - Nurai1 - Obi-Dann +- okalil - OlegDev1 - omahs - omar-moquete @@ -267,6 +299,7 @@ - parveen232 - paulsmithkc - pavsoldatov +- pawelblaszczyk5 - pcattori - penx - petersendidit @@ -279,9 +312,12 @@ - pruszel - pwdcd - pyitphyoaung +- redabacha - refusado +- remorses - renyu-io - reyronald +- richardscarrott - rifaidev - rimian - robbtraister @@ -291,16 +327,20 @@ - rtmann - rtzll - rubeonline +- ruidi-huang +- rururux - ryanflorence - ryanhiebert - saengmotmi - samimsu - sanjai451 - sanketshah19 +- sapphi-red - saul-atomrigs - sbolel - scarf005 - sealer3 +- seasick - senseibarni - sergiodxa - serranoarevalo @@ -312,14 +352,17 @@ - shivamsinghchahar - silvenon - SimenB +- SirDaev - SkayuX - skratchdot +- skrhlm - smff - smithki - soartec-lab - sorokya - sorrycc - souzasmatheus +- SovietGhost - soxtoby - srmagura - SsongQ-92 @@ -341,6 +384,7 @@ - ThornWu - tiborbarsi - timdorr +- timfisher - TkDodo - tkindy - tlinhart @@ -354,6 +398,7 @@ - tosinamuda - triangularcube - trungpv1601 +- tryonelove - TrySound - ttys026 - Tumas2 @@ -363,6 +408,8 @@ - valerii15298 - ValiantCat - vdusart +- vesan +- vezaynk - VictorElHajj - vijaypushkin - vikingviolinist @@ -372,17 +419,24 @@ - vonagam - WalkAlone0325 - whxhlgy +- wilcoxmd - willemarcel - williamsdyyz - willsawyerrrr - willsmithte +- Willvillegas - wkovacs64 +- woodywoodsta - xavier-lc - xcsnowcity - xdaxer - yionr - yracnet - ytori +- yuhwan-park - yuleicul +- yuri-poliantsev +- zeevick10 - zeromask1337 - zheng-chuang +- zxTomw diff --git a/decisions/0003-data-strategy.md b/decisions/0003-data-strategy.md index dafb3b4fef..cb8884363e 100644 --- a/decisions/0003-data-strategy.md +++ b/decisions/0003-data-strategy.md @@ -113,7 +113,7 @@ Therefore, we're introducing the concept of a `DataStrategyMatch` which is just ```js function dataStrategy({ matches, defaultStrategy }) { return Promise.all( - matches.map((m) => match.route.then((route) => route.loader(/* ... */))) + matches.map((m) => match.route.then((route) => route.loader(/* ... */))), ); } ``` @@ -141,7 +141,7 @@ We considered how to handle `shouldRevalidate` behavior. There's sort of 2 basic 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 informaiton required for that (`currentUrl`, `currentParams`, `nextUrl`, `nextParams`, `submission` info, `actionResult`, etc.) - the API becomes a mess. +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. diff --git a/decisions/0005-remixing-react-router.md b/decisions/0005-remixing-react-router.md index 9b24896a86..e5b6e7389d 100644 --- a/decisions/0005-remixing-react-router.md +++ b/decisions/0005-remixing-react-router.md @@ -204,7 +204,7 @@ function NewErrorBoundary() { const error = useRouteError(); if (error instanceof Response) { - return ; + return ; } else { return ; } @@ -283,7 +283,7 @@ If folks still prefer the JSX notation, they can leverage `createRoutesFromEleme const routes = createRoutesFromElements( }> } /> - + , ); const router = createBrowserRouter(routes); diff --git a/decisions/0010-splitting-up-client-and-server-code-in-vite.md b/decisions/0010-splitting-up-client-and-server-code-in-vite.md index 70755110d7..144ce7d57c 100644 --- a/decisions/0010-splitting-up-client-and-server-code-in-vite.md +++ b/decisions/0010-splitting-up-client-and-server-code-in-vite.md @@ -96,6 +96,7 @@ Checking for `.server` modules only requires checking the module's path and does `vite-env-only` does require AST parsing and transformations so it will always be slower than `.server` modules. [^1]: Vite provides a lower-level module graph API, but the module graph is not guaranteed to be complete as it is only populated as modules are requested. + [^2]: When a file changes on disk, Vite invalidates the corresponding module in its cache to power features like HMR. [decision-0009]: ./0009-do-not-rely-on-treeshaking-for-correctness.md diff --git a/decisions/0014-context-middleware.md b/decisions/0014-context-middleware.md index a5be46d17f..10dd5db633 100644 --- a/decisions/0014-context-middleware.md +++ b/decisions/0014-context-middleware.md @@ -105,7 +105,7 @@ The middleware API we landed on to ship looks as follows: ```ts const myMiddleware: Route.unstable_MiddlewareFunction = async ( { request, context }, - next + next, ) => { // Do stuff before the handlers are called context.user = await getUser(request); @@ -125,7 +125,7 @@ export const middleware = [myMiddleware]; // `clientLoader`/`clientAction` const myClientMiddleware: Route.unstable_ClientMiddlewareFunction = ( { context }, - next + next, ) => { //... }; diff --git a/docs/api/components/Await.md b/docs/api/components/Await.md index feaf291683..259f55d1d4 100644 --- a/docs/api/components/Await.md +++ b/docs/api/components/Await.md @@ -4,6 +4,18 @@ title: Await # Await + + [MODES: framework, data] ## Summary @@ -12,16 +24,16 @@ title: Await Used to render promise values with automatic error handling. +**Note:** `` expects to be rendered inside a [``](https://react.dev/reference/react/Suspense) + ```tsx import { Await, useLoaderData } from "react-router"; -export function loader() { +export async function loader() { // not awaited const reviews = getReviews(); // awaited (blocks the transition) - const book = await fetch("/api/book").then((res) => - res.json() - ); + const book = await fetch("/api/book").then((res) => res.json()); return { book, reviews }; } @@ -47,14 +59,20 @@ function Book() { } ``` -`` expects to be rendered inside of a `` +## Signature + +```tsx +function Await({ + children, + errorElement, + resolve, +}: AwaitProps) +``` ## Props ### children -[modes: framework, data] - When using a function, the resolved value is provided as the parameter. ```tsx [2] @@ -63,13 +81,13 @@ When using a function, the resolved value is provided as the parameter. ``` -When using React elements, [useAsyncValue](../hooks/useAsyncValue) will provide the +When using React elements, [`useAsyncValue`](../hooks/useAsyncValue) will provide the resolved value: ```tsx [2] -; + function Reviews() { const resolvedReviews = useAsyncValue(); @@ -79,9 +97,8 @@ function Reviews() { ### errorElement -[modes: framework, data] - -The error element renders instead of the children when the promise rejects. +The error element renders instead of the `children` when the [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +rejects. ```tsx ``` -To provide a more contextual error, you can use the [useAsyncError](../hooks/useAsyncError) in a +To provide a more contextual error, you can use the [`useAsyncError`](../hooks/useAsyncError) in a child component ```tsx @@ -101,7 +118,7 @@ child component resolve={reviewsPromise} > -; + function ReviewsError() { const error = useAsyncError(); @@ -109,18 +126,18 @@ function ReviewsError() { } ``` -If you do not provide an errorElement, the rejected value will bubble up to -the nearest route-level ErrorBoundary and be accessible -via [useRouteError](../hooks/useRouteError) hook. +If you do not provide an `errorElement`, the rejected value will bubble up +to the nearest route-level [`ErrorBoundary`](../../start/framework/route-module#errorboundary) +and be accessible via the [`useRouteError`](../hooks/useRouteError) hook. ### resolve -[modes: framework, data] +Takes a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +returned from a [`loader`](../../start/framework/route-module#loader) to be +resolved and rendered. -Takes a promise returned from a [LoaderFunction](../Other/LoaderFunction) value to be resolved and rendered. - -```jsx -import { useLoaderData, Await } from "react-router"; +```tsx +import { Await, useLoaderData } from "react-router"; export async function loader() { let reviews = getReviews(); // not awaited @@ -153,3 +170,4 @@ export default function Book() { ); } ``` + diff --git a/docs/api/components/Form.md b/docs/api/components/Form.md index a393a37e4f..a63c679707 100644 --- a/docs/api/components/Form.md +++ b/docs/api/components/Form.md @@ -4,17 +4,41 @@ title: Form # Form + + [MODES: framework, data] ## Summary -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Form.html) +[Reference Documentation โ†—](https://api.reactrouter.com/v7/variables/react_router.Form.html) -A progressively enhanced HTML [`
`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) that submits data to actions via `fetch`, activating pending states in `useNavigation` which enables advanced user interfaces beyond a basic HTML form. After a form's action completes, all data on the page is automatically revalidated to keep the UI in sync with the data. +A progressively enhanced HTML [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) +that submits data to actions via [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), +activating pending states in [`useNavigation`](../hooks/useNavigation) which enables advanced +user interfaces beyond a basic HTML [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form). +After a form's `action` completes, all data on the page is automatically +revalidated to keep the UI in sync with the data. -Because it uses the HTML form API, server rendered pages are interactive at a basic level before JavaScript loads. Instead of React Router managing the submission, the browser manages the submission as well as the pending states (like the spinning favicon). After JavaScript loads, React Router takes over enabling web application user experiences. +Because it uses the HTML form API, server rendered pages are interactive at a +basic level before JavaScript loads. Instead of React Router managing the +submission, the browser manages the submission as well as the pending states +(like the spinning favicon). After JavaScript loads, React Router takes over +enabling web application user experiences. -Form is most useful for submissions that should also change the URL or otherwise add an entry to the browser history stack. For forms that shouldn't manipulate the browser history stack, use [``][fetcher_form]. +`Form` is most useful for submissions that should also change the URL or +otherwise add an entry to the browser history stack. For forms that shouldn't +manipulate the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +stack, use [``](https://api.reactrouter.com/v7/types/react_router.FetcherWithComponents.html#Form). ```tsx import { Form } from "react-router"; @@ -33,96 +57,90 @@ function NewEvent() { ### action -[modes: framework, data] - -The URL to submit the form data to. If `undefined`, this defaults to the closest route in context. +The URL to submit the form data to. If `undefined`, this defaults to the +closest route in context. ### discover -[modes: framework, data] +Defines the link discovery behavior. See [`DiscoverBehavior`](https://api.reactrouter.com/v7/types/react_router.DiscoverBehavior.html). -Determines application manifest discovery behavior. +```tsx + // default ("render") + + +``` -### encType +- **render** โ€” default, discover the route when the link renders +- **none** โ€” don't eagerly discover, only discover if the link is clicked -[modes: framework, data] +### encType The encoding type to use for the form submission. -### fetcherKey +```tsx + // Default + + +``` -[modes: framework, data] +### fetcherKey Indicates a specific fetcherKey to use when using `navigate={false}` so you -can pick up the fetcher's state in a different component in a [useFetcher](../hooks/useFetcher). +can pick up the fetcher's state in a different component in a [`useFetcher`](../hooks/useFetcher). ### method -[modes: framework, data] - -The HTTP verb to use when the form is submitted. Supports "get", "post", -"put", "delete", and "patch". +The HTTP verb to use when the form is submitted. Supports `"delete"`, +`"get"`, `"patch"`, `"post"`, and `"put"`. -Native `` only supports `get` and `post`, avoid the other verbs if -you'd like to support progressive enhancement +Native [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) +only supports `"get"` and `"post"`, avoid the other verbs if you'd like to +support progressive enhancement ### navigate -[modes: framework, data] - -Skips the navigation and uses a [useFetcher](../hooks/useFetcher) internally -when `false`. This is essentially a shorthand for `useFetcher()` + -`` where you don't care about the resulting data in this -component. +When `false`, skips the navigation and submits via a fetcher internally. +This is essentially a shorthand for [`useFetcher`](../hooks/useFetcher) + `` where +you don't care about the resulting data in this component. ### onSubmit -[modes: framework, data] - A function to call when the form is submitted. If you call -`event.preventDefault()` then this form will not do anything. +[`event.preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) +then this form will not do anything. ### preventScrollReset -[modes: framework, data] - Prevent the scroll position from resetting to the top of the viewport on -completion of the navigation when using the component +completion of the navigation when using the +```` component ### relative -[modes: framework, data] - Determines whether the form action is relative to the route hierarchy or the pathname. Use this if you want to opt out of navigating the route -hierarchy and want to instead route based on /-delimited URL segments +hierarchy and want to instead route based on slash-delimited URL segments. +See [`RelativeRoutingType`](https://api.reactrouter.com/v7/types/react_router.RelativeRoutingType.html). ### reloadDocument -[modes: framework, data] - -Forces a full document navigation instead of client side routing + data +Forces a full document navigation instead of client side routing and data fetch. ### replace -[modes: framework, data] - -Replaces the current entry in the browser history stack when the form -navigates. Use this if you don't want the user to be able to click "back" -to the page with the form on it. +Replaces the current entry in the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +stack when the form navigates. Use this if you don't want the user to be +able to click "back" to the page with the form on it. ### state -[modes: framework, data] - -State object to add to the history stack entry for this navigation +State object to add to the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +stack entry for this navigation ### viewTransition -[modes: framework, data] +Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) +for this navigation. To apply specific styles during the transition, see +[`useViewTransitionState`](../hooks/useViewTransitionState). -Enables a [View -Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) -for this navigation. To apply specific styles during the transition see -[useViewTransitionState](../hooks/useViewTransitionState). diff --git a/docs/api/components/Link.md b/docs/api/components/Link.md index d4ba095ce7..5b31e804ff 100644 --- a/docs/api/components/Link.md +++ b/docs/api/components/Link.md @@ -4,13 +4,26 @@ title: Link # Link + + [MODES: framework, data, declarative] ## Summary -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Link.html) +[Reference Documentation โ†—](https://api.reactrouter.com/v7/variables/react_router.Link.html) -A progressively enhanced `` wrapper to enable navigation with client-side routing. +A progressively enhanced [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) +wrapper to enable navigation with client-side routing. ```tsx import { Link } from "react-router"; @@ -35,11 +48,13 @@ import { Link } from "react-router"; Defines the link discovery behavior ```tsx + // default ("render") + ``` -- **render** - default, discover the route when the link renders -- **none** - don't eagerly discover, only discover if the link is clicked +- **render** โ€” default, discover the route when the link renders +- **none** โ€” don't eagerly discover, only discover if the link is clicked ### prefetch @@ -48,15 +63,20 @@ Defines the link discovery behavior Defines the data and module prefetching behavior for the link. ```tsx + // default + + + ``` -- **none** - default, no prefetching -- **intent** - prefetches when the user hovers or focuses the link -- **render** - prefetches when the link renders -- **viewport** - prefetches when the link is in the viewport, very useful for mobile +- **none** โ€” default, no prefetching +- **intent** โ€” prefetches when the user hovers or focuses the link +- **render** โ€” prefetches when the link renders +- **viewport** โ€” prefetches when the link is in the viewport, very useful for mobile -Prefetching is done with HTML `` tags. They are inserted after the link. +Prefetching is done with HTML [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) +tags. They are inserted after the link. ```tsx @@ -64,13 +84,18 @@ Prefetching is done with HTML `` tags. They are inserted af // might conditionally render ``` -Because of this, if you are using `nav :last-child` you will need to use `nav :last-of-type` so the styles don't conditionally fall off your last link (and any other similar selectors). +Because of this, if you are using `nav :last-child` you will need to use +`nav :last-of-type` so the styles don't conditionally fall off your last link +(and any other similar selectors). ### preventScrollReset [modes: framework, data] -Prevents the scroll position from being reset to the top of the window when the link is clicked and the app is using [ScrollRestoration](../components/ScrollRestoration). This only prevents new locations reseting scroll to the top, scroll position will be restored for back/forward button navigation. +Prevents the scroll position from being reset to the top of the window when +the link is clicked and the app is using [`ScrollRestoration`](../components/ScrollRestoration). This only +prevents new locations resetting scroll to the top, scroll position will be +restored for back/forward button navigation. ```tsx @@ -88,18 +113,25 @@ Defines the relative path behavior for the link. ``` -Consider a route hierarchy where a parent route pattern is "blog" and a child route pattern is "blog/:slug/edit". +Consider a route hierarchy where a parent route pattern is `"blog"` and a child +route pattern is `"blog/:slug/edit"`. -- **route** - default, resolves the link relative to the route pattern. In the example above a relative link of `".."` will remove both `:slug/edit` segments back to "/blog". -- **path** - relative to the path so `..` will only remove one URL segment up to "/blog/:slug" +- **route** โ€” default, resolves the link relative to the route pattern. In the +example above, a relative link of `"..."` will remove both `:slug/edit` segments +back to `"/blog"`. +- **path** โ€” relative to the path so `"..."` will only remove one URL segment up +to `"/blog/:slug"` -Note that index routes and layout routes have no paths so they are not included in the relative path calculation. +Note that index routes and layout routes do not have paths so they are not +included in the relative path calculation. ### reloadDocument [modes: framework, data, declarative] -Will use document navigation instead of client side routing when the link is clicked: the browser will handle the transition normally (as if it were an ``). +Will use document navigation instead of client side routing when the link is +clicked: the browser will handle the transition normally (as if it were an +[``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)). ```tsx @@ -109,7 +141,8 @@ Will use document navigation instead of client side routing when the link is cli [modes: framework, data, declarative] -Replaces the current entry in the history stack instead of pushing a new one onto it. +Replaces the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +stack instead of pushing a new one onto it. ```tsx @@ -145,13 +178,14 @@ function SomeComp() { } ``` -This state is inaccessible on the server as it is implemented on top of [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) +This state is inaccessible on the server as it is implemented on top of +[`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) ### to [modes: framework, data, declarative] -Can be a string or a partial [Path](../Other/Path): +Can be a string or a partial [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html): ```tsx @@ -167,9 +201,10 @@ Can be a string or a partial [Path](../Other/Path): ### viewTransition -[modes: framework, data, declarative] +[modes: framework, data] -Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for this navigation. +Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) +for this navigation. ```jsx @@ -177,4 +212,5 @@ Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/Vie ``` -To apply specific styles for the transition, see [useViewTransitionState](../hooks/useViewTransitionState) +To apply specific styles for the transition, see [`useViewTransitionState`](../hooks/useViewTransitionState) + diff --git a/docs/api/components/Links.md b/docs/api/components/Links.md index ccad683437..b1125a41a3 100644 --- a/docs/api/components/Links.md +++ b/docs/api/components/Links.md @@ -4,13 +4,28 @@ title: Links # Links + + [MODES: framework] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Links.html) -Renders all of the `` tags created by route module [`links`](../../start/framework/route-module#links) export. You should render it inside the `` of your document. +Renders all the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) +tags created by the route module's [`links`](../../start/framework/route-module#links) +export. You should render it inside the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) +of your document. ```tsx import { Links } from "react-router"; @@ -27,6 +42,17 @@ export default function Root() { } ``` +## Signature + +```tsx +function Links({ nonce }: LinksProps): React.JSX.Element +``` + ## Props -None +### nonce + +A [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce) +attribute to render on the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) +element + diff --git a/docs/api/components/Meta.md b/docs/api/components/Meta.md index 8c90529312..46a280cd42 100644 --- a/docs/api/components/Meta.md +++ b/docs/api/components/Meta.md @@ -4,13 +4,28 @@ title: Meta # Meta + + [MODES: framework] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Meta.html) -Renders all the `` tags created by route module [`meta`](../../start/framework/route-module#meta) export. You should render it inside the `` of your HTML. +Renders all the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) +tags created by the route module's [`meta`](../../start/framework/route-module#meta) +export. You should render it inside the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) +of your document. ```tsx import { Meta } from "react-router"; @@ -26,6 +41,9 @@ export default function Root() { } ``` -## Props +## Signature + +```tsx +function Meta(): React.JSX.Element +``` -None diff --git a/docs/api/components/NavLink.md b/docs/api/components/NavLink.md index c837765e7f..8af6deddf3 100644 --- a/docs/api/components/NavLink.md +++ b/docs/api/components/NavLink.md @@ -4,25 +4,40 @@ title: NavLink # NavLink + + [MODES: framework, data, declarative] ## Summary -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.NavLink.html) +[Reference Documentation โ†—](https://api.reactrouter.com/v7/variables/react_router.NavLink.html) -Wraps [Link](../components/Link) with additional props for styling active and pending states. +Wraps [``](../components/Link) with additional props for styling active and +pending states. -- Automatically applies classes to the link based on its active and pending states, see NavLinkProps.className. -- Automatically applies `aria-current="page"` to the link when the link is active. See [`aria-current`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current) on MDN. +- Automatically applies classes to the link based on its `active` and `pending` +states, see [`NavLinkProps.className`](https://api.reactrouter.com/v7/interfaces/react_router.NavLinkProps.html#className) + - Note that `pending` is only available with Framework and Data modes. +- Automatically applies `aria-current="page"` to the link when the link is active. +See [`aria-current`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current) +on MDN. +- States are additionally available through the className, style, and children +render props. See [`NavLinkRenderProps`](https://api.reactrouter.com/v7/types/react_router.NavLinkRenderProps.html). ```tsx -import { NavLink } from "react-router"; -; -``` +Messages -States are available through the className, style, and children render props. See [NavLinkRenderProps](../Other/NavLinkRenderProps). - -```tsx +// Using render props @@ -50,21 +65,22 @@ Changes the matching logic to make it case-sensitive: [modes: framework, data, declarative] -Can be regular React children or a function that receives an object with the active and pending states of the link. +Can be regular React children or a function that receives an object with the +`active` and `pending` states of the link. -```tsx - - {({ isActive }) => ( - Tasks - )} - -``` + ```tsx + + {({ isActive }) => ( + Tasks + )} + + ``` ### className [modes: framework, data, declarative] -Classes are automatically applied to NavLink that correspond to the state. +Classes are automatically applied to `NavLink` that correspond to the state. ```css a.active { @@ -78,7 +94,16 @@ a.transitioning { } ``` -Note that `pending` is only available with Framework and Data modes. +Or you can specify a function that receives [`NavLinkRenderProps`](https://api.reactrouter.com/v7/types/react_router.NavLinkRenderProps.html) and +returns the `className`: + +```tsx + ( + isActive ? "my-active-class" : + isPending ? "my-pending-class" : + "" +)} /> +``` ### discover @@ -87,17 +112,21 @@ Note that `pending` is only available with Framework and Data modes. Defines the link discovery behavior ```tsx + // default ("render") + ``` -- **render** - default, discover the route when the link renders -- **none** - don't eagerly discover, only discover if the link is clicked +- **render** โ€” default, discover the route when the link renders +- **none** โ€” don't eagerly discover, only discover if the link is clicked ### end [modes: framework, data, declarative] -Changes the matching logic for the `active` and `pending` states to only match to the "end" of the NavLinkProps.to. If the URL is longer, it will no longer be considered active. +Changes the matching logic for the `active` and `pending` states to only match +to the "end" of the [`NavLinkProps.to`](https://api.reactrouter.com/v7/interfaces/react_router.NavLinkProps.html#to). If the URL is longer, it will no +longer be considered active. | Link | URL | isActive | | ----------------------------- | ------------ | -------- | @@ -106,7 +135,9 @@ Changes the matching logic for the `active` and `pending` states to only match t | `` | `/tasks` | true | | `` | `/tasks/123` | false | -`` is an exceptional case because _every_ URL matches `/`. To avoid this matching every single route by default, it effectively ignores the `end` prop and only matches when you're at the root route. +`` is an exceptional case because _every_ URL matches `/`. +To avoid this matching every single route by default, it effectively ignores +the `end` prop and only matches when you're at the root route. ### prefetch @@ -115,15 +146,20 @@ Changes the matching logic for the `active` and `pending` states to only match t Defines the data and module prefetching behavior for the link. ```tsx + // default + + + ``` -- **none** - default, no prefetching -- **intent** - prefetches when the user hovers or focuses the link -- **render** - prefetches when the link renders -- **viewport** - prefetches when the link is in the viewport, very useful for mobile +- **none** โ€” default, no prefetching +- **intent** โ€” prefetches when the user hovers or focuses the link +- **render** โ€” prefetches when the link renders +- **viewport** โ€” prefetches when the link is in the viewport, very useful for mobile -Prefetching is done with HTML `` tags. They are inserted after the link. +Prefetching is done with HTML [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) +tags. They are inserted after the link. ```tsx @@ -131,13 +167,18 @@ Prefetching is done with HTML `` tags. They are inserted af // might conditionally render ``` -Because of this, if you are using `nav :last-child` you will need to use `nav :last-of-type` so the styles don't conditionally fall off your last link (and any other similar selectors). +Because of this, if you are using `nav :last-child` you will need to use +`nav :last-of-type` so the styles don't conditionally fall off your last link +(and any other similar selectors). ### preventScrollReset [modes: framework, data] -Prevents the scroll position from being reset to the top of the window when the link is clicked and the app is using [ScrollRestoration](../components/ScrollRestoration). This only prevents new locations reseting scroll to the top, scroll position will be restored for back/forward button navigation. +Prevents the scroll position from being reset to the top of the window when +the link is clicked and the app is using [`ScrollRestoration`](../components/ScrollRestoration). This only +prevents new locations resetting scroll to the top, scroll position will be +restored for back/forward button navigation. ```tsx @@ -155,16 +196,25 @@ Defines the relative path behavior for the link. ``` -Consider a route hierarchy where a parent route pattern is "blog" and a child route pattern is "blog/:slug/edit". +Consider a route hierarchy where a parent route pattern is `"blog"` and a child +route pattern is `"blog/:slug/edit"`. + +- **route** โ€” default, resolves the link relative to the route pattern. In the +example above, a relative link of `"..."` will remove both `:slug/edit` segments +back to `"/blog"`. +- **path** โ€” relative to the path so `"..."` will only remove one URL segment up +to `"/blog/:slug"` -- **route** - default, resolves the link relative to the route pattern. In the example above a relative link of `".."` will remove both `:slug/edit` segments back to "/blog". -- **path** - relative to the path so `..` will only remove one URL segment up to "/blog/:slug" +Note that index routes and layout routes do not have paths so they are not +included in the relative path calculation. ### reloadDocument [modes: framework, data, declarative] -Will use document navigation instead of client side routing when the link is clicked: the browser will handle the transition normally (as if it were an ``). +Will use document navigation instead of client side routing when the link is +clicked: the browser will handle the transition normally (as if it were an +[``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)). ```tsx @@ -174,7 +224,8 @@ Will use document navigation instead of client side routing when the link is cli [modes: framework, data, declarative] -Replaces the current entry in the history stack instead of pushing a new one onto it. +Replaces the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +stack instead of pushing a new one onto it. ```tsx @@ -210,13 +261,15 @@ function SomeComp() { } ``` -This state is inaccessible on the server as it is implemented on top of [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) +This state is inaccessible on the server as it is implemented on top of +[`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) ### style [modes: framework, data, declarative] -Regular React style object or a function that receives an object with the active and pending states of the link. +Styles can also be applied dynamically via a function that receives +[`NavLinkRenderProps`](https://api.reactrouter.com/v7/types/react_router.NavLinkRenderProps.html) and returns the styles: ```tsx @@ -227,13 +280,11 @@ Regular React style object or a function that receives an object with the active })} /> ``` -Note that `pending` is only available with Framework and Data modes. - ### to [modes: framework, data, declarative] -Can be a string or a partial [Path](../Other/Path): +Can be a string or a partial [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html): ```tsx @@ -249,9 +300,10 @@ Can be a string or a partial [Path](../Other/Path): ### viewTransition -[modes: framework, data, declarative] +[modes: framework, data] -Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for this navigation. +Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) +for this navigation. ```jsx @@ -259,4 +311,5 @@ Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/Vie ``` -To apply specific styles for the transition, see [useViewTransitionState](../hooks/useViewTransitionState) +To apply specific styles for the transition, see [`useViewTransitionState`](../hooks/useViewTransitionState) + diff --git a/docs/api/components/Navigate.md b/docs/api/components/Navigate.md index d351c552d5..e512b36323 100644 --- a/docs/api/components/Navigate.md +++ b/docs/api/components/Navigate.md @@ -4,44 +4,57 @@ title: Navigate # Navigate + + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Navigate.html) -A component-based version of [useNavigate](../hooks/useNavigate) to use in a [`React.Component -Class`](https://reactjs.org/docs/react-component.html) where hooks are not -able to be used. +A component-based version of [`useNavigate`](../hooks/useNavigate) to use in a +[`React.Component` class](https://react.dev/reference/react/Component) where +hooks cannot be used. -It's recommended to avoid using this component in favor of [useNavigate](../hooks/useNavigate) +It's recommended to avoid using this component in favor of [`useNavigate`](../hooks/useNavigate). ```tsx ``` +## Signature + +```tsx +function Navigate({ to, replace, state, relative }: NavigateProps): null +``` + ## Props ### relative -[modes: framework, data, declarative] - -_No documentation_ +How to interpret relative routing in the `to` prop. +See [`RelativeRoutingType`](https://api.reactrouter.com/v7/types/react_router.RelativeRoutingType.html). ### replace -[modes: framework, data, declarative] - -_No documentation_ +Whether to replace the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +stack ### state -[modes: framework, data, declarative] - -_No documentation_ +State to pass to the new [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) to store in [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state). ### to -[modes: framework, data, declarative] +The path to navigate to. This can be a string or a [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html) object -_No documentation_ diff --git a/docs/api/components/Outlet.md b/docs/api/components/Outlet.md index 0d38ae8b8e..385e233435 100644 --- a/docs/api/components/Outlet.md +++ b/docs/api/components/Outlet.md @@ -4,13 +4,26 @@ title: Outlet # Outlet + + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Outlet.html) -Renders the matching child route of a parent route or nothing if no child route matches. +Renders the matching child route of a parent route or nothing if no child +route matches. ```tsx import { Outlet } from "react-router"; @@ -25,16 +38,22 @@ export default function SomeParent() { } ``` +## Signature + +```tsx +function Outlet(props: OutletProps): React.ReactElement | null +``` + ## Props ### context -[modes: framework, data, declarative] - -Provides a context value to the element tree below the outlet. Use when the parent route needs to provide values to child routes. +Provides a context value to the element tree below the outlet. Use when +the parent route needs to provide values to child routes. ```tsx ``` -Access the context with [useOutletContext](../hooks/useOutletContext). +Access the context with [`useOutletContext`](../hooks/useOutletContext). + diff --git a/docs/api/components/PrefetchPageLinks.md b/docs/api/components/PrefetchPageLinks.md index 47bfe6afb5..3822ea7434 100644 --- a/docs/api/components/PrefetchPageLinks.md +++ b/docs/api/components/PrefetchPageLinks.md @@ -4,62 +4,54 @@ title: PrefetchPageLinks # PrefetchPageLinks + + [MODES: framework] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.PrefetchPageLinks.html) -Renders `` tags for modules and data of another page to enable an instant navigation to that page. `` uses this internally, but you can render it to prefetch a page for any other reason. +Renders [``](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/rel) +tags for modules and data of another page to enable an instant navigation to +that page. [``](./Link#prefetch) uses this internally, but you +can render it to prefetch a page for any other reason. + +For example, you may render one of this as the user types into a search field +to prefetch search results before they click through to their selection. ```tsx import { PrefetchPageLinks } from "react-router"; -; + ``` -For example, you may render one of this as the user types into a search field to prefetch search results before they click through to their selection. - -## Props - -### crossOrigin - -[modes: framework] - -How the element handles crossorigin requests - -### disabled - -[modes: framework] +## Signature -Whether the link is disabled - -### hrefLang - -[modes: framework] - -Language of the linked resource - -### integrity - -[modes: framework] - -Integrity metadata used in Subresource Integrity checks - -### media - -[modes: framework] +```tsx +function PrefetchPageLinks({ page, ...linkProps }: PageLinkDescriptor) +``` -Applicable media: "screen", "print", "(max-width: 764px)" +## Props ### page -[modes: framework] - -The absolute path of the page to prefetch. +The absolute path of the page to prefetch, e.g. `/absolute/path`. -### referrerPolicy +### linkProps -[modes: framework] +Additional props to spread onto the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) tags, such as [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/crossOrigin), +[`integrity`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/integrity), +[`rel`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/rel), +etc. -Referrer policy for fetches initiated by the element diff --git a/docs/api/components/Route.md b/docs/api/components/Route.md index 90c59b871c..3db1398209 100644 --- a/docs/api/components/Route.md +++ b/docs/api/components/Route.md @@ -4,6 +4,18 @@ title: Route # Route + + [MODES: framework, data, declarative] ## Summary @@ -11,38 +23,121 @@ title: Route [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Route.html) Configures an element to render when a pattern matches the current location. -It must be rendered within a [Routes](../components/Routes) element. Note that these routes +It must be rendered within a [`Routes`](../components/Routes) element. Note that these routes do not participate in data loading, actions, code splitting, or any other route module features. +```tsx +// Usually used in a declarative router +function App() { + return ( + + + } /> + } /> + } /> + + + ); +} + +// But can be used with a data router as well if you prefer the JSX notation +const routes = createRoutesFromElements( + <> + + + + +); + +const router = createBrowserRouter(routes); + +function App() { + return ; +} +``` + +## Signature + +```tsx +function Route(props: RouteProps): React.ReactElement | null +``` + ## Props +### action + +The route action. +See [`action`](../../start/data/route-object#action). + ### caseSensitive -[modes: framework, data, declarative] +Whether the path should be case-sensitive. Defaults to `false`. -Whether the path should be matched in a case-sensitive manner. +### Component + +The React Component to render when this route matches. +Mutually exclusive with `element`. ### children -[modes: framework, data, declarative] +Child Route components -_No documentation_ +### element -### Component +The React element to render when this Route matches. +Mutually exclusive with `Component`. -[modes: framework, data, declarative] +### ErrorBoundary -_No documentation_ +The React Component to render at this route if an error occurs. +Mutually exclusive with `errorElement`. -### element +### errorElement + +The React element to render at this route if an error occurs. +Mutually exclusive with `ErrorBoundary`. + +### handle -[modes: framework, data, declarative] +The route handle. -_No documentation_ +### HydrateFallback + +The React Component to render while this router is loading data. +Mutually exclusive with `hydrateFallbackElement`. + +### hydrateFallbackElement + +The React element to render while this router is loading data. +Mutually exclusive with `HydrateFallback`. + +### id + +The unique identifier for this route (for use with [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react_router.DataRouter.html)s) + +### index + +Whether this is an index route. + +### lazy + +A function that returns a promise that resolves to the route object. +Used for code-splitting routes. +See [`lazy`](../../start/data/route-object#lazy). + +### loader + +The route loader. +See [`loader`](../../start/data/route-object#loader). ### path -[modes: framework, data, declarative] +The path pattern to match. If unspecified or empty, then this becomes a +layout route. + +### shouldRevalidate + +The route shouldRevalidate function. +See [`shouldRevalidate`](../../start/data/route-object#shouldRevalidate). -The path to match against the current location. diff --git a/docs/api/components/Routes.md b/docs/api/components/Routes.md index 187b52cc4c..4fa2c28c9e 100644 --- a/docs/api/components/Routes.md +++ b/docs/api/components/Routes.md @@ -4,36 +4,55 @@ title: Routes # Routes + + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Routes.html) -Renders a branch of [Route](../components/Route) that best matches the current -location. Note that these routes do not participate in data loading, actions, -code splitting, or any other route module features. +Renders a branch of [``s](../components/Route) that best matches the current +location. Note that these routes do not participate in [data loading](../../start/framework/route-module#loader), +[`action`](../../start/framework/route-module#action), code splitting, or +any other [route module](../../start/framework/route-module) features. ```tsx -import { Routes, Route } from "react-router" +import { Route, Routes } from "react-router"; - } /> - } /> - } /> + } /> + } /> + }> ``` +## Signature + +```tsx +function Routes({ + children, + location, +}: RoutesProps): React.ReactElement | null +``` + ## Props ### children -[modes: framework, data, declarative] - -Nested [Route](../components/Route) elements +Nested [`Route`](../components/Route) elements ### location -[modes: framework, data, declarative] +The [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) to match against. Defaults to the current location. -The location to match against. Defaults to the current location. diff --git a/docs/api/components/Scripts.md b/docs/api/components/Scripts.md index 1e1d1830db..19c03e2167 100644 --- a/docs/api/components/Scripts.md +++ b/docs/api/components/Scripts.md @@ -4,13 +4,31 @@ title: Scripts # Scripts + + [MODES: framework] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Scripts.html) -Renders the client runtime of your app. It should be rendered inside the `` of the document. +Renders the client runtime of your app. It should be rendered inside the +[``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body) + of the document. + +If server rendering, you can omit `` and the app will work as a +traditional web app without JavaScript, relying solely on HTML and browser +behaviors. ```tsx import { Scripts } from "react-router"; @@ -27,17 +45,17 @@ export default function Root() { } ``` -If server rendering, you can omit `` and the app will work as a traditional web app without JavaScript, relying solely on HTML and browser behaviors. +## Signature -## Props - -### ScriptsProps +```tsx +function Scripts(scriptProps: ScriptsProps): React.JSX.Element | null +``` -[modes: framework] +## Props -A couple common attributes: +### scriptProps -- `` for hosting your static assets on a different server than your app. -- `` to support a [content security policy for scripts](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) with [nonce-sources](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources) for your ` + + + + + + ); +} +``` + +## Components to Render + +Because the root route manages your document, it is the proper place to render a handful of "document-level" components React Router provides. These components are to be used once inside your root route and they include everything React Router figured out or built in order for your page to render properly. + +```tsx filename=app/root.tsx +import { + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +export default function App() { + return ( + + + + + + + {/* Child routes render here */} + + + {/* Manages scroll position for client-side transitions */} + {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} + + + {/* Script tags go here */} + {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} + + + + ); +} +``` + +If you are are not on React 19 or choosing not to use React's [``][react-link], [``][react-title], and [`<meta>`][react-meta] components, and instead relying on React Router's [`links`][react-router-links] and [`meta`][react-router-meta] exports, you need to add the following to your root route: + +```tsx filename=app/root.tsx +import { Links, Meta } from "react-router"; + +export default function App() { + return ( + <html lang="en"> + <head> + {/* All `meta` exports on all routes will render here */} + <Meta /> + + {/* All `link` exports on all routes will render here */} + <Links /> + <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head> + <body> + <Outlet /> + <ScrollRestoration /> + <Scripts /> + </body> + </html> + ); +} +``` + +## Layout Export + +The root route supports all [route module exports][route-module]. + +The root route also supports an additional optional `Layout` export. The `Layout` component serves 2 purposes: + +1. Avoid duplicating your document's "app shell" across your root component, `HydrateFallback`, and `ErrorBoundary` +2. Prevent React from re-mounting your app shell elements when switching between the root component/`HydrateFallback`/`ErrorBoundary` which can cause a FOUC if React removes and re-adds `<link rel="stylesheet">` tags from your `<Links>` component. + +`Layout` takes a single `children` prop, which is the `default` export (e.g. `App`), `HydrateFallback`, or `ErrorBoundary`. + +```tsx filename=app/root.tsx +export function Layout({ children }) { + return ( + <html lang="en"> + <head> + <meta charSet="utf-8" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1" + /> + <Meta /> + <Links /> + <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head> + <body> + {/* children will be the root Component, ErrorBoundary, or HydrateFallback */} + {children} + <Scripts /> + <ScrollRestoration /> + </body> + </html> + ); +} + +export default function App() { + return <Outlet />; +} + +export function ErrorBoundary() {} +``` + +**A note on `useLoaderData`in the `Layout` Component** + +`useLoaderData` is not permitted to be used in `ErrorBoundary` components because it is intended for the happy-path route rendering, and its typings have a built-in assumption that the `loader` ran successfully and returned something. That assumption doesn't hold in an `ErrorBoundary` because it could have been the `loader` that threw and triggered the boundary! In order to access loader data in `ErrorBoundary`'s, you can use `useRouteLoaderData` which accounts for the loader data potentially being `undefined`. + +Because your `Layout` component is used in both success and error flows, this same restriction holds. If you need to fork logic in your `Layout` depending on if it was a successful request or not, you can use `useRouteLoaderData("root")` and `useRouteError()`. + +<docs-warn>Because your `<Layout>` component is used for rendering the `ErrorBoundary`, you should be _very defensive_ to ensure that you can render your `ErrorBoundary` without encountering any render errors. If your `Layout` throws another error trying to render the boundary, then it can't be used and your UI will fall back to the very minimal built-in default `ErrorBoundary`.</docs-warn> + +```tsx filename=app/root.tsx lines=[6-7,19-29,32-34] +export function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const data = useRouteLoaderData("root"); + const error = useRouteError(); + + return ( + <html lang="en"> + <head> + <meta charSet="utf-8" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1" + /> + <Meta /> + <Links /> + <style + dangerouslySetInnerHTML={{ + __html: ` + :root { + --themeVar: ${ + data?.themeVar || defaultThemeVar + } + } + `, + }} + /> + <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head> + <body> + {data ? ( + <Analytics token={data.analyticsToken} /> + ) : null} + {children} + <ScrollRestoration /> + <Scripts /> + </body> + </html> + ); +} +``` + +[route-module]: ../start/framework/route-module +[react-link]: https://react.dev/reference/react-dom/components/link +[react-meta]: https://react.dev/reference/react-dom/components/meta +[react-title]: https://react.dev/reference/react-dom/components/title +[react-router-links]: ../../start/framework/route-module#links +[react-router-meta]: ../../start/framework/route-module#meta diff --git a/docs/api/framework-conventions/routes.ts.md b/docs/api/framework-conventions/routes.ts.md new file mode 100644 index 0000000000..7d4845c42d --- /dev/null +++ b/docs/api/framework-conventions/routes.ts.md @@ -0,0 +1,67 @@ +--- +title: routes.ts +order: 2 +--- + +# routes.ts + +[MODES: framework] + +## Summary + +<docs-info> +This file is required +</docs-info> + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/interfaces/_react_router_dev.routes.RouteConfigEntry.html) + +Configuration file that maps URL patterns to route modules in your application. + +See the [routing guide][routing] for more information. + +## Examples + +### Basic + +Configure your routes as an array of objects. + +```tsx filename=app/routes.ts +import { + type RouteConfig, + route, +} from "@react-router/dev/routes"; + +export default [ + route("some/path", "./some/file.tsx"), + // pattern ^ ^ module file +] satisfies RouteConfig; +``` + +You can use the following helpers to create route config entries: + +- [`route`][route] โ€” Helper function for creating a route config entry +- [`index`][index] โ€” Helper function for creating a route config entry for an index route +- [`layout`][layout] โ€” Helper function for creating a route config entry for a layout route +- [`prefix`][prefix] โ€” Helper function for adding a path prefix to a set of routes without needing to introduce a parent route file +- [`relative`][relative] โ€” Creates a set of route config helpers that resolve file paths relative to the given directory. Designed to support splitting route config into multiple files within different directories + +### File-based Routing + +If you prefer to define your routes via file naming conventions rather than configuration, the `@react-router/fs-routes` package provides a [file system routing convention][file-route-conventions]: + +```ts filename=app/routes.ts +import { type RouteConfig } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +export default flatRoutes() satisfies RouteConfig; +``` + +### Route Helpers + +[routing]: ../../start/framework/routing +[route]: https://api.reactrouter.com/v7/functions/_react_router_dev.routes.route.html +[index]: https://api.reactrouter.com/v7/functions/_react_router_dev.routes.index.html +[layout]: https://api.reactrouter.com/v7/functions/_react_router_dev.routes.layout.html +[prefix]: https://api.reactrouter.com/v7/functions/_react_router_dev.routes.prefix.html +[relative]: https://api.reactrouter.com/v7/functions/_react_router_dev.routes.relative.html +[file-route-conventions]: ../../how-to/file-route-conventions diff --git a/docs/api/framework-conventions/server-modules.md b/docs/api/framework-conventions/server-modules.md new file mode 100644 index 0000000000..b753f9d8b1 --- /dev/null +++ b/docs/api/framework-conventions/server-modules.md @@ -0,0 +1,158 @@ +--- +title: .server modules +--- + +# `.server` modules + +[MODES: framework] + +## Summary + +Server-only modules that are excluded from client bundles and only run on the server. + +```ts filename=auth.server.ts +// This would expose secrets on the client if not exported from a server-only module +export const JWT_SECRET = process.env.JWT_SECRET; + +export function validateToken(token: string) { + // Server-only authentication logic +} +``` + +`.server` modules are a good way to explicitly mark entire modules as server-only. The build will fail if any code in a `.server` file or `.server` directory accidentally ends up in the client module graph. + +<docs-warning> + +Route modules should not be marked as `.server` or `.client` as they have special handling and need to be referenced in both server and client module graphs. Attempting to do so will cause build errors. + +</docs-warning> + +<docs-info> + +If you need more sophisticated control over what is included in the client/server bundles, check out the [`vite-env-only` plugin](https://github.com/pcattori/vite-env-only). + +</docs-info> + + +## Usage Patterns + +### Individual Files + +Mark individual files as server-only by adding `.server` to the filename: + +```txt +app/ +โ”œโ”€โ”€ auth.server.ts ๐Ÿ‘ˆ server-only file +โ”œโ”€โ”€ database.server.ts +โ”œโ”€โ”€ email.server.ts +โ””โ”€โ”€ root.tsx +``` + +### Server Directories + +Mark entire directories as server-only by using `.server` in the directory name: + +```txt +app/ +โ”œโ”€โ”€ .server/ ๐Ÿ‘ˆ entire directory is server-only +โ”‚ โ”œโ”€โ”€ auth.ts +โ”‚ โ”œโ”€โ”€ database.ts +โ”‚ โ””โ”€โ”€ email.ts +โ”œโ”€โ”€ components/ +โ””โ”€โ”€ root.tsx +``` + +## Examples + +### Database Connection + +```ts filename=app/utils/db.server.ts +import { PrismaClient } from "@prisma/client"; + +// This would expose database credentials on the client +const db = new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL, + }, + }, +}); + +export { db }; +``` + +### Authentication Utilities + +```ts filename=app/utils/auth.server.ts +import jwt from "jsonwebtoken"; +import bcrypt from "bcryptjs"; + +const JWT_SECRET = process.env.JWT_SECRET!; + +export function hashPassword(password: string) { + return bcrypt.hash(password, 10); +} + +export function verifyPassword( + password: string, + hash: string +) { + return bcrypt.compare(password, hash); +} + +export function createToken(userId: string) { + return jwt.sign({ userId }, JWT_SECRET, { + expiresIn: "7d", + }); +} + +export function verifyToken(token: string) { + return jwt.verify(token, JWT_SECRET) as { + userId: string; + }; +} +``` + +### Using Server Modules + +```tsx filename=app/routes/login.tsx +import type { ActionFunctionArgs } from "react-router"; +import { redirect } from "react-router"; +import { + hashPassword, + createToken, +} from "../utils/auth.server"; +import { db } from "../utils/db.server"; + +export async function action({ + request, +}: ActionFunctionArgs) { + const formData = await request.formData(); + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + // Server-only operations + const hashedPassword = await hashPassword(password); + const user = await db.user.create({ + data: { email, password: hashedPassword }, + }); + + const token = createToken(user.id); + + return redirect("/dashboard", { + headers: { + "Set-Cookie": `token=${token}; HttpOnly; Secure; SameSite=Strict`, + }, + }); +} + +export default function Login() { + return ( + <form method="post"> + <input name="email" type="email" required /> + <input name="password" type="password" required /> + <button type="submit">Login</button> + </form> + ); +} +``` diff --git a/docs/api/framework-routers/HydratedRouter.md b/docs/api/framework-routers/HydratedRouter.md new file mode 100644 index 0000000000..cc7b1becf4 --- /dev/null +++ b/docs/api/framework-routers/HydratedRouter.md @@ -0,0 +1,58 @@ +--- +title: HydratedRouter +--- + +# HydratedRouter + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom-export/hydrated-router.tsx +--> + +[MODES: framework] + +## Summary + +Framework-mode router component to be used to hydrate a router from a +[`ServerRouter`](../framework-routers/ServerRouter). See [`entry.client.tsx`](../framework-conventions/entry.client.tsx). + +## Signature + +```tsx +function HydratedRouter(props: HydratedRouterProps) +``` + +## Props + +### unstable_getContext + +Context object to be passed through to [`createBrowserRouter`](../data-routers/createBrowserRouter) and made +available to +[`clientAction`](../../start/framework/route-module#clientAction)/[`clientLoader`](../../start/framework/route-module#clientLoader) +functions + +### unstable_onError + +An error handler function that will be called for any loader/action/render +errors that are encountered in your application. This is useful for +logging or reporting errors instead of the `ErrorBoundary` because it's not +subject to re-rendering and will only run one time per error. + +The `errorInfo` parameter is passed along from +[`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch) +and is only present for render errors. + +```tsx +<HydratedRouter unstable_onError={(error, errorInfo) => { + console.error(error, errorInfo); + reportToErrorService(error, errorInfo); +}} /> +``` + diff --git a/docs/api/framework-routers/ServerRouter.md b/docs/api/framework-routers/ServerRouter.md new file mode 100644 index 0000000000..1710cbc378 --- /dev/null +++ b/docs/api/framework-routers/ServerRouter.md @@ -0,0 +1,54 @@ +--- +title: ServerRouter +--- + +# ServerRouter + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/ssr/server.tsx +--> + +[MODES: framework] + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.ServerRouter.html) + +The server entry point for a React Router app in Framework Mode. This +component is used to generate the HTML in the response from the server. See +[`entry.server.tsx`](../framework-conventions/entry.server.tsx). + +## Signature + +```tsx +function ServerRouter({ + context, + url, + nonce, +}: ServerRouterProps): ReactElement +``` + +## Props + +### context + +The entry context containing the manifest, route modules, and other data +needed for rendering. + +### nonce + +An optional `nonce` for [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) +compliance, used to allow inline scripts to run safely. + +### url + +The URL of the request being handled. + diff --git a/docs/api/framework-routers/index.md b/docs/api/framework-routers/index.md new file mode 100644 index 0000000000..9942039def --- /dev/null +++ b/docs/api/framework-routers/index.md @@ -0,0 +1,4 @@ +--- +title: Framework Routers +order: 4 +--- diff --git a/docs/api/hooks/unstable_usePrompt.md b/docs/api/hooks/unstable_usePrompt.md deleted file mode 100644 index 6238a0205e..0000000000 --- a/docs/api/hooks/unstable_usePrompt.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: unstable_usePrompt ---- - -# unstable_usePrompt - -[MODES: framework, data] - -## Summary - -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.unstable_usePrompt.html) - -Wrapper around useBlocker to show a window.confirm prompt to users instead of building a custom UI with [useBlocker](../hooks/useBlocker). - -The `unstable_` flag will not be removed because this technique has a lot of rough edges and behaves very differently (and incorrectly sometimes) across browsers if users click addition back/forward navigations while the confirmation is open. Use at your own risk. - -```tsx -function ImportantForm() { - let [value, setValue] = React.useState(""); - - // Block navigating elsewhere when data has been entered into the input - unstable_usePrompt({ - message: "Are you sure?", - when: ({ currentLocation, nextLocation }) => - value !== "" && - currentLocation.pathname !== nextLocation.pathname, - }); - - return ( - <Form method="post"> - <label> - Enter some important data: - <input - name="data" - value={value} - onChange={(e) => setValue(e.target.value)} - /> - </label> - <button type="submit">Save</button> - </Form> - ); -} -``` - -## Signature - -```tsx -unstable_usePrompt(options): void -``` - -## Params - -### options - -_No documentation_ diff --git a/docs/api/hooks/useActionData.md b/docs/api/hooks/useActionData.md index 98e1a91b3a..764cb5f1a1 100644 --- a/docs/api/hooks/useActionData.md +++ b/docs/api/hooks/useActionData.md @@ -4,13 +4,27 @@ title: useActionData # useActionData +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useActionData.html) -Returns the action data from the most recent POST navigation form submission or `undefined` if there hasn't been one. +Returns the [`action`](../../start/framework/route-module#action) data from +the most recent `POST` navigation form submission or `undefined` if there +hasn't been one. ```tsx import { Form, useActionData } from "react-router"; @@ -35,5 +49,12 @@ export default function Invoices() { ## Signature ```tsx -useActionData(): undefined +function useActionData<T = any>(): SerializeFrom<T> | undefined ``` + +## Returns + +The data returned from the route's [`action`](../../start/framework/route-module#action) +function, or `undefined` if no [`action`](../../start/framework/route-module#action) +has been called + diff --git a/docs/api/hooks/useAsyncError.md b/docs/api/hooks/useAsyncError.md index 0fc171490b..77e0d34762 100644 --- a/docs/api/hooks/useAsyncError.md +++ b/docs/api/hooks/useAsyncError.md @@ -4,13 +4,25 @@ title: useAsyncError # useAsyncError +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useAsyncError.html) -Returns the rejection value from the closest [Await](../components/Await). +Returns the rejection value from the closest [`<Await>`](../components/Await). ```tsx import { Await, useAsyncError } from "react-router"; @@ -32,5 +44,10 @@ function ErrorElement() { ## Signature ```tsx -useAsyncError(): unknown +function useAsyncError(): unknown ``` + +## Returns + +The error that was thrown in the nearest [`Await`](../components/Await) component + diff --git a/docs/api/hooks/useAsyncValue.md b/docs/api/hooks/useAsyncValue.md index fc83879809..c6f15663a1 100644 --- a/docs/api/hooks/useAsyncValue.md +++ b/docs/api/hooks/useAsyncValue.md @@ -4,13 +4,25 @@ title: useAsyncValue # useAsyncValue -[MODES: framework] +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + +[MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useAsyncValue.html) -Returns the resolved promise value from the closest [Await](../components/Await). +Returns the resolved promise value from the closest [`<Await>`](../components/Await). ```tsx function SomeDescendant() { @@ -27,5 +39,10 @@ function SomeDescendant() { ## Signature ```tsx -useAsyncValue(): unknown +function useAsyncValue(): unknown ``` + +## Returns + +The resolved value from the nearest [`Await`](../components/Await) component + diff --git a/docs/api/hooks/useBeforeUnload.md b/docs/api/hooks/useBeforeUnload.md index 2b9aa4f635..dc43799cd1 100644 --- a/docs/api/hooks/useBeforeUnload.md +++ b/docs/api/hooks/useBeforeUnload.md @@ -4,30 +4,48 @@ title: useBeforeUnload # useBeforeUnload +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useBeforeUnload.html) -Setup a callback to be fired on the window's `beforeunload` event. +Set up a callback to be fired on [Window's `beforeunload` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event). ## Signature ```tsx -useBeforeUnload(callback, options): void +function useBeforeUnload( + callback: (event: BeforeUnloadEvent) => any, + options?: { + capture?: boolean; + }, +): void ``` ## Params ### callback -[modes: framework, data, declarative] +The callback to be called when the [`beforeunload` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) is fired. + +### options.capture -_No documentation_ +If `true`, the event will be captured during the capture phase. Defaults to `false`. -### options +## Returns -[modes: framework, data, declarative] +No return value. -_No documentation_ diff --git a/docs/api/hooks/useBlocker.md b/docs/api/hooks/useBlocker.md index 21297ac991..cf9945fab4 100644 --- a/docs/api/hooks/useBlocker.md +++ b/docs/api/hooks/useBlocker.md @@ -4,31 +4,78 @@ title: useBlocker # useBlocker +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useBlocker.html) -Allow the application to block navigations within the SPA and present the user a confirmation dialog to confirm the navigation. Mostly used to avoid using half-filled form data. This does not handle hard-reloads or cross-origin navigations. +Allow the application to block navigations within the SPA and present the +user a confirmation dialog to confirm the navigation. Mostly used to avoid +using half-filled form data. This does not handle hard-reloads or +cross-origin navigations. + +The [`Blocker`](https://api.reactrouter.com/v7/types/react_router.Blocker.html) object returned by the hook has the following properties: + +- **`state`** + - `unblocked` - the blocker is idle and has not prevented any navigation + - `blocked` - the blocker has prevented a navigation + - `proceeding` - the blocker is proceeding through from a blocked navigation +- **`location`** + - When in a `blocked` state, this represents the [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) to which + we blocked a navigation. When in a `proceeding` state, this is the + location being navigated to after a `blocker.proceed()` call. +- **`proceed()`** + - When in a `blocked` state, you may call `blocker.proceed()` to proceed to + the blocked location. +- **`reset()`** + - When in a `blocked` state, you may call `blocker.reset()` to return the + blocker to an `unblocked` state and leave the user at the current + location. + +```tsx +// Boolean version +let blocker = useBlocker(value !== ""); + +// Function version +let blocker = useBlocker( + ({ currentLocation, nextLocation, historyAction }) => + value !== "" && + currentLocation.pathname !== nextLocation.pathname +); +``` ## Signature ```tsx -useBlocker(shouldBlock): Blocker +function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker ``` ## Params ### shouldBlock -[modes: framework, data] +Either a boolean or a function returning a boolean which indicates whether the navigation should be blocked. The function format +receives a single object parameter containing the `currentLocation`, +`nextLocation`, and `historyAction` of the potential navigation. -_No documentation_ +## Returns -## Examples +A [`Blocker`](https://api.reactrouter.com/v7/types/react_router.Blocker.html) object with state and reset functionality -### Basic +## Examples ```tsx import { useCallback, useState } from "react"; @@ -92,3 +139,4 @@ export function ImportantForm() { ); } ``` + diff --git a/docs/api/hooks/useFetcher.md b/docs/api/hooks/useFetcher.md index d1b89dd424..64100c541c 100644 --- a/docs/api/hooks/useFetcher.md +++ b/docs/api/hooks/useFetcher.md @@ -4,15 +4,30 @@ title: useFetcher # useFetcher +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useFetcher.html) -Useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation. +Useful for creating complex, dynamic user interfaces that require multiple, +concurrent data interactions without causing a navigation. -Fetchers track their own, independent state and can be used to load data, submit forms, and generally interact with loaders and actions. +Fetchers track their own, independent state and can be used to load data, submit +forms, and generally interact with [`action`](../../start/framework/route-module#action) +and [`loader`](../../start/framework/route-module#loader) functions. ```tsx import { useFetcher } from "react-router" @@ -42,13 +57,38 @@ function SomeComponent() { ## Signature ```tsx -useFetcher(options): FetcherWithComponents +function useFetcher<T = any>({ + key, +}: { + key?: string; +} = ): FetcherWithComponents<SerializeFrom<T>> {} ``` ## Params -### options +### options.key + +A unique key to identify the fetcher. + +By default, `useFetcher` generates a unique fetcher scoped to that component. +If you want to identify a fetcher with your own key such that you can access +it from elsewhere in your app, you can do that with the `key` option: + +```tsx +function SomeComp() { + let fetcher = useFetcher({ key: "my-key" }) + // ... +} + +// Somewhere else +function AnotherComp() { + // this will be the same fetcher, sharing the state across the app + let fetcher = useFetcher({ key: "my-key" }); + // ... +} +``` + +## Returns -[modes: framework, data] +A [`FetcherWithComponents`](https://api.reactrouter.com/v7/types/react_router.FetcherWithComponents.html) object that contains the fetcher's state, data, and components for submitting forms and loading data. -_No documentation_ diff --git a/docs/api/hooks/useFetchers.md b/docs/api/hooks/useFetchers.md index e30bd2910f..3cd5a3544f 100644 --- a/docs/api/hooks/useFetchers.md +++ b/docs/api/hooks/useFetchers.md @@ -4,13 +4,27 @@ title: useFetchers # useFetchers +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useFetchers.html) -Returns an array of all in-flight fetchers. This is useful for components throughout the app that didn't create the fetchers but want to use their submissions to participate in optimistic UI. +Returns an array of all in-flight [`Fetcher`](https://api.reactrouter.com/v7/types/react_router.Fetcher.html)s. This is useful for components +throughout the app that didn't create the fetchers but want to use their submissions +to participate in optimistic UI. ```tsx import { useFetchers } from "react-router"; @@ -26,5 +40,13 @@ function SomeComponent() { ## Signature ```tsx -useFetchers(): undefined +function useFetchers(): (Fetcher & { + key: string; +})[] ``` + +## Returns + +An array of all in-flight [`Fetcher`](https://api.reactrouter.com/v7/types/react_router.Fetcher.html)s, each with a unique `key` +property. + diff --git a/docs/api/hooks/useFormAction.md b/docs/api/hooks/useFormAction.md index 4ff9ee2bed..46d2627468 100644 --- a/docs/api/hooks/useFormAction.md +++ b/docs/api/hooks/useFormAction.md @@ -4,15 +4,29 @@ title: useFormAction # useFormAction +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useFormAction.html) -Resolves the URL to the closest route in the component hierarchy instead of the current URL of the app. +Resolves the URL to the closest route in the component hierarchy instead of +the current URL of the app. -This is used internally by [Form](../components/Form) resolve the action to the closest route, but can be used generically as well. +This is used internally by [`Form`](../components/Form) to resolve the `action` to the closest +route, but can be used generically as well. ```tsx import { useFormAction } from "react-router"; @@ -29,19 +43,27 @@ function SomeComponent() { ## Signature ```tsx -useFormAction(action, __namedParameters): string +function useFormAction( + action?: string, + { + relative, + }: { + relative?: RelativeRoutingType; + } = , +): string {} ``` ## Params ### action -[modes: framework, data] +The action to append to the closest route URL. Defaults to the closest route URL. + +### options.relative -The action to append to the closest route URL. +The relative routing type to use when resolving the action. Defaults to `"route"`. -### \_\_namedParameters +## Returns -[modes: framework, data] +The resolved action URL. -_No documentation_ diff --git a/docs/api/hooks/useHref.md b/docs/api/hooks/useHref.md index 02b206b224..51d5458b54 100644 --- a/docs/api/hooks/useHref.md +++ b/docs/api/hooks/useHref.md @@ -4,13 +4,25 @@ title: useHref # useHref +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useHref.html) -Resolves a URL against the current location. +Resolves a URL against the current [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html). ```tsx import { useHref } from "react-router"; @@ -24,19 +36,28 @@ function SomeComponent() { ## Signature ```tsx -useHref(to, __namedParameters): string +function useHref( + to: To, + { + relative, + }: { + relative?: RelativeRoutingType; + } = , +): string {} ``` ## Params ### to -[modes: framework, data, declarative] +The path to resolve + +### options.relative -_No documentation_ +Defaults to `"route"` so routing is relative to the route tree. +Set to `"path"` to make relative routing operate against path segments. -### \_\_namedParameters +## Returns -[modes: framework, data, declarative] +The resolved href string -_No documentation_ diff --git a/docs/api/hooks/useInRouterContext.md b/docs/api/hooks/useInRouterContext.md index 6d0250effb..ef7d4edf42 100644 --- a/docs/api/hooks/useInRouterContext.md +++ b/docs/api/hooks/useInRouterContext.md @@ -4,17 +4,34 @@ title: useInRouterContext # useInRouterContext +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useInRouterContext.html) -Returns true if this component is a descendant of a Router, useful to ensure -a component is used within a Router. +Returns `true` if this component is a descendant of a [`Router`](../declarative-routers/Router), useful +to ensure a component is used within a [`Router`](../declarative-routers/Router). ## Signature ```tsx -useInRouterContext(): boolean +function useInRouterContext(): boolean ``` + +## Returns + +Whether the component is within a [`Router`](../declarative-routers/Router) context + diff --git a/docs/api/hooks/useLinkClickHandler.md b/docs/api/hooks/useLinkClickHandler.md index 9613ecc5fe..ca8996dd70 100644 --- a/docs/api/hooks/useLinkClickHandler.md +++ b/docs/api/hooks/useLinkClickHandler.md @@ -4,35 +4,84 @@ title: useLinkClickHandler # useLinkClickHandler +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useLinkClickHandler.html) -Handles the click behavior for router `<Link>` components. This is useful if -you need to create custom `<Link>` components with the same click behavior we -use in our exported `<Link>`. - - +Handles the click behavior for router [`<Link>`](../components/Link) components.This +is useful if you need to create custom [`<Link>`](../components/Link) components with +the same click behavior we use in our exported [`<Link>`](../components/Link). ## Signature ```tsx -useLinkClickHandler(to, __namedParameters): undefined +function useLinkClickHandler<E extends Element = HTMLAnchorElement>( + to: To, + { + target, + replace: replaceProp, + state, + preventScrollReset, + relative, + viewTransition, + }: { + target?: React.HTMLAttributeAnchorTarget; + replace?: boolean; + state?: any; + preventScrollReset?: boolean; + relative?: RelativeRoutingType; + viewTransition?: boolean; + } = , +): (event: React.MouseEvent<E, MouseEvent>) => void {} ``` ## Params ### to -[modes: framework, data, declarative] +The URL to navigate to, can be a string or a partial [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html). + +### options.preventScrollReset + +Whether to prevent the scroll position from being reset to the top of the viewport on completion of the navigation when +using the [`ScrollRestoration`](../components/ScrollRestoration) component. Defaults to `false`. + +### options.relative + +The [relative routing type](https://api.reactrouter.com/v7/types/react_router.RelativeRoutingType.html) to use for the link. Defaults to `"route"`. + +### options.replace + +Whether to replace the current [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) entry instead of pushing a new one. Defaults to `false`. + +### options.state + +The state to add to the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) entry for this navigation. Defaults to `undefined`. + +### options.target + +The target attribute for the link. Defaults to `undefined`. -_No documentation_ +### options.viewTransition -### __namedParameters +Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for this navigation. To apply specific styles during the transition, see +[`useViewTransitionState`](../hooks/useViewTransitionState). Defaults to `false`. -[modes: framework, data, declarative] +## Returns -_No documentation_ +A click handler function that can be used in a custom [`Link`](../components/Link) component. diff --git a/docs/api/hooks/useLoaderData.md b/docs/api/hooks/useLoaderData.md index be0f6d07ba..fdbd4a01ff 100644 --- a/docs/api/hooks/useLoaderData.md +++ b/docs/api/hooks/useLoaderData.md @@ -4,13 +4,27 @@ title: useLoaderData # useLoaderData +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useLoaderData.html) -Returns the data from the closest route [LoaderFunction](../Other/LoaderFunction) or [ClientLoaderFunction](../Other/ClientLoaderFunction). +Returns the data from the closest route +[`loader`](../../start/framework/route-module#loader) or +[`clientLoader`](../../start/framework/route-module#clientloader). ```tsx import { useLoaderData } from "react-router"; @@ -28,5 +42,10 @@ export default function Invoices() { ## Signature ```tsx -useLoaderData(): SerializeFrom +function useLoaderData<T = any>(): SerializeFrom<T> ``` + +## Returns + +The data returned from the route's [`loader`](../../start/framework/route-module#loader) or [`clientLoader`](../../start/framework/route-module#clientloader) function + diff --git a/docs/api/hooks/useLocation.md b/docs/api/hooks/useLocation.md index 5072be97b8..d9d46f05fd 100644 --- a/docs/api/hooks/useLocation.md +++ b/docs/api/hooks/useLocation.md @@ -4,13 +4,26 @@ title: useLocation # useLocation +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useLocation.html) -Returns the current [Location]([../Other/Location](https://api.reactrouter.com/v7/interfaces/react_router.Location.html)). This can be useful if you'd like to perform some side effect whenever it changes. +Returns the current [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html). This can be useful if you'd like to +perform some side effect whenever it changes. ```tsx import * as React from 'react' @@ -33,5 +46,10 @@ function SomeComponent() { ## Signature ```tsx -useLocation(): Location +function useLocation(): Location ``` + +## Returns + +The current [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) object + diff --git a/docs/api/hooks/useMatch.md b/docs/api/hooks/useMatch.md index 5667ef946a..bd59777fa6 100644 --- a/docs/api/hooks/useMatch.md +++ b/docs/api/hooks/useMatch.md @@ -4,29 +4,43 @@ title: useMatch # useMatch +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useMatch.html) -Returns a PathMatch object if the given pattern matches the current URL. +Returns a [`PathMatch`](https://api.reactrouter.com/v7/interfaces/react_router.PathMatch.html) object if the given pattern matches the current URL. This is useful for components that need to know "active" state, e.g. -`<NavLink>`. - - +[`<NavLink>`](../components/NavLink). ## Signature ```tsx -useMatch(pattern): undefined +function useMatch<ParamKey extends ParamParseKey<Path>, Path extends string>( + pattern: PathPattern<Path> | Path, +): PathMatch<ParamKey> | null ``` ## Params ### pattern -[modes: framework, data, declarative] +The pattern to match against the current [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) + +## Returns -_No documentation_ +The path match object if the pattern matches, `null` otherwise diff --git a/docs/api/hooks/useMatches.md b/docs/api/hooks/useMatches.md index af7bb17d00..fc5391e702 100644 --- a/docs/api/hooks/useMatches.md +++ b/docs/api/hooks/useMatches.md @@ -4,17 +4,35 @@ title: useMatches # useMatches +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useMatches.html) -Returns the active route matches, useful for accessing loaderData for -parent/child routes or the route "handle" property +Returns the active route matches, useful for accessing `loaderData` for +parent/child routes or the route [`handle`](../../start/framework/route-module#handle) +property ## Signature ```tsx -useMatches(): undefined +function useMatches(): UIMatch[] ``` + +## Returns + +An array of [UI matches](https://api.reactrouter.com/v7/interfaces/react_router.UIMatch.html) for the current route hierarchy + diff --git a/docs/api/hooks/useNavigate.md b/docs/api/hooks/useNavigate.md index 5d3b5ed721..973fd8e1c3 100644 --- a/docs/api/hooks/useNavigate.md +++ b/docs/api/hooks/useNavigate.md @@ -4,13 +4,40 @@ title: useNavigate # useNavigate +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useNavigate.html) -Returns a function that lets you navigate programmatically in the browser in response to user interactions or effects. +Returns a function that lets you navigate programmatically in the browser in +response to user interactions or effects. + +It's often better to use [`redirect`](../utils/redirect) in [`action`](../../start/framework/route-module#action)/[`loader`](../../start/framework/route-module#loader) +functions than this hook. + +The returned function signature is `navigate(to, options?)`/`navigate(delta)` where: + +* `to` can be a string path, a [`To`](https://api.reactrouter.com/v7/types/react_router.To.html) object, or a number (delta) +* `options` contains options for modifying the navigation + * `flushSync`: Wrap the DOM updates in [`ReactDom.flushSync`](https://react.dev/reference/react-dom/flushSync) + * `preventScrollReset`: Do not scroll back to the top of the page after navigation + * `relative`: `"route"` or `"path"` to control relative routing logic + * `replace`: Replace the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack + * `state`: Optional [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) to include with the new [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) + * `viewTransition`: Enable [`document.startViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for this navigation ```tsx import { useNavigate } from "react-router"; @@ -18,43 +45,33 @@ import { useNavigate } from "react-router"; function SomeComponent() { let navigate = useNavigate(); return ( - <button - onClick={() => { - navigate(-1); - }} - /> + <button onClick={() => navigate(-1)}> + Go Back + </button> ); } ``` -It's often better to use [redirect](../utils/redirect) in [ActionFunction](../Other/ActionFunction) and [LoaderFunction](../Other/LoaderFunction) than this hook. - ## Signature ```tsx -navigate( - to: To, - options?: { - flushSync?: boolean; - preventScrollReset?: boolean; - relative?: RelativeRoutingType; - replace?: boolean; - state?: any; - viewTransition?: boolean; - } -): void | Promise<void>; +function useNavigate(): NavigateFunction ``` +## Returns + +A navigate function for programmatic navigation + ## Examples -### Navigate to another path: +### Navigate to another path ```tsx navigate("/some/route"); navigate("/some/route?search=param"); ``` -### Navigate with a `To` object: +### Navigate with a [`To`](https://api.reactrouter.com/v7/types/react_router.To.html) object All properties are optional. @@ -67,9 +84,10 @@ navigate({ }); ``` -If you use `state`, that will be available on the `location` object on the next page. Access it with `useLocation().state` (see [useLocation](./useLocation)). +If you use `state`, that will be available on the [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) object on +the next page. Access it with `useLocation().state` (see [`useLocation`](../hooks/useLocation)). -### Navigate back or forward in the history stack: +### Navigate back or forward in the history stack ```tsx // back @@ -77,17 +95,23 @@ If you use `state`, that will be available on the `location` object on the next navigate(-1); // forward -// often used in a multi-step wizard workflows +// often used in a multistep wizard workflows navigate(1); ``` -Be cautions with `navigate(number)`. If your application can load up to a route that has a button that tries to navigate forward/back, there may not be a history entry to go back or forward to, or it can go somewhere you don't expect (like a different domain). +Be cautious with `navigate(number)`. If your application can load up to a +route that has a button that tries to navigate forward/back, there may not be +a `[`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +entry to go back or forward to, or it can go somewhere you don't expect +(like a different domain). -Only use this if you're sure they will have an entry in the history stack to navigate to. +Only use this if you're sure they will have an entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +stack to navigate to. -### Replace the current entry in the history stack: +### Replace the current entry in the history stack -This will remove the current entry in the history stack, replacing it with a new one, similar to a server side redirect. +This will remove the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +stack, replacing it with a new one, similar to a server side redirect. ```tsx navigate("/some/route", { replace: true }); @@ -95,12 +119,57 @@ navigate("/some/route", { replace: true }); ### Prevent Scroll Reset -[modes: framework, data] +[MODES: framework, data] + +<br/> +<br/> -To prevent `<ScrollRestoration>` from resetting the scroll position, use the `preventScrollReset` option. +To prevent [`<ScrollRestoration>`](../components/ScrollRestoration) from resetting +the scroll position, use the `preventScrollReset` option. ```tsx navigate("?some-tab=1", { preventScrollReset: true }); ``` -For example, if you have a tab interface connected to search params in the middle of a page and you don't want it to scroll to the top when a tab is clicked. +For example, if you have a tab interface connected to search params in the +middle of a page, and you don't want it to scroll to the top when a tab is +clicked. + +### Return Type Augmentation + +Internally, `useNavigate` uses a separate implementation when you are in +Declarative mode versus Data/Framework mode - the primary difference being +that the latter is able to return a stable reference that does not change +identity across navigations. The implementation in Data/Framework mode also +returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +that resolves when the navigation is completed. This means the return type of +`useNavigate` is `void | Promise<void>`. This is accurate, but can lead to +some red squigglies based on the union in the return value: + +- If you're using `typescript-eslint`, you may see errors from + [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises) +- In Framework/Data mode, `React.use(navigate())` will show a false-positive + `Argument of type 'void | Promise<void>' is not assignable to parameter of + type 'Usable<void>'` error + +The easiest way to work around these issues is to augment the type based on the +router you're using: + +```ts +// If using <BrowserRouter> +declare module "react-router" { + interface NavigateFunction { + (to: To, options?: NavigateOptions): void; + (delta: number): void; + } +} + +// If using <RouterProvider> or Framework mode +declare module "react-router" { + interface NavigateFunction { + (to: To, options?: NavigateOptions): Promise<void>; + (delta: number): Promise<void>; + } +} +``` + diff --git a/docs/api/hooks/useNavigation.md b/docs/api/hooks/useNavigation.md index 74c5e1563c..485ae734b0 100644 --- a/docs/api/hooks/useNavigation.md +++ b/docs/api/hooks/useNavigation.md @@ -4,13 +4,28 @@ title: useNavigation # useNavigation +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useNavigation.html) -Returns the current navigation, defaulting to an "idle" navigation when no navigation is in progress. You can use this to render pending UI (like a global spinner) or read FormData from a form navigation. +Returns the current [`Navigation`](https://api.reactrouter.com/v7/types/react_router.Navigation.html), defaulting to an "idle" navigation +when no navigation is in progress. You can use this to render pending UI +(like a global spinner) or read [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) +from a form navigation. ```tsx import { useNavigation } from "react-router"; @@ -26,5 +41,10 @@ function SomeComponent() { ## Signature ```tsx -useNavigation(): Navigation +function useNavigation(): Navigation ``` + +## Returns + +The current [`Navigation`](https://api.reactrouter.com/v7/types/react_router.Navigation.html) object + diff --git a/docs/api/hooks/useNavigationType.md b/docs/api/hooks/useNavigationType.md index 849d77617a..2a29b8084d 100644 --- a/docs/api/hooks/useNavigationType.md +++ b/docs/api/hooks/useNavigationType.md @@ -4,20 +4,35 @@ title: useNavigationType # useNavigationType +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useNavigationType.html) -Returns the current navigation action which describes how the router came to -the current location, either by a pop, push, or replace on the history stack. - - +Returns the current [`Navigation`](https://api.reactrouter.com/v7/types/react_router.Navigation.html) action which describes how the router +came to the current [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html), either by a pop, push, or replace on +the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack. ## Signature ```tsx -useNavigationType(): NavigationType +function useNavigationType(): NavigationType ``` +## Returns + +The current [`NavigationType`](https://api.reactrouter.com/v7/enums/react_router.NavigationType.html) (`"POP"`, `"PUSH"`, or `"REPLACE"`) + diff --git a/docs/api/hooks/useOutlet.md b/docs/api/hooks/useOutlet.md index a8f6b32a96..8601fc504a 100644 --- a/docs/api/hooks/useOutlet.md +++ b/docs/api/hooks/useOutlet.md @@ -4,6 +4,18 @@ title: useOutlet # useOutlet +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary @@ -11,18 +23,22 @@ title: useOutlet [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useOutlet.html) Returns the element for the child route at this level of the route -hierarchy. Used internally by `<Outlet>` to render child routes. +hierarchy. Used internally by [`<Outlet>`](../components/Outlet) to render child +routes. ## Signature ```tsx -useOutlet(context): undefined +function useOutlet(context?: unknown): React.ReactElement | null ``` ## Params ### context -[modes: framework, data, declarative] +The context to pass to the outlet + +## Returns + +The child route element or `null` if no child routes match -_No documentation_ diff --git a/docs/api/hooks/useOutletContext.md b/docs/api/hooks/useOutletContext.md index 82bc66dc0c..f7395cc7f3 100644 --- a/docs/api/hooks/useOutletContext.md +++ b/docs/api/hooks/useOutletContext.md @@ -4,19 +4,102 @@ title: useOutletContext # useOutletContext +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useOutletContext.html) -Returns the parent route `<Outlet context>`. +Returns the parent route [`<Outlet context>`](../components/Outlet). + +Often parent routes manage state or other values you want shared with child +routes. You can create your own [context provider](https://react.dev/learn/passing-data-deeply-with-context) +if you like, but this is such a common situation that it's built-into +[`<Outlet>`](../components/Outlet). + +```tsx +// Parent route +function Parent() { + const [count, setCount] = React.useState(0); + return <Outlet context={[count, setCount]} />; +} +``` + +```tsx +// Child route +import { useOutletContext } from "react-router"; + +function Child() { + const [count, setCount] = useOutletContext(); + const increment = () => setCount((c) => c + 1); + return <button onClick={increment}>{count}</button>; +} +``` + +If you're using TypeScript, we recommend the parent component provide a +custom hook for accessing the context value. This makes it easier for +consumers to get nice typings, control consumers, and know who's consuming +the context value. + +Here's a more realistic example: + +```tsx filename=src/routes/dashboard.tsx lines=[14,20] +import { useState } from "react"; +import { Outlet, useOutletContext } from "react-router"; + +import type { User } from "./types"; + +type ContextType = { user: User | null }; + +export default function Dashboard() { + const [user, setUser] = useState<User | null>(null); + return ( + <div> + <h1>Dashboard</h1> + <Outlet context={{ user } satisfies ContextType} /> + </div> + ); +} +export function useUser() { + return useOutletContext<ContextType>(); +} +``` + +```tsx filename=src/routes/dashboard/messages.tsx lines=[1,4] +import { useUser } from "../dashboard"; + +export default function DashboardMessages() { + const { user } = useUser(); + return ( + <div> + <h2>Messages</h2> + <p>Hello, {user.name}!</p> + </div> + ); +} +``` ## Signature ```tsx -useOutletContext(): Context +function useOutletContext<Context = unknown>(): Context ``` +## Returns + +The context value passed to the parent [`Outlet`](../components/Outlet) component + diff --git a/docs/api/hooks/useParams.md b/docs/api/hooks/useParams.md index a20d871701..fd5eea146e 100644 --- a/docs/api/hooks/useParams.md +++ b/docs/api/hooks/useParams.md @@ -4,13 +4,30 @@ title: useParams # useParams +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useParams.html) -Returns an object of key/value pairs of the dynamic params from the current URL that were matched by the routes. Child routes inherit all params from their parent routes. +Returns an object of key/value-pairs of the dynamic params from the current +URL that were matched by the routes. Child routes inherit all params from +their parent routes. + +Assuming a route pattern like `/posts/:postId` is matched by `/posts/123` +then `params.postId` will be `"123"`. ```tsx import { useParams } from "react-router"; @@ -21,7 +38,19 @@ function SomeComponent() { } ``` -Assuming a route pattern like `/posts/:postId` is matched by `/posts/123` then `params.postId` will be `"123"`. +## Signature + +```tsx +function useParams< + ParamsOrKey extends string | Record<string, string | undefined> = string, +>(): Readonly< + [ParamsOrKey] extends [string] ? Params<ParamsOrKey> : Partial<ParamsOrKey> +> +``` + +## Returns + +An object containing the dynamic route parameters ## Examples @@ -107,3 +136,4 @@ export default function File() { console.log(catchall); } ``` + diff --git a/docs/api/hooks/usePrompt.md b/docs/api/hooks/usePrompt.md new file mode 100644 index 0000000000..8ce621a0cc --- /dev/null +++ b/docs/api/hooks/usePrompt.md @@ -0,0 +1,95 @@ +--- +title: usePrompt +unstable: true +--- + +# unstable_usePrompt + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + +[MODES: framework, data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.unstable_usePrompt.html) + +Wrapper around [`useBlocker`](../hooks/useBlocker) to show a [`window.confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm) +prompt to users instead of building a custom UI with [`useBlocker`](../hooks/useBlocker). + +The `unstable_` flag will not be removed because this technique has a lot of +rough edges and behaves very differently (and incorrectly sometimes) across +browsers if users click addition back/forward navigations while the +confirmation is open. Use at your own risk. + +```tsx +function ImportantForm() { + let [value, setValue] = React.useState(""); + + // Block navigating elsewhere when data has been entered into the input + unstable_usePrompt({ + message: "Are you sure?", + when: ({ currentLocation, nextLocation }) => + value !== "" && + currentLocation.pathname !== nextLocation.pathname, + }); + + return ( + <Form method="post"> + <label> + Enter some important data: + <input + name="data" + value={value} + onChange={(e) => setValue(e.target.value)} + /> + </label> + <button type="submit">Save</button> + </Form> + ); +} +``` + +## Signature + +```tsx +function usePrompt({ + when, + message, +}: { + when: boolean | BlockerFunction; + message: string; +}): void +``` + +## Params + +### options.message + +The message to show in the confirmation dialog. + +### options.when + +A boolean or a function that returns a boolean indicating whether to block the navigation. If a function is provided, it will receive an +object with `currentLocation` and `nextLocation` properties. + +## Returns + +No return value. + diff --git a/docs/api/hooks/useResolvedPath.md b/docs/api/hooks/useResolvedPath.md index 672de2e050..bdd64b3563 100644 --- a/docs/api/hooks/useResolvedPath.md +++ b/docs/api/hooks/useResolvedPath.md @@ -4,13 +4,27 @@ title: useResolvedPath # useResolvedPath +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useResolvedPath.html) -Resolves the pathname of the given `to` value against the current location. Similar to [useHref](../hooks/useHref), but returns a [Path](../Other/Path) instead of a string. +Resolves the pathname of the given `to` value against the current +[`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html). Similar to [`useHref`](../hooks/useHref), but returns a +[`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html) instead of a string. ```tsx import { useResolvedPath } from "react-router"; @@ -27,19 +41,27 @@ function SomeComponent() { ## Signature ```tsx -useResolvedPath(to, __namedParameters): Path +function useResolvedPath( + to: To, + { + relative, + }: { + relative?: RelativeRoutingType; + } = , +): Path {} ``` ## Params ### to -[modes: framework, data, declarative] +The path to resolve + +### options.relative -_No documentation_ +Defaults to `"route"` so routing is relative to the route tree. Set to `"path"` to make relative routing operate against path segments. -### \_\_namedParameters +## Returns -[modes: framework, data, declarative] +The resolved [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html) object with `pathname`, `search`, and `hash` -_No documentation_ diff --git a/docs/api/hooks/useRevalidator.md b/docs/api/hooks/useRevalidator.md index 0f02ea7786..f37a1b29be 100644 --- a/docs/api/hooks/useRevalidator.md +++ b/docs/api/hooks/useRevalidator.md @@ -4,13 +4,33 @@ title: useRevalidator # useRevalidator +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useRevalidator.html) -Revalidate the data on the page for reasons outside of normal data mutations like window focus or polling on an interval. +Revalidate the data on the page for reasons outside of normal data mutations +like [`Window` focus](https://developer.mozilla.org/en-US/docs/Web/API/Window/focus_event) +or polling on an interval. + +Note that page data is already revalidated automatically after actions. +If you find yourself using this for normal CRUD operations on your data in +response to user interactions, you're probably not taking advantage of the +other APIs like [`useFetcher`](../hooks/useFetcher), [`Form`](../components/Form), [`useSubmit`](../hooks/useSubmit) that do +this automatically. ```tsx import { useRevalidator } from "react-router"; @@ -30,10 +50,17 @@ function WindowFocusRevalidator() { } ``` -Note that page data is already revalidated automatically after actions. If you find yourself using this for normal CRUD operations on your data in response to user interactions, you're probably not taking advantage of the other APIs like [useFetcher](../hooks/useFetcher), [Form](../components/Form), [useSubmit](../hooks/useSubmit) that do this automatically. - ## Signature ```tsx -useRevalidator(): undefined +function useRevalidator(): { + revalidate: () => Promise<void>; + state: DataRouter["state"]["revalidation"]; +} ``` + +## Returns + +An object with a `revalidate` function and the current revalidation +`state` + diff --git a/docs/api/hooks/useRouteError.md b/docs/api/hooks/useRouteError.md index 4c164230c6..861a3fb98a 100644 --- a/docs/api/hooks/useRouteError.md +++ b/docs/api/hooks/useRouteError.md @@ -4,13 +4,29 @@ title: useRouteError # useRouteError +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useRouteError.html) -Accesses the error thrown during an [ActionFunction](../Other/ActionFunction), [LoaderFunction](../Other/LoaderFunction), or component render to be used in a route module Error Boundary. +Accesses the error thrown during an +[`action`](../../start/framework/route-module#action), +[`loader`](../../start/framework/route-module#loader), +or component render to be used in a route module +[`ErrorBoundary`](../../start/framework/route-module#errorboundary). ```tsx export function ErrorBoundary() { @@ -22,5 +38,11 @@ export function ErrorBoundary() { ## Signature ```tsx -useRouteError(): unknown +function useRouteError(): unknown ``` + +## Returns + +The error that was thrown during route [loading](../../start/framework/route-module#loader), +[`action`](../../start/framework/route-module#action) execution, or rendering + diff --git a/docs/api/hooks/useRouteLoaderData.md b/docs/api/hooks/useRouteLoaderData.md index af0e5cc62d..b700496746 100644 --- a/docs/api/hooks/useRouteLoaderData.md +++ b/docs/api/hooks/useRouteLoaderData.md @@ -4,23 +4,29 @@ title: useRouteLoaderData # useRouteLoaderData +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useRouteLoaderData.html) -Returns the loader data for a given route by route ID. +Returns the [`loader`](../../start/framework/route-module#loader) data for a +given route by route ID. -```tsx -import { useRouteLoaderData } from "react-router"; - -function SomeComponent() { - const { user } = useRouteLoaderData("root"); -} -``` - -Route IDs are created automatically. They are simply the path of the route file relative to the app folder without the extension. +Route IDs are created automatically. They are simply the path of the route file +relative to the app folder without the extension. | Route Filename | Route ID | | ---------------------------- | ---------------------- | @@ -28,22 +34,34 @@ Route IDs are created automatically. They are simply the path of the route file | `app/routes/teams.tsx` | `"routes/teams"` | | `app/whatever/teams.$id.tsx` | `"whatever/teams.$id"` | -If you created an ID manually, you can use that instead: - ```tsx -route("/", "containers/app.tsx", { id: "app" }}) +import { useRouteLoaderData } from "react-router"; + +function SomeComponent() { + const { user } = useRouteLoaderData("root"); +} + +// You can also specify your own route ID's manually in your routes.ts file: +route("/", "containers/app.tsx", { id: "app" }) +useRouteLoaderData("app"); ``` ## Signature ```tsx -useRouteLoaderData(routeId): undefined +function useRouteLoaderData<T = any>( + routeId: string, +): SerializeFrom<T> | undefined ``` ## Params ### routeId -[modes: framework, data] +The ID of the route to return loader data from + +## Returns + +The data returned from the specified route's [`loader`](../../start/framework/route-module#loader) +function, or `undefined` if not found -_No documentation_ diff --git a/docs/api/hooks/useRoutes.md b/docs/api/hooks/useRoutes.md index 6ad39ef296..5bc71198ab 100644 --- a/docs/api/hooks/useRoutes.md +++ b/docs/api/hooks/useRoutes.md @@ -4,18 +4,30 @@ title: useRoutes # useRoutes +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useRoutes.html) -Hook version of [Routes](../components/Routes) that uses objects instead of components. These objects have the same properties as the component props. - -The return value of `useRoutes` is either a valid React element you can use to render the route tree, or `null` if nothing matched. +Hook version of [`<Routes>`](../components/Routes) that uses objects instead of +components. These objects have the same properties as the component props. +The return value of `useRoutes` is either a valid React element you can use +to render the route tree, or `null` if nothing matched. ```tsx -import * as React from "react"; import { useRoutes } from "react-router"; function App() { @@ -41,19 +53,23 @@ function App() { ## Signature ```tsx -useRoutes(routes, locationArg): undefined +function useRoutes( + routes: RouteObject[], + locationArg?: Partial<Location> | string, +): React.ReactElement | null ``` ## Params ### routes -[modes: framework, data, declarative] - -_No documentation_ +An array of [`RouteObject`](https://api.reactrouter.com/v7/types/react_router.RouteObject.html)s that define the route hierarchy ### locationArg -[modes: framework, data, declarative] +An optional [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) object or pathname string to use instead of the current [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) + +## Returns + +A React element to render the matched route, or `null` if no routes matched -_No documentation_ diff --git a/docs/api/hooks/useSearchParams.md b/docs/api/hooks/useSearchParams.md index 3ccf95c4d6..e5d7a676f2 100644 --- a/docs/api/hooks/useSearchParams.md +++ b/docs/api/hooks/useSearchParams.md @@ -4,13 +4,26 @@ title: useSearchParams # useSearchParams +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useSearchParams.html) -Returns a tuple of the current URL's URLSearchParams and a function to update them. Setting the search params causes a navigation. +Returns a tuple of the current URL's [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) +and a function to update them. Setting the search params causes a navigation. ```tsx import { useSearchParams } from "react-router"; @@ -21,40 +34,11 @@ export function SomeComponent() { } ``` -## Signature - -```tsx -useSearchParams(defaultInit): undefined -``` - -## Params - -### defaultInit - -[modes: framework, data, declarative] - -You can initialize the search params with a default value, though it **will not** change the URL on the first render. - -```tsx -// a search param string -useSearchParams("?tab=1"); - -// a short-hand object -useSearchParams({ tab: "1" }); - -// object keys can be arrays for multiple values on the key -useSearchParams({ brand: ["nike", "reebok"] }); - -// an array of tuples -useSearchParams([["tab", "1"]]); - -// a URLSearchParams object -useSearchParams(new URLSearchParams("?tab=1")); -``` - -## SetSearchParams Function +### `setSearchParams` function -The second element of the tuple is a function that can be used to update the search params. It accepts the same types as `defaultInit` and will cause a navigation to the new URL. +The second element of the tuple is a function that can be used to update the +search params. It accepts the same types as `defaultInit` and will cause a +navigation to the new URL. ```tsx let [searchParams, setSearchParams] = useSearchParams(); @@ -62,7 +46,7 @@ let [searchParams, setSearchParams] = useSearchParams(); // a search param string setSearchParams("?tab=1"); -// a short-hand object +// a shorthand object setSearchParams({ tab: "1" }); // object keys can be arrays for multiple values on the key @@ -71,11 +55,12 @@ setSearchParams({ brand: ["nike", "reebok"] }); // an array of tuples setSearchParams([["tab", "1"]]); -// a URLSearchParams object +// a `URLSearchParams` object setSearchParams(new URLSearchParams("?tab=1")); ``` -It also supports a function callback like `setState`: +It also supports a function callback like React's +[`setState`](https://react.dev/reference/react/useState#setstate): ```tsx setSearchParams((searchParams) => { @@ -84,9 +69,17 @@ setSearchParams((searchParams) => { }); ``` -## Notes +<docs-warning>The function callback version of `setSearchParams` does not support +the [queueing](https://react.dev/reference/react/useState#setstate-parameters) +logic that React's `setState` implements. Multiple calls to `setSearchParams` +in the same tick will not build on the prior value. If you need this behavior, +you can use `setState` manually.</docs-warning> -Note that `searchParams` is a stable reference, so you can reliably use it as a dependency in `useEffect` hooks. +### Notes + +Note that `searchParams` is a stable reference, so you can reliably use it +as a dependency in React's [`useEffect`](https://react.dev/reference/react/useEffect) +hooks. ```tsx useEffect(() => { @@ -94,4 +87,45 @@ useEffect(() => { }, [searchParams]); ``` -However, this also means it's mutable. If you change the object without calling `setSearchParams`, its values will change between renders if some other state causes the component to re-render and URL will not reflect the values. +However, this also means it's mutable. If you change the object without +calling `setSearchParams`, its values will change between renders if some +other state causes the component to re-render and URL will not reflect the +values. + +## Signature + +```tsx +function useSearchParams( + defaultInit?: URLSearchParamsInit, +): [URLSearchParams, SetURLSearchParams] +``` + +## Params + +### defaultInit + +You can initialize the search params with a default value, though it **will +not** change the URL on the first render. + +```tsx +// a search param string +useSearchParams("?tab=1"); + +// a shorthand object +useSearchParams({ tab: "1" }); + +// object keys can be arrays for multiple values on the key +useSearchParams({ brand: ["nike", "reebok"] }); + +// an array of tuples +useSearchParams([["tab", "1"]]); + +// a `URLSearchParams` object +useSearchParams(new URLSearchParams("?tab=1")); +``` + +## Returns + +A tuple of the current [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) +and a function to update them. + diff --git a/docs/api/hooks/useSubmit.md b/docs/api/hooks/useSubmit.md index fc83daa6a6..5f4a5828d2 100644 --- a/docs/api/hooks/useSubmit.md +++ b/docs/api/hooks/useSubmit.md @@ -4,13 +4,26 @@ title: useSubmit # useSubmit +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useSubmit.html) -The imperative version of [Form](../components/Form) that lets you submit a form from code instead of a user interaction. +The imperative version of [`<Form>`](../components/Form) that lets you submit a form +from code instead of a user interaction. ```tsx import { useSubmit } from "react-router"; @@ -18,11 +31,7 @@ import { useSubmit } from "react-router"; function SomeComponent() { const submit = useSubmit(); return ( - <Form - onChange={(event) => { - submit(event.currentTarget); - }} - /> + <Form onChange={(event) => submit(event.currentTarget)} /> ); } ``` @@ -30,5 +39,10 @@ function SomeComponent() { ## Signature ```tsx -useSubmit(): SubmitFunction +function useSubmit(): SubmitFunction ``` + +## Returns + +A function that can be called to submit a [`Form`](../components/Form) imperatively. + diff --git a/docs/api/hooks/useViewTransitionState.md b/docs/api/hooks/useViewTransitionState.md index deb6e8f92c..9f01a156ee 100644 --- a/docs/api/hooks/useViewTransitionState.md +++ b/docs/api/hooks/useViewTransitionState.md @@ -4,30 +4,56 @@ title: useViewTransitionState # useViewTransitionState +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.useViewTransitionState.html) -This hook returns `true` when there is an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) to the specified location. This can be used to apply finer-grained styles to elements to further customize the view transition. This requires that view transitions have been enabled for the given navigation via LinkProps.viewTransition (or the `Form`, `submit`, or `navigate` call) +This hook returns `true` when there is an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) +to the specified location. This can be used to apply finer-grained styles to +elements to further customize the view transition. This requires that view +transitions have been enabled for the given navigation via [`LinkProps.viewTransition`](https://api.reactrouter.com/v7/interfaces/react_router.LinkProps.html#viewTransition) +(or the `Form`, `submit`, or `navigate` call) ## Signature ```tsx -useViewTransitionState(to, opts): boolean +function useViewTransitionState( + to: To, + { + relative, + }: { + relative?: RelativeRoutingType; + } = , +) {} ``` ## Params ### to -[modes: framework, data, declarative] +The [`To`](https://api.reactrouter.com/v7/types/react_router.To.html) location to check for an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API). + +### options.relative -_No documentation_ +The relative routing type to use when resolving the `to` location, defaults to `"route"`. See [`RelativeRoutingType`](https://api.reactrouter.com/v7/types/react_router.RelativeRoutingType.html) for +more details. -### opts +## Returns -[modes: framework, data, declarative] +`true` if there is an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) +to the specified [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html), otherwise `false`. -_No documentation_ diff --git a/docs/api/other-api/adapter.md b/docs/api/other-api/adapter.md new file mode 100644 index 0000000000..b69469f535 --- /dev/null +++ b/docs/api/other-api/adapter.md @@ -0,0 +1,170 @@ +--- +title: "@react-router/{adapter}" +--- + +# Server Adapters + +## Official Adapters + +Idiomatic React Router apps can generally be deployed anywhere because React Router adapts the server's request/response to the [Web Fetch API][web-fetch-api]. It does this through adapters. We maintain a few adapters: + +- `@react-router/architect` +- `@react-router/cloudflare` +- `@react-router/express` + +These adapters are imported into your server's entry and are not used inside your React Router app itself. + +If you initialized your app with `npx create-react-router@latest` with something other than the built-in [React Router App Server][rr-serve] (`@react-router/serve`), you will note a `server/index.js` file that imports and uses one of these adapters. + +<docs-info>If you're using the built-in React Router App Server, you don't interact with this API</docs-info> + +Each adapter has the same API. In the future, we may have helpers specific to the platform you're deploying to. + +## `@react-router/express` + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/modules/_react_router_express.html) + +Here's an example with [Express][express]: + +```ts lines=[1-3,11-22] +const { + createRequestHandler, +} = require("@react-router/express"); +const express = require("express"); + +const app = express(); + +// needs to handle all verbs (GET, POST, etc.) +app.all( + "*", + createRequestHandler({ + // `react-router build` and `react-router dev` output files to a build directory, + // you need to pass that build to the request handler + build: require("./build"), + + // Return anything you want here to be available as `context` in your + // loaders and actions. This is where you can bridge the gap between your + // server and React Router + getLoadContext(req, res) { + return {}; + }, + }), +); +``` + +### Migrating from the React Router App Server + +If you started an app with the [React Router App Server][rr-serve] but find that you want to take control over the Express server and customize it, it should be fairly straightforward to migrate way from `@react-router/serve`. + +You can refer to the [Express template][express-template] as a reference, but here are the main changes you will need to make: + +**1. Update deps** + +```shellscript nonumber +npm uninstall @react-router/serve +npm install @react-router/express compression express morgan cross-env +npm install --save-dev @types/express @types/express-serve-static-core @types/morgan +``` + +**2. Add a server** + +Create your React Router Express server in `server/app.ts`: + +```ts filename=server/app.ts +import "react-router"; +import { createRequestHandler } from "@react-router/express"; +import express from "express"; + +export const app = express(); + +app.use( + createRequestHandler({ + build: () => + import("virtual:react-router/server-build"), + }), +); +``` + +Copy the [`server.js`][express-template-server-js] into your app. This is the boilerplate setup we recommend to allow the same server code to run both the development and production builds of your app. Two separate files are used here so that the main Express server code can be written in TypeScript (`server/app.ts`) and compiled into your server build by React Router, and then executed via `node server.js`. + +**3. Update `vite.config.ts` to compile the server** + +```tsx filename=vite.config.ts lines=[6-10] +import { reactRouter } from "@react-router/dev/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ isSsrBuild }) => ({ + build: { + rollupOptions: isSsrBuild + ? { input: "./server/app.ts" } + : undefined, + }, + plugins: [reactRouter(), tsconfigPaths()], +})); +``` + +**4. Update `package.json` scripts** + +Update the `dev` and `start` scripts to use your new Express server: + +```json filename=package.json +{ + // ... + "scripts": { + "dev": "cross-env NODE_ENV=development node server.js", + "start": "node server.js" + // ... + } + // ... +} +``` + +## `@react-router/cloudflare` + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/modules/_react_router_cloudflare.html) + +Here's an example with Cloudflare: + +```ts +import { createRequestHandler } from "react-router"; + +declare module "react-router" { + export interface AppLoadContext { + cloudflare: { + env: Env; + ctx: ExecutionContext; + }; + } +} + +const requestHandler = createRequestHandler( + () => import("virtual:react-router/server-build"), + import.meta.env.MODE, +); + +export default { + async fetch(request, env, ctx) { + return requestHandler(request, { + cloudflare: { env, ctx }, + }); + }, +} satisfies ExportedHandler<Env>; +``` + +## `@react-router/node` + +While not a direct "adapter" like the above, this package contains utilities for working with Node-based adapters. + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/modules/_react_router_node.html) + +### Node Version Support + +React Router officially supports **Active** and **Maintenance** [Node LTS versions][node-releases] at any given point in time. Dropped support for End of Life Node versions is done in a React Router Minor release. + +[express]: https://expressjs.com +[node-releases]: https://nodejs.org/en/about/previous-releases +[web-fetch-api]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API +[rr-serve]: ./serve +[express-template]: https://github.com/remix-run/react-router-templates/tree/main/node-custom-server +[express-template-server-js]: https://github.com/remix-run/react-router-templates/blob/main/node-custom-server/server.js diff --git a/docs/api/other-api/dev.md b/docs/api/other-api/dev.md new file mode 100644 index 0000000000..f21bdcd955 --- /dev/null +++ b/docs/api/other-api/dev.md @@ -0,0 +1,127 @@ +--- +title: "@react-router/dev (CLI)" +--- + +# React Router CLI + +The React Router CLI comes from the `@react-router/dev` package. Make sure it is in your `package.json` `devDependencies` so it doesn't get deployed to your server. + +To get a full list of available commands and flags, run: + +```shellscript nonumber +npx @react-router/dev -h +``` + +## `react-router build` + +Builds your app for production with [Vite][vite]. This command will set `process.env.NODE_ENV` to `production` and minify the output for deployment. + +```shellscript nonumber +react-router build +``` + +| Flag | Description | Type | Default | +| --------------------- | ------------------------------------------------------- | --------------------------------------------------- | ----------- | +| `--assetsInlineLimit` | Static asset base64 inline threshold in bytes | `number` | `4096` | +| `--clearScreen` | Allow/disable clear screen when logging | `boolean` | | +| `--config`, `-c` | Use specified config file | `string` | | +| `--emptyOutDir` | Force empty outDir when it's outside of root | `boolean` | | +| `--logLevel`, `-l` | Use specified log level | `"info" \| "warn" \| "error" \| "silent" \| string` | | +| `--minify` | Enable/disable minification, or specify minifier to use | `boolean \| "terser" \| "esbuild"` | `"esbuild"` | +| `--mode`, `-m` | Set env mode | `string` | | +| `--profile` | Start built-in Node.js inspector | | | +| `--sourcemapClient` | Output source maps for client build | `boolean \| "inline" \| "hidden"` | `false` | +| `--sourcemapServer` | Output source maps for server build | `boolean \| "inline" \| "hidden"` | `false` | + +## `react-router dev` + +Runs your app in development mode with HMR and Hot Data Revalidation (HDR), powered by [Vite][vite]. + +```shellscript nonumber +react-router dev +``` + +<docs-info> + +What is "Hot Data Revalidation"? + +Like HMR, HDR is a way of hot updating your app without needing to refresh the page. +That way you can keep your app state as your edits are applied in your app. +HMR handles client-side code updates like when you change the components, markup, or styles in your app. +Likewise, HDR handles server-side code updates. + +That means any time you make a change to the current page (or any code that your current page depends on), React Router will re-fetch data from your [loaders][loaders]. +That way your app is _always_ up to date with the latest code changes, client-side or server-side. + +</docs-info> + +| Flag | Description | Type | Default | +| ------------------ | ----------------------------------------------------- | --------------------------------------------------- | ------- | +| `--clearScreen` | Allow/disable clear screen when logging | `boolean` | | +| `--config`, `-c` | Use specified config file | `string` | | +| `--cors` | Enable CORS | `boolean` | | +| `--force` | Force the optimizer to ignore the cache and re-bundle | `boolean` | | +| `--host` | Specify hostname | `string` | | +| `--logLevel`, `-l` | Use specified log level | `"info" \| "warn" \| "error" \| "silent" \| string` | | +| `--mode`, `-m` | Set env mode | `string` | | +| `--open` | Open browser on startup | `boolean \| string` | | +| `--port` | Specify port | `number` | | +| `--profile` | Start built-in Node.js inspector | | | +| `--strictPort` | Exit if specified port is already in use | `boolean` | | + +## `react-router reveal` + +React Router handles the entry points of your application by default. + +If you want to have control over these entry points, you can run `npx react-router reveal` to generate the [`entry.client.tsx`][entry-client] and [`entry.server.tsx`][entry-server] files in your `app` directory. When these files are present, React Router will use them instead of the defaults. + +```shellscript nonumber +npx react-router reveal +``` + +| Flag | Description | Type | Default | +| ----------------- | ------------------------------- | --------- | ------- | +| `--config`, `-c` | Use specified config file | `string` | | +| `--mode`, `-m` | Set env mode | `string` | | +| `--no-typescript` | Generate plain JavaScript files | `boolean` | `false` | +| `--typescript` | Generate TypeScript files | `boolean` | `true` | + +## `react-router routes` + +Prints the routes in your app to the terminal. + +```shellscript nonumber +react-router routes +``` + +Your route tree will be in a JSX format by default. You can also use the `--json` flag to get the routes in a JSON format. + +```shellscript nonumber +react-router routes --json +``` + +| Flag | Description | Type | Default | +| ---------------- | ---------------------------- | --------- | ------- | +| `--config`, `-c` | Use specified config file | `string` | | +| `--json` | Output routes in JSON format | `boolean` | `false` | +| `--mode`, `-m` | Set env mode | `string` | | + +## `react-router typegen` + +Generates TypeScript types for your routes. This happens automatically during development, but you can manually run it when needed, e.g., to generate types in CI before running `tsc`. See [Type Safety][type-safety] for more information. + +```shellscript nonumber +react-router typegen +``` + +| Flag | Description | Type | Default | +| ---------------- | ------------------------- | --------- | ------- | +| `--config`, `-c` | Use specified config file | `string` | | +| `--mode`, `-m` | Set env mode | `string` | | +| `--watch` | Watch for changes | `boolean` | `false` | + +[loaders]: ../../start/framework/data-loading +[vite]: https://vite.dev +[entry-server]: ../framework-conventions/entry.server.tsx +[entry-client]: ../framework-conventions/entry.client.tsx +[type-safety]: ../../explanation/type-safety diff --git a/docs/api/other-api/index.md b/docs/api/other-api/index.md new file mode 100644 index 0000000000..055fa5fb8c --- /dev/null +++ b/docs/api/other-api/index.md @@ -0,0 +1,4 @@ +--- +title: Other API +order: 9 +--- diff --git a/docs/api/other-api/serve.md b/docs/api/other-api/serve.md new file mode 100644 index 0000000000..be5a106b57 --- /dev/null +++ b/docs/api/other-api/serve.md @@ -0,0 +1,99 @@ +--- +title: "@react-router/serve" +--- + +# React Router App Server + +React Router is designed for you to own your server, but if you don't want to set one up, you can use the React Router App Server instead. It's a production-ready but basic Node.js server built with [Express][express]. + +By design, we do not provide options to customize the React Router App Server because if you need to customize the underlying `express` server, we'd rather you manage the server completely instead of creating an abstraction to handle all the possible customizations you may require. If you find you want to customize it, you can [migrate to the `@react-router/express` adapter][migrate-to-express]. + +You can see the underlying `express` server configuration in [packages/react-router-serve/cli.ts][rr-serve-code]. By default, it uses the following Express middlewares (please refer to their documentation for default behaviors): + +- [`compression`][compression] +- [`express.static`][express-static] (and thus [`serve-static`][serve-static]) +- [`morgan`][morgan] + +## `HOST` environment variable + +You can configure the hostname for your Express app via `process.env.HOST` and that value will be passed to the internal [`app.listen`][express-listen] method when starting the server. + +```shellscript nonumber +HOST=127.0.0.1 npx react-router-serve build/index.js +``` + +```shellscript nonumber +react-router-serve <server-build-path> +# e.g. +react-router-serve build/index.js +``` + +## `PORT` environment variable + +You can change the port of the server with an environment variable. + +```shellscript nonumber +PORT=4000 npx react-router-serve build/index.js +``` + +## Development Environment + +Depending on `process.env.NODE_ENV`, the server will boot in development or production mode. + +The `server-build-path` needs to point to the `serverBuildPath` defined in [`react-router.config.ts`][rr-config]. + +Because only the build artifacts (`build/`, `public/build/`) need to be deployed to production, the `react-router.config.ts` is not guaranteed to be available in production, so you need to tell React Router where your server build is with this option. + +In development, `react-router-serve` will ensure the latest code is run by purging the `require` cache for every request. This has some effects on your code you might need to be aware of: + +- Any values in the module scope will be "reset" + + ```tsx lines=[1-3] + // this will be reset for every request because the module cache was + // cleared and this will be required brand new + const cache = new Map(); + + export async function loader({ + params, + }: Route.LoaderArgs) { + if (cache.has(params.foo)) { + return cache.get(params.foo); + } + + const record = await fakeDb.stuff.find(params.foo); + cache.set(params.foo, record); + return record; + } + ``` + + If you need a workaround for preserving cache in development, you can set up a singleton in your server. + +- Any **module side effects** will remain in place! This may cause problems but should probably be avoided anyway. + + ```tsx lines=[1-4] + // this starts running the moment the module is imported + setInterval(() => { + console.log(Date.now()); + }, 1000); + + export async function loader() { + // ... + } + ``` + + If you need to write your code in a way that has these types of module side effects, you should set up your own [@react-router/express][rr-express] server and a tool in development like [`pm2-dev`][pm2-dev] or [`nodemon`][nodemon] to restart the server on file changes instead. + +In production, this doesn't happen. The server boots up, and that's the end of it. + +[rr-express]: ./adapter#react-routerexpress +[express-listen]: https://expressjs.com/en/api.html#app.listen +[rr-config]: ../framework-conventions/react-router.config.ts +[rr-serve-code]: https://github.com/remix-run/react-router/blob/main/packages/react-router-serve/cli.ts +[compression]: https://expressjs.com/en/resources/middleware/compression.html +[express-static]: https://expressjs.com/en/4x/api.html#express.static +[serve-static]: https://expressjs.com/en/resources/middleware/serve-static.html +[morgan]: https://expressjs.com/en/resources/middleware/morgan.html +[express]: https://expressjs.com +[migrate-to-express]: ./adapter#migrating-from-the-react-router-app-server +[pm2-dev]: https://npm.im/pm2-dev +[nodemon]: https://npm.im/nodemon diff --git a/docs/api/rsc/RSCHydratedRouter.md b/docs/api/rsc/RSCHydratedRouter.md new file mode 100644 index 0000000000..4a21571ab8 --- /dev/null +++ b/docs/api/rsc/RSCHydratedRouter.md @@ -0,0 +1,99 @@ +--- +title: RSCHydratedRouter +unstable: true +--- + +# unstable_RSCHydratedRouter + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/browser.tsx +--> + +[MODES: data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.unstable_RSCHydratedRouter.html) + +Hydrates a server rendered [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react_router.unstable_RSCPayload.html) in the browser. + +```tsx +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { + unstable_getRSCStream as getRSCStream, + unstable_RSCHydratedRouter as RSCHydratedRouter, +} from "react-router"; +import type { unstable_RSCPayload as RSCPayload } from "react-router"; + +createFromReadableStream(getRSCStream()).then((payload) => + startTransition(async () => { + hydrateRoot( + document, + <StrictMode> + <RSCHydratedRouter + createFromReadableStream={createFromReadableStream} + payload={payload} + /> + </StrictMode>, + { formState: await getFormState(payload) }, + ); + }), +); +``` + +## Signature + +```tsx +function RSCHydratedRouter({ + createFromReadableStream, + fetch: fetchImplementation = fetch, + payload, + routeDiscovery = "eager", + unstable_getContext, +}: RSCHydratedRouterProps) +``` + +## Props + +### createFromReadableStream + +Your `react-server-dom-xyz/client`'s `createFromReadableStream` function, +used to decode payloads from the server. + +### fetch + +Optional fetch implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch). + +### unstable_getContext + +A function that returns an [`unstable_RouterContextProvider`](../utils/RouterContextProvider) instance +which is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s, +[`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware). +This function is called to generate a fresh `context` instance on each +navigation or fetcher call. + +### payload + +The decoded [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react_router.unstable_RSCPayload.html) to hydrate. + +### routeDiscovery + +`"eager"` or `"lazy"` - Determines if links are eagerly discovered, or +delayed until clicked. + diff --git a/docs/api/rsc/RSCStaticRouter.md b/docs/api/rsc/RSCStaticRouter.md new file mode 100644 index 0000000000..18a7fdc2f1 --- /dev/null +++ b/docs/api/rsc/RSCStaticRouter.md @@ -0,0 +1,74 @@ +--- +title: RSCStaticRouter +unstable: true +--- + +# unstable_RSCStaticRouter + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/server.ssr.tsx +--> + +[MODES: data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.unstable_RSCStaticRouter.html) + +Pre-renders an [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react_router.unstable_RSCPayload.html) to HTML. Usually used in +[`unstable_routeRSCServerRequest`](../rsc/routeRSCServerRequest)'s `renderHTML` callback. + +```tsx +import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; +import * as ReactDomServer from "react-dom/server.edge"; +import { + unstable_RSCStaticRouter as RSCStaticRouter, + unstable_routeRSCServerRequest as routeRSCServerRequest, +} from "react-router"; + +routeRSCServerRequest({ + request, + fetchServer, + createFromReadableStream, + async renderHTML(getPayload) { + const payload = await getPayload(); + + return await renderHTMLToReadableStream( + <RSCStaticRouter getPayload={getPayload} />, + { + bootstrapScriptContent, + formState: await getFormState(payload), + } + ); + }, +}); +``` + +## Signature + +```tsx +function RSCStaticRouter({ getPayload }: RSCStaticRouterProps) +``` + +## Props + +### getPayload + +A function that starts decoding of the [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react_router.unstable_RSCPayload.html). Usually passed +through from [`unstable_routeRSCServerRequest`](../rsc/routeRSCServerRequest)'s `renderHTML`. + diff --git a/docs/api/rsc/createCallServer.md b/docs/api/rsc/createCallServer.md new file mode 100644 index 0000000000..4ae506c9f5 --- /dev/null +++ b/docs/api/rsc/createCallServer.md @@ -0,0 +1,91 @@ +--- +title: createCallServer +unstable: true +--- + +# unstable_createCallServer + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/browser.tsx +--> + +[MODES: data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.unstable_createCallServer.html) + +Create a React `callServer` implementation for React Router. + +```tsx +import { + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, +} from "@vitejs/plugin-rsc/browser"; +import { unstable_createCallServer as createCallServer } from "react-router"; + +setServerCallback( + createCallServer({ + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + }) +); +``` + +## Signature + +```tsx +function createCallServer({ + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + fetch: fetchImplementation = fetch, +}: { + createFromReadableStream: BrowserCreateFromReadableStreamFunction; + createTemporaryReferenceSet: () => unknown; + encodeReply: EncodeReplyFunction; + fetch?: (request: Request) => Promise<Response>; +}) +``` + +## Params + +### opts.createFromReadableStream + +Your `react-server-dom-xyz/client`'s `createFromReadableStream`. Used to decode payloads from the server. + +### opts.createTemporaryReferenceSet + +A function that creates a temporary reference set for the [RSC](https://react.dev/reference/rsc/server-components) +payload. + +### opts.encodeReply + +Your `react-server-dom-xyz/client`'s `encodeReply`. Used when sending payloads to the server. + +### opts.fetch + +Optional [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch). + +## Returns + +A function that can be used to call server actions. + diff --git a/docs/api/rsc/getRSCStream.md b/docs/api/rsc/getRSCStream.md new file mode 100644 index 0000000000..71a38a7ac9 --- /dev/null +++ b/docs/api/rsc/getRSCStream.md @@ -0,0 +1,74 @@ +--- +title: getRSCStream +unstable: true +--- + +# unstable_getRSCStream + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/html-stream/browser.ts +--> + +[MODES: data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.unstable_getRSCStream.html) + +Get the prerendered [RSC](https://react.dev/reference/rsc/server-components) +stream for hydration. Usually passed directly to your +`react-server-dom-xyz/client`'s `createFromReadableStream`. + +```tsx +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { + unstable_getRSCStream as getRSCStream, + unstable_RSCHydratedRouter as RSCHydratedRouter, +} from "react-router"; +import type { unstable_RSCPayload as RSCPayload } from "react-router"; + +createFromReadableStream(getRSCStream()).then( + (payload: RSCServerPayload) => { + startTransition(async () => { + hydrateRoot( + document, + <StrictMode> + <RSCHydratedRouter {...props} /> + </StrictMode>, + { + // Options + } + ); + }); + } +); +``` + +## Signature + +```tsx +function getRSCStream(): ReadableStream +``` + +## Returns + +A [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) +that contains the [RSC](https://react.dev/reference/rsc/server-components) +data for hydration. + diff --git a/docs/api/rsc/index.md b/docs/api/rsc/index.md new file mode 100644 index 0000000000..51d22b79dd --- /dev/null +++ b/docs/api/rsc/index.md @@ -0,0 +1,4 @@ +--- +title: RSC (Unstable) +order: 7 +--- diff --git a/docs/api/rsc/matchRSCServerRequest.md b/docs/api/rsc/matchRSCServerRequest.md new file mode 100644 index 0000000000..8e7b2f1e6c --- /dev/null +++ b/docs/api/rsc/matchRSCServerRequest.md @@ -0,0 +1,162 @@ +--- +title: matchRSCServerRequest +unstable: true +--- + +# unstable_matchRSCServerRequest + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/server.rsc.ts +--> + +[MODES: data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/variables/react_router.unstable_matchRSCServerRequest.html) + +Matches the given routes to a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) +and returns an [RSC](https://react.dev/reference/rsc/server-components) +[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +encoding an [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react_router.unstable_RSCPayload.html) for consumption by an [RSC](https://react.dev/reference/rsc/server-components) +enabled client router. + +```tsx +import { + createTemporaryReferenceSet, + decodeAction, + decodeReply, + loadServerAction, + renderToReadableStream, +} from "@vitejs/plugin-rsc/rsc"; +import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router"; + +matchRSCServerRequest({ + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction, + request, + routes: routes(), + generateResponse(match) { + return new Response( + renderToReadableStream(match.payload), + { + status: match.statusCode, + headers: match.headers, + } + ); + }, +}); +``` + +## Signature + +```tsx +async function matchRSCServerRequest({ + createTemporaryReferenceSet, + basename, + decodeReply, + requestContext, + loadServerAction, + decodeAction, + decodeFormState, + onError, + request, + routes, + generateResponse, +}: { + createTemporaryReferenceSet: () => unknown; + basename?: string; + decodeReply?: DecodeReplyFunction; + decodeAction?: DecodeActionFunction; + decodeFormState?: DecodeFormStateFunction; + requestContext?: unstable_RouterContextProvider; + loadServerAction?: LoadServerActionFunction; + onError?: (error: unknown) => void; + request: Request; + routes: RSCRouteConfigEntry[]; + generateResponse: ( + match: RSCMatch, + { + temporaryReferences, + }: { + temporaryReferences: unknown; + }, + ) => Response; +}): Promise<Response> +``` + +## Params + +### opts.basename + +The basename to use when matching the request. + +### opts.createTemporaryReferenceSet + +A function that returns a temporary reference set for the request, used to track temporary references in the [RSC](https://react.dev/reference/rsc/server-components) +stream. + +### opts.decodeAction + +Your `react-server-dom-xyz/server`'s `decodeAction` function, responsible for loading a server action. + +### opts.decodeFormState + +A function responsible for decoding form state for progressively enhanceable forms with React's [`useActionState`](https://react.dev/reference/react/useActionState) +using your `react-server-dom-xyz/server`'s `decodeFormState`. + +### opts.decodeReply + +Your `react-server-dom-xyz/server`'s `decodeReply` function, used to decode the server function's arguments and bind them to the +implementation for invocation by the router. + +### opts.generateResponse + +A function responsible for using your `renderToReadableStream` to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +encoding the [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react_router.unstable_RSCPayload.html). + +### opts.loadServerAction + +Your `react-server-dom-xyz/server`'s `loadServerAction` function, used to load a server action by ID. + +### opts.onError + +An optional error handler that will be called with any errors that occur during the request processing. + +### opts.request + +The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) to match against. + +### opts.requestContext + +An instance of [`unstable_RouterContextProvider`](../utils/RouterContextProvider) that should be created per request, to be passed to [`action`](../../start/data/route-object#action)s, +[`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware). + +### opts.routes + +Your [route definitions](https://api.reactrouter.com/v7/types/react_router.unstable_RSCRouteConfigEntry.html). + +## Returns + +A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +that contains the [RSC](https://react.dev/reference/rsc/server-components) +data for hydration. + diff --git a/docs/api/rsc/routeRSCServerRequest.md b/docs/api/rsc/routeRSCServerRequest.md new file mode 100644 index 0000000000..f22fd9af3a --- /dev/null +++ b/docs/api/rsc/routeRSCServerRequest.md @@ -0,0 +1,112 @@ +--- +title: routeRSCServerRequest +unstable: true +--- + +# unstable_routeRSCServerRequest + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/server.ssr.tsx +--> + +[MODES: data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.unstable_routeRSCServerRequest.html) + +Routes the incoming [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) +to the [RSC](https://react.dev/reference/rsc/server-components) server and +appropriately proxies the server response for data / resource requests, or +renders to HTML for a document request. + +```tsx +import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; +import * as ReactDomServer from "react-dom/server.edge"; +import { + unstable_RSCStaticRouter as RSCStaticRouter, + unstable_routeRSCServerRequest as routeRSCServerRequest, +} from "react-router"; + +routeRSCServerRequest({ + request, + fetchServer, + createFromReadableStream, + async renderHTML(getPayload) { + const payload = await getPayload(); + + return await renderHTMLToReadableStream( + <RSCStaticRouter getPayload={getPayload} />, + { + bootstrapScriptContent, + formState: await getFormState(payload), + } + ); + }, +}); +``` + +## Signature + +```tsx +async function routeRSCServerRequest({ + request, + fetchServer, + createFromReadableStream, + renderHTML, + hydrate = true, +}: { + request: Request; + fetchServer: (request: Request) => Promise<Response>; + createFromReadableStream: SSRCreateFromReadableStreamFunction; + renderHTML: ( + getPayload: () => Promise<RSCPayload>, + ) => ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>; + hydrate?: boolean; +}): Promise<Response> +``` + +## Params + +### opts.createFromReadableStream + +Your `react-server-dom-xyz/client`'s `createFromReadableStream` function, used to decode payloads from the server. + +### opts.fetchServer + +A function that forwards a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) to the [RSC](https://react.dev/reference/rsc/server-components) handler +and returns a `Promise<Response>` containing a serialized [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react_router.unstable_RSCPayload.html). + +### opts.hydrate + +Whether to hydrate the server response with the RSC payload. Defaults to `true`. + +### opts.renderHTML + +A function that renders the [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react_router.unstable_RSCPayload.html) to HTML, usually using a [`<RSCStaticRouter>`](../rsc/RSCStaticRouter). + +### opts.request + +The request to route. + +## Returns + +A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +that either contains the [RSC](https://react.dev/reference/rsc/server-components) +payload for data requests, or renders the HTML for document requests. + diff --git a/docs/api/utils/Location.md b/docs/api/utils/Location.md deleted file mode 100644 index 459f91710f..0000000000 --- a/docs/api/utils/Location.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Location -hidden: true ---- - -# Location - -[MODES: framework, data, declarative] - -## Summary - -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.Location.html) - -An entry in a history stack. A location contains information about the -URL path, as well as possibly some arbitrary state and a key. - - - diff --git a/docs/api/utils/RouterContextProvider.md b/docs/api/utils/RouterContextProvider.md new file mode 100644 index 0000000000..fa2627e089 --- /dev/null +++ b/docs/api/utils/RouterContextProvider.md @@ -0,0 +1,49 @@ +--- +title: RouterContextProvider +unstable: true +--- + +# unstable_RouterContextProvider + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + +[MODES: framework, data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/classes/react_router.unstable_RouterContextProvider.html) + +Provides methods for writing/reading values in application context in a +type-safe way. Primarily for usage with [middleware](../../how-to/middleware). + +```tsx +import { + unstable_createContext, + unstable_RouterContextProvider +} from "react-router"; + +const userContext = unstable_createContext<User | null>(null); +const contextProvider = new unstable_RouterContextProvider(); +contextProvider.set(userContext, getUser()); +// ^ Type-safe +const user = contextProvider.get(userContext); +// ^ User +``` + diff --git a/docs/api/utils/createContext.md b/docs/api/utils/createContext.md new file mode 100644 index 0000000000..cfa4055538 --- /dev/null +++ b/docs/api/utils/createContext.md @@ -0,0 +1,99 @@ +--- +title: createContext +unstable: true +--- + +# unstable_createContext + +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + +[MODES: framework, data] + +<br /> +<br /> + +<docs-warning>This API is experimental and subject to breaking changes in +minor/patch releases. Please use with caution and pay **very** close attention +to release notes for relevant changes.</docs-warning> + +## Summary + +[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.unstable_createContext.html) + +Creates a type-safe [`unstable_RouterContext`](https://api.reactrouter.com/v7/interfaces/react_router.unstable_RouterContext.html) object that can be used to +store and retrieve arbitrary values in [`action`](../../start/framework/route-module#action)s, +[`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware). +Similar to React's [`createContext`](https://react.dev/reference/react/createContext), +but specifically designed for React Router's request/response lifecycle. + +If a `defaultValue` is provided, it will be returned from `context.get()` +when no value has been set for the context. Otherwise, reading this context +when no value has been set will throw an error. + +```tsx filename=app/context.ts +import { unstable_createContext } from "react-router"; + +// Create a context for user data +export const userContext = + unstable_createContext<User | null>(null); +``` + +```tsx filename=app/middleware/auth.ts +import { getUserFromSession } from "~/auth.server"; +import { userContext } from "~/context"; + +export const authMiddleware = async ({ + context, + request, +}) => { + const user = await getUserFromSession(request); + context.set(userContext, user); +}; +``` + +```tsx filename=app/routes/profile.tsx +import { userContext } from "~/context"; + +export async function loader({ + context, +}: Route.LoaderArgs) { + const user = context.get(userContext); + + if (!user) { + throw new Response("Unauthorized", { status: 401 }); + } + + return { user }; +} +``` + +## Signature + +```tsx +function unstable_createContext<T>( + defaultValue?: T, +): unstable_RouterContext<T> +``` + +## Params + +### defaultValue + +An optional default value for the context. This value will be returned if no value has been set for this context. + +## Returns + +A [`unstable_RouterContext`](https://api.reactrouter.com/v7/interfaces/react_router.unstable_RouterContext.html) object that can be used with +`context.get()` and `context.set()` in [`action`](../../start/framework/route-module#action)s, +[`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware). + diff --git a/docs/api/utils/createRoutesFromElements.md b/docs/api/utils/createRoutesFromElements.md index 20f6b9bd83..19818ce68d 100644 --- a/docs/api/utils/createRoutesFromElements.md +++ b/docs/api/utils/createRoutesFromElements.md @@ -4,30 +4,54 @@ title: createRoutesFromElements # createRoutesFromElements -[MODES: framework, data, declarative] +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ -## Summary +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx +--> + +[MODES: data] -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.createRoutesFromElements.html) +## Summary -Create route objects from JSX elements instead of arrays of objects +[Reference Documentation โ†—](https://api.reactrouter.com/v7/variables/react_router.createRoutesFromElements.html) -## Signature +Create route objects from JSX elements instead of arrays of objects. ```tsx -createRoutesFromElements(children, parentPath): undefined +const routes = createRoutesFromElements( + <> + <Route index loader={step1Loader} Component={StepOne} /> + <Route path="step-2" loader={step2Loader} Component={StepTwo} /> + <Route path="step-3" loader={step3Loader} Component={StepThree} /> + </> +); + +const router = createBrowserRouter(routes); + +function App() { + return <RouterProvider router={router} />; +} ``` ## Params ### children -[modes: framework, data, declarative] - -_No documentation_ +The React children to convert into a route config ### parentPath -[modes: framework, data, declarative] +The path of the parent route, used to generate unique IDs. This is used for internal recursion and is not intended to be used by the +application developer. + +## Returns + +An array of [`RouteObject`](https://api.reactrouter.com/v7/types/react_router.RouteObject.html)s that can be used with a [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react_router.DataRouter.html) -_No documentation_ diff --git a/docs/api/utils/createStaticHandler.md b/docs/api/utils/createStaticHandler.md deleted file mode 100644 index 229bec0ae6..0000000000 --- a/docs/api/utils/createStaticHandler.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: createStaticHandler ---- - -# createStaticHandler - -[MODES: data] - -## Summary - -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.createStaticHandler.html) - -## Signature - -```tsx -createStaticHandler(routes, opts): StaticHandler -``` - -## Params - -### routes - -[modes: data] - -_No documentation_ - -### opts - -[modes: data] - -_No documentation_ diff --git a/docs/api/utils/data.md b/docs/api/utils/data.md index 08557d6eab..8e2d49d910 100644 --- a/docs/api/utils/data.md +++ b/docs/api/utils/data.md @@ -4,31 +4,58 @@ title: data # data +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.data.html) -Create "responses" that contain `status`/`headers` without forcing -serialization into an actual `Response` - used by Remix single fetch +Create "responses" that contain `headers`/`status` without forcing +serialization into an actual [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) + +```tsx +import { data } from "react-router"; + +export async function action({ request }: Route.ActionArgs) { + let formData = await request.formData(); + let item = await createItem(formData); + return data(item, { + headers: { "X-Custom-Header": "value" } + status: 201, + }); +} +``` ## Signature ```tsx -data(data, init): DataWithResponseInit +function data<D>(data: D, init?: number | ResponseInit) ``` ## Params ### data -[modes: framework, data] - -_No documentation_ +The data to be included in the response. ### init -[modes: framework, data] +The status code or a `ResponseInit` object to be included in the response. + +## Returns + +A `DataWithResponseInit` instance containing the data and +response init. -_No documentation_ diff --git a/docs/api/utils/generatePath.md b/docs/api/utils/generatePath.md index dd54246856..9b2e98bd7c 100644 --- a/docs/api/utils/generatePath.md +++ b/docs/api/utils/generatePath.md @@ -4,6 +4,18 @@ title: generatePath # generatePath +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data, declarative] ## Summary @@ -12,22 +24,34 @@ title: generatePath Returns a path with params interpolated. +```tsx +import { generatePath } from "react-router"; + +generatePath("/users/:id", { id: "123" }); // "/users/123" +``` + ## Signature ```tsx -generatePath(originalPath, params): string +function generatePath<Path extends string>( + originalPath: Path, + params: { + [key in PathParam<Path>]: string | null; + } = as any, +): string {} ``` ## Params ### originalPath -[modes: framework, data, declarative] - -_No documentation_ +The original path to generate. ### params -[modes: framework, data, declarative] +The parameters to interpolate into the path. + +## Returns + +The generated path with parameters interpolated. -_No documentation_ diff --git a/docs/api/utils/index.md b/docs/api/utils/index.md index 4ac34cf861..2202127d85 100644 --- a/docs/api/utils/index.md +++ b/docs/api/utils/index.md @@ -1,4 +1,4 @@ --- title: Utils +order: 8 --- - diff --git a/docs/api/utils/isRouteErrorResponse.md b/docs/api/utils/isRouteErrorResponse.md index 9649e61b2f..b45aca4a0d 100644 --- a/docs/api/utils/isRouteErrorResponse.md +++ b/docs/api/utils/isRouteErrorResponse.md @@ -4,25 +4,60 @@ title: isRouteErrorResponse # isRouteErrorResponse +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.isRouteErrorResponse.html) -Check if the given error is an ErrorResponse generated from a 4xx/5xx -Response thrown from an action/loader +Check if the given error is an [`ErrorResponse`](https://api.reactrouter.com/v7/types/react_router.ErrorResponse.html) generated from a 4xx/5xx +[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +thrown from an [`action`](../../start/framework/route-module#action)/[`loader`](../../start/framework/route-module#loader) + +```tsx +import { isRouteErrorResponse } from "react-router"; + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + if (isRouteErrorResponse(error)) { + return ( + <> + <p>Error: `${error.status}: ${error.statusText}`</p> + <p>{error.data}</p> + </> + ); + } + + return ( + <p>Error: {error instanceof Error ? error.message : "Unknown Error"}</p> + ); +} +``` ## Signature ```tsx -isRouteErrorResponse(error): error +function isRouteErrorResponse(error: any): error is ErrorResponse ``` ## Params ### error -[modes: framework, data] +The error to check. + +## Returns + +`true` if the error is an [`ErrorResponse`](https://api.reactrouter.com/v7/types/react_router.ErrorResponse.html), `false` otherwise. -_No documentation_ diff --git a/docs/api/utils/matchPath.md b/docs/api/utils/matchPath.md index 44dd5d73b4..24c0377492 100644 --- a/docs/api/utils/matchPath.md +++ b/docs/api/utils/matchPath.md @@ -4,6 +4,18 @@ title: matchPath # matchPath +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data, declarative] ## Summary @@ -16,19 +28,26 @@ the match. ## Signature ```tsx -matchPath(pattern, pathname): undefined +function matchPath<ParamKey extends ParamParseKey<Path>, Path extends string>( + pattern: PathPattern<Path> | Path, + pathname: string, +): PathMatch<ParamKey> | null ``` ## Params ### pattern -[modes: framework, data, declarative] - -_No documentation_ +The pattern to match against the URL pathname. This can be a string or a [`PathPattern`](https://api.reactrouter.com/v7/interfaces/react_router.PathPattern.html) object. If a string is provided, it will be +treated as a pattern with `caseSensitive` set to `false` and `end` set to +`true`. ### pathname -[modes: framework, data, declarative] +The URL pathname to match against the pattern. + +## Returns + +A path match object if the pattern matches the pathname, +or `null` if it does not match. -_No documentation_ diff --git a/docs/api/utils/matchRoutes.md b/docs/api/utils/matchRoutes.md index 06808d62e0..8bdaad062e 100644 --- a/docs/api/utils/matchRoutes.md +++ b/docs/api/utils/matchRoutes.md @@ -4,6 +4,18 @@ title: matchRoutes # matchRoutes +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data, declarative] ## Summary @@ -12,28 +24,48 @@ title: matchRoutes Matches the given routes to a location and returns the match data. +```tsx +import { matchRoutes } from "react-router"; + +let routes = [{ + path: "/", + Component: Root, + children: [{ + path: "dashboard", + Component: Dashboard, + }] +}]; + +matchRoutes(routes, "/dashboard"); // [rootMatch, dashboardMatch] +``` + ## Signature ```tsx -matchRoutes(routes, locationArg, basename): undefined +function matchRoutes< + RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, +>( + routes: RouteObjectType[], + locationArg: Partial<Location> | string, + basename = "/", +): AgnosticRouteMatch<string, RouteObjectType>[] | null ``` ## Params ### routes -[modes: framework, data, declarative] - -_No documentation_ +The array of route objects to match against. ### locationArg -[modes: framework, data, declarative] - -_No documentation_ +The location to match against, either a string path or a partial [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) object ### basename -[modes: framework, data, declarative] +Optional base path to strip from the location before matching. Defaults to `/`. + +## Returns + +An array of matched routes, or `null` if no matches were found. -_No documentation_ diff --git a/docs/api/utils/redirect.md b/docs/api/utils/redirect.md index 7285e35704..4a52481762 100644 --- a/docs/api/utils/redirect.md +++ b/docs/api/utils/redirect.md @@ -4,31 +4,53 @@ title: redirect # redirect +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data] ## Summary -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.redirect.html) - -A redirect response. Sets the status code and the `Location` header. -Defaults to "302 Found". +[Reference Documentation โ†—](https://api.reactrouter.com/v7/variables/react_router.redirect.html) -## Signature +A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). +Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) +header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302). ```tsx -redirect(url, init): Response +import { redirect } from "react-router"; + +export async function loader({ request }: Route.LoaderArgs) { + if (!isLoggedIn(request)) + throw redirect("/login"); + } + + // ... +} ``` ## Params ### url -[modes: framework, data] - -_No documentation_ +The URL to redirect to. ### init -[modes: framework, data] +The status code or a `ResponseInit` object to be included in the response. + +## Returns + +A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) +header. -_No documentation_ diff --git a/docs/api/utils/redirectDocument.md b/docs/api/utils/redirectDocument.md index 3f7fc9f658..7fc57a0379 100644 --- a/docs/api/utils/redirectDocument.md +++ b/docs/api/utils/redirectDocument.md @@ -4,32 +4,55 @@ title: redirectDocument # redirectDocument +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data] ## Summary -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.redirectDocument.html) +[Reference Documentation โ†—](https://api.reactrouter.com/v7/variables/react_router.redirectDocument.html) -A redirect response that will force a document reload to the new location. -Sets the status code and the `Location` header. -Defaults to "302 Found". +A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +that will force a document reload to the new location. Sets the status code +and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) +header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302). -## Signature +```tsx filename=routes/logout.tsx +import { redirectDocument } from "react-router"; -```tsx -redirectDocument(url, init): Response +import { destroySession } from "../sessions.server"; + +export async function action({ request }: Route.ActionArgs) { + let session = await getSession(request.headers.get("Cookie")); + return redirectDocument("/", { + headers: { "Set-Cookie": await destroySession(session) } + }); +} ``` ## Params ### url -[modes: framework, data] - -_No documentation_ +The URL to redirect to. ### init -[modes: framework, data] +The status code or a `ResponseInit` object to be included in the response. + +## Returns + +A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) +header. -_No documentation_ diff --git a/docs/api/utils/renderMatches.md b/docs/api/utils/renderMatches.md index 3dadcda4a3..a3544c5fbf 100644 --- a/docs/api/utils/renderMatches.md +++ b/docs/api/utils/renderMatches.md @@ -4,24 +4,41 @@ title: renderMatches # renderMatches +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.renderMatches.html) -Renders the result of `matchRoutes()` into a React element. +Renders the result of [`matchRoutes`](../utils/matchRoutes) into a React element. ## Signature ```tsx -renderMatches(matches): undefined +function renderMatches( + matches: RouteMatch[] | null, +): React.ReactElement | null ``` ## Params ### matches -[modes: framework, data, declarative] +The array of [route matches](https://api.reactrouter.com/v7/interfaces/react_router.RouteMatch.html) to render + +## Returns + +A React element that renders the matched routes or `null` if no matches -_No documentation_ diff --git a/docs/api/utils/replace.md b/docs/api/utils/replace.md index cc2502d945..39c2ddb21c 100644 --- a/docs/api/utils/replace.md +++ b/docs/api/utils/replace.md @@ -4,33 +4,51 @@ title: replace # replace +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data] ## Summary -[Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.replace.html) - -A redirect response that will perform a `history.replaceState` instead of a -`history.pushState` for client-side navigation redirects. -Sets the status code and the `Location` header. -Defaults to "302 Found". +[Reference Documentation โ†—](https://api.reactrouter.com/v7/variables/react_router.replace.html) -## Signature +A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +that will perform a [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState) +instead of a [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) +for client-side navigation redirects. Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) +header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302). ```tsx -replace(url, init): Response +import { replace } from "react-router"; + +export async function loader() { + return replace("/new-location"); +} ``` ## Params ### url -[modes: framework, data] - -_No documentation_ +The URL to redirect to. ### init -[modes: framework, data] +The status code or a `ResponseInit` object to be included in the response. + +## Returns + +A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) +object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) +header. -_No documentation_ diff --git a/docs/api/utils/resolvePath.md b/docs/api/utils/resolvePath.md index 24b0521940..5c552e7918 100644 --- a/docs/api/utils/resolvePath.md +++ b/docs/api/utils/resolvePath.md @@ -4,30 +4,43 @@ title: resolvePath # resolvePath +<!-- +โš ๏ธ โš ๏ธ IMPORTANT โš ๏ธ โš ๏ธ + +Thank you for helping improve our documentation! + +This file is auto-generated from the JSDoc comments in the source +code, so please edit the JSDoc comments in the file below and this +file will be re-generated once those changes are merged. + +https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts +--> + [MODES: framework, data, declarative] ## Summary [Reference Documentation โ†—](https://api.reactrouter.com/v7/functions/react_router.resolvePath.html) -Returns a resolved path object relative to the given pathname. +Returns a resolved [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html) object relative to the given pathname. ## Signature ```tsx -resolvePath(to, fromPathname): Path +function resolvePath(to: To, fromPathname = "/"): Path ``` ## Params ### to -[modes: framework, data, declarative] - -_No documentation_ +The path to resolve, either a string or a partial [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html) object. ### fromPathname -[modes: framework, data, declarative] +The pathname to resolve the path from. Defaults to `/`. + +## Returns + +A [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html) object with the resolved pathname, search, and hash. -_No documentation_ diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 10d95c4fef..c5b8a7e90a 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -8,6 +8,10 @@ Thanks for contributing, you rock! When it comes to open source, there are many different kinds of contributions that can be made, all of which are valuable. Here are a few guidelines that should help you as you prepare your contribution. +## Open Governance Model + +Before going any further, please read the Open Governance [blog post](https://remix.run/blog/rr-governance) and [document](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md) for information on how we handle bugs/issues/feature proposals in React Router. + ## Setup Before you can contribute to the codebase, you will need to fork the repo. This will look a bit different depending on what type of contribution you are making: @@ -33,21 +37,23 @@ The following steps will get you set up to contribute changes to this repo: ## Think You Found a Bug? -Please conform to the issue template and provide a clear path to reproduction with a code example. Best is a pull request with a [failing test](https://github.com/remix-run/react-router/blob/dev/integration/bug-report-test.ts). Next best is a link to [StackBlitz](https://reactrouter.com/new) or repository that illustrates the bug. +Please conform to the issue template and provide a **minimal** and **runnable** reproduction. Best is a pull request with a [failing test](https://github.com/remix-run/react-router/blob/dev/integration/bug-report-test.ts). Next best is a link to [StackBlitz](https://reactrouter.com/new), CodeSsndbox, or GitHub repository that illustrates the bug. -## Adding an Example? +## Issue Not Getting Attention? -Examples can be added directly to the main branch. Create a branch off of your local clone of main. Once you've finished, create a pull request and outline your example. +If you need a bug fixed and nobody is fixing it, your best bet is to provide a fix for it and make a [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). Open source code belongs to all of us, and it's all of our responsibility to push it forward. ## Proposing New or Changed API? -Please provide thoughtful comments and some sample code that show what you'd like to do with React Router in your app. It helps the conversation if you can show us how you're limited by the current API first before jumping to a conclusion about what needs to be changed and/or added. +โš ๏ธ _Please do not start with a PR for a new feature._ + +New features need to go through the process outlined in the [Open Governance Model](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#new-feature-process) and can be started by opening a [Proposal Discussion](https://github.com/remix-run/react-router/discussions/new?category=proposals) on GitHub. Please provide thoughtful comments and some sample code that show what you'd like to do with React Router in your app. It helps the conversation if you can show us how you're limited by the current API first before jumping to a conclusion about what needs to be changed and/or added. We have learned by experience that small APIs are usually better, so we may be a little reluctant to add something new unless there's an obvious limitation with the current API. That being said, we are always anxious to hear about cases that we just haven't considered before, so please don't be shy! :) -## Issue Not Getting Attention? +## Adding an Example? -If you need a bug fixed and nobody is fixing it, your best bet is to provide a fix for it and make a [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). Open source code belongs to all of us, and it's all of our responsibility to push it forward. +Examples can be added directly to the `main` branch. Create a branch off of your local clone of `main`. Once you've finished, create a pull request and outline your example. ## Making a Pull Request? @@ -57,7 +63,7 @@ Pull requests need only the approval of two or more collaborators to be merged; ### Tests -All commits that fix bugs or add features need a test. +All commits that fix bugs or add features need one or more tests. <docs-error>Do not merge code without tests!</docs-error> @@ -83,14 +89,14 @@ Calling `pnpm build` from the root directory will run the build, which should ta ### Testing -Before running the tests, you need to run a build. After you build, running `pnpm test` from the root directory will run **every** package's tests. If you want to run tests for a specific package, use `pnpm test --projects packages/<package-name>`: +Before running the tests, you need to run a build. After you build, running `pnpm test` from the root directory will run **every** package's tests. If you want to run tests for a specific package, use `pnpm test packages/<package-name>/`: ```bash # Test all packages pnpm test -# Test only react-router-dom -pnpm test --projects packages/react-router-dom +# Test only @react-router/dev +pnpm test packages/react-router-dev/ ``` ## Repository Branching @@ -100,84 +106,11 @@ This repo maintains separate branches for different purposes. They will look som ``` - main > the most recent release and current docs - dev > code under active development between stable releases -- v5 > the most recent code for a specific major release +- v6 > the most recent code for a specific major release ``` There may be other branches for various features and experimentation, but all of the magic happens from these branches. -## New Releases +## Releases -When it's time to cut a new release, we follow a process based on our branching strategy depending on the type of release. - -### `react-router@next` Releases - -We create experimental releases from the current state of the `dev` branch. They can be installed by using the `@next` tag: - -```bash -pnpm add react-router-dom@next -# or -npm install react-router-dom@next -``` - -These releases will be automated as PRs are merged into the `dev` branch. - -### Latest Major Releases - -```bash -# Start from the dev branch. -git checkout dev - -# Merge the main branch into dev to ensure that any hotfixes and -# docs updates are available in the release. -git merge main - -# Create a new release branch from dev. -git checkout -b release/v6.1.0 - -# Create a new tag and update version references throughout the -# codebase. -pnpm run version [nextVersion] - -# Push the release branch along with the new release tag. -git push origin release/v6.1.0 --follow-tags - -# Wait for GitHub actions to run all tests. If the tests pass, the -# release is ready to go! Merge the release branch into main and dev. -git checkout main -git merge release/v6.1.0 -git checkout dev -git merge release/v6.1.0 - -# The release branch can now be deleted. -git branch -D release/v6.1.0 -git push origin --delete release/v6.1.0 - -# Now go to GitHub and create the release from the new tag. Let -# GitHub Actions take care of the rest! -``` - -### Hot-fix Releases - -Sometimes we have a crucial bug that needs to be patched right away. If the bug affects the latest release, we can create a new version directly from `main` (or the relevant major release branch where the bug exists): - -```bash -# From the main branch, make sure to run the build and all tests -# before creating a new release. -pnpm install && pnpm build && pnpm test - -# Assuming the tests pass, create the release tag and update -# version references throughout the codebase. -pnpm run version [nextVersion] - -# Push changes along with the new release tag. -git push origin main --follow-tags - -# In GitHub, create the release from the new tag and it will be -# published via GitHub actions - -# When the hot-fix is done, merge the changes into dev and clean -# up conflicts as needed. -git checkout dev -git merge main -git push origin dev -``` +Please refer to [DEVELOPMENT.md](https://github.com/remix-run/react-router/blob/main/DEVELOPMENT.md) for an outline of the release process. 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] + +<br/> +<br/> + +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 index e764299b0b..e5425dec20 100644 --- a/docs/explanation/code-splitting.md +++ b/docs/explanation/code-splitting.md @@ -4,6 +4,11 @@ title: Automatic Code Splitting # Automatic Code Splitting +[MODES: framework] + +<br/> +<br/> + 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 @@ -26,7 +31,7 @@ Instead of bundling all routes into a single giant build, the modules referenced 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 ensures drastically reduces the JavaScript footprint for initial page loads and speeds up your application. +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 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] + +<br/> +<br/> + +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<typeof loader>(); + + return ( + <fetcher.Form action="/city-search"> + <Combobox aria-label="Cities"> + <ComboboxInput + name="q" + onChange={(event) => + // submit the form onChange to get the list of cities + fetcher.submit(event.target.form) + } + /> + + {/* render with the loader's data */} + {fetcher.data ? ( + <ComboboxPopover className="shadow-popup"> + {fetcher.data.length > 0 ? ( + <ComboboxList> + {fetcher.data.map((city) => ( + <ComboboxOption + key={city.id} + value={city.name} + /> + ))} + </ComboboxList> + ) : ( + <span>No results found</span> + )} + </ComboboxPopover> + ) : null} + </Combobox> + </fetcher.Form> + ); +} +``` + +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>`][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>`][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 | +| ----------------------------- | -------------------- | +| `<Form>` | `<fetcher.Form>` | +| `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 ( + <Form method="post"> + <label> + Title: <input name="title" /> + {errors?.title ? <span>{errors.title}</span> : null} + </label> + <label> + Ingredients: <textarea name="ingredients" /> + {errors?.ingredients ? ( + <span>{errors.ingredients}</span> + ) : null} + </label> + <label> + Directions: <textarea name="directions" /> + {errors?.directions ? ( + <span>{errors.directions}</span> + ) : null} + </label> + <button type="submit"> + {isSubmitting ? "Saving..." : "Create Recipe"} + </button> + </Form> + ); +} +``` + +The example leverages [`<Form>`][form-component], component props, and [`useNavigation`][use-navigation] to facilitate an intuitive record creation process. + +Using `<Form>` ensures direct and logical navigation. After creating a record, the user is naturally guided to the new recipe's unique URL, reinforcing the outcome of their action. + +The component props bridge server and client, providing immediate feedback on submission issues. This quick response enables users to rectify any errors without hindrance. + +Lastly, `useNavigation` dynamically reflects the form's submission state. This subtle UI change, like toggling the button's label, assures users that their actions are being processed. + +Combined, these APIs offer a balanced blend of structured navigation and feedback. + +### Updating a Record + +Now consider we're looking at a list of recipes that have delete buttons on each item. When a user clicks the delete button, we want to delete the recipe from the database and remove it from the list without navigating away from the list. + +First, consider the basic route setup to get a list of recipes on the page: + +```tsx filename=app/pages/recipes.tsx +import type { Route } from "./+types/recipes"; + +export async function loader({ + request, +}: Route.LoaderArgs) { + return { + recipes: await db.recipes.findAll({ limit: 30 }), + }; +} + +export default function Recipes({ + loaderData, +}: Route.ComponentProps) { + const { recipes } = loaderData; + return ( + <ul> + {recipes.map((recipe) => ( + <RecipeListItem key={recipe.id} recipe={recipe} /> + ))} + </ul> + ); +} +``` + +Now we'll look at the action that deletes a recipe and the component that renders each recipe in the list. + +```tsx filename=app/pages/recipes.tsx lines=[10,21,27] +import { useFetcher } from "react-router"; +import type { Recipe } from "./recipe.server"; +import type { Route } from "./+types/recipes"; + +export async function action({ + request, +}: Route.ActionArgs) { + const formData = await request.formData(); + const id = formData.get("id"); + await db.recipes.delete(id); + return { ok: true }; +} + +export default function Recipes() { + return ( + // ... + // doesn't matter, somewhere it's using <RecipeListItem /> + ) +} + +function RecipeListItem({ recipe }: { recipe: Recipe }) { + const fetcher = useFetcher(); + const isDeleting = fetcher.state !== "idle"; + + return ( + <li> + <h2>{recipe.title}</h2> + <fetcher.Form method="post"> + <input type="hidden" name="id" value={recipe.id} /> + <button disabled={isDeleting} type="submit"> + {isDeleting ? "Deleting..." : "Delete"} + </button> + </fetcher.Form> + </li> + ); +} +``` + +Using [`useFetcher`][use-fetcher] in this scenario works perfectly. Instead of navigating away or refreshing the entire page, we want in-place updates. When a user deletes a recipe, the `action` is called and the fetcher manages the corresponding state transitions. + +The key advantage here is the maintenance of context. The user stays on the list when the deletion completes. The fetcher's state management capabilities are leveraged to give real-time feedback: it toggles between `"Deleting..."` and `"Delete"`, providing a clear indication of the ongoing process. + +Furthermore, with each `fetcher` having the autonomy to manage its own state, operations on individual list items become independent, ensuring that actions on one item don't affect the others (though revalidation of the page data is a shared concern covered in [Network Concurrency Management][network-concurrency-management]). + +In essence, `useFetcher` offers a seamless mechanism for actions that don't necessitate a change in the URL or navigation, enhancing the user experience by providing real-time feedback and context preservation. + +### Mark Article as Read + +Imagine you want to mark that an article has been read by the current user, after they've been on the page for a while and scrolled to the bottom. You could make a hook that looks something like this: + +```tsx +import { useFetcher } from "react-router"; + +function useMarkAsRead({ articleId, userId }) { + const marker = useFetcher(); + + useSpentSomeTimeHereAndScrolledToTheBottom(() => { + marker.submit( + { userId }, + { + action: `/article/${articleId}/mark-as-read`, + method: "post", + }, + ); + }); +} +``` + +### User Avatar Details Popup + +Anytime you show the user avatar, you could put a hover effect that fetches data from a loader and displays it in a popup. + +```tsx filename=app/pages/user-details.tsx +import { useState, useEffect } from "react"; +import { useFetcher } from "react-router"; +import type { Route } from "./+types/user-details"; + +export async function loader({ params }: Route.LoaderArgs) { + return await fakeDb.user.find({ + where: { id: params.id }, + }); +} + +type LoaderData = Route.ComponentProps["loaderData"]; + +function UserAvatar({ partialUser }) { + const userDetails = useFetcher<LoaderData>(); + const [showDetails, setShowDetails] = useState(false); + + useEffect(() => { + if ( + showDetails && + userDetails.state === "idle" && + !userDetails.data + ) { + userDetails.load(`/user-details/${partialUser.id}`); + } + }, [showDetails, userDetails, partialUser.id]); + + return ( + <div + onMouseEnter={() => setShowDetails(true)} + onMouseLeave={() => setShowDetails(false)} + > + <img src={partialUser.profileImageUrl} /> + {showDetails ? ( + userDetails.state === "idle" && userDetails.data ? ( + <UserPopup user={userDetails.data} /> + ) : ( + <UserPopupLoading /> + ) + ) : null} + </div> + ); +} +``` + +## Conclusion + +React Router offers a range of tools to cater to varied developmental needs. While some functionalities might seem to overlap, each tool has been crafted with specific scenarios in mind. By understanding the intricacies and ideal applications of `<Form>`, `useFetcher`, and `useNavigation`, along with how data flows through component props, developers can create more intuitive, responsive, and user-friendly web applications. + +[form-component]: ../api/components/Form +[use-fetcher]: ../api/hooks/useFetcher +[use-navigation]: ../api/hooks/useNavigation +[network-concurrency-management]: ./concurrency diff --git a/docs/explanation/hot-module-replacement.md b/docs/explanation/hot-module-replacement.md index b9455785d6..0467cb7719 100644 --- a/docs/explanation/hot-module-replacement.md +++ b/docs/explanation/hot-module-replacement.md @@ -4,6 +4,11 @@ title: Hot Module Replacement # Hot Module Replacement +[MODES: framework] + +<br/> +<br/> + Hot Module Replacement is a technique for updating modules in your app without needing to reload the page. It's a great developer experience, and React Router supports it when using Vite. diff --git a/docs/explanation/index-query-param.md b/docs/explanation/index-query-param.md new file mode 100644 index 0000000000..d42a63dd48 --- /dev/null +++ b/docs/explanation/index-query-param.md @@ -0,0 +1,86 @@ +--- +title: Index Query Param +--- + +# Index Query Param + +[MODES: framework, data] + +## Overview + +You may find a wild `?index` appearing in the URL of your app when submitting forms. + +Because of nested routes, multiple routes in your route hierarchy can match the URL. Unlike navigations where all matching route [`loader`][loader]s are called to build up the UI, when a [`form`][form_element] is submitted, _only one action is called_. + +Because index routes share the same URL as their parent, the `?index` param lets you disambiguate between the two. + +## Understanding Index Routes + +For example, consider the following route structure: + +```ts filename=app/routes.ts +import { + type RouteConfig, + route, + index, +} from "@react-router/dev/routes"; + +export default [ + route("projects", "./pages/projects.tsx", [ + index("./pages/projects/index.tsx"), + route(":id", "./pages/projects/project.tsx"), + ]), +] satisfies RouteConfig; +``` + +This creates two routes that match `/projects`: + +- The parent route (`./pages/projects.tsx`) +- The index route (`./pages/projects/index.tsx`) + +## Form Submission Targeting + +For example, consider the following forms: + +```tsx +<Form method="post" action="/projects" /> +<Form method="post" action="/projects?index" /> +``` + +The `?index` param will submit to the index route; the action without the index param will submit to the parent route. + +When a [`<Form>`][form_component] is rendered in an index route without an [`action`][action], the `?index` param will automatically be appended so that the form posts to the index route. The following form, when submitted, will post to `/projects?index` because it is rendered in the context of the `projects` index route: + +```tsx filename=app/pages/projects/index.tsx +function ProjectsIndex() { + return <Form method="post" />; +} +``` + +If you moved the code to the project layout (`./pages/projects.tsx` in this example), it would instead post to `/projects`. + +This applies to `<Form>` and all of its cousins: + +```tsx +function Component() { + const submit = useSubmit(); + submit({}, { action: "/projects" }); + submit({}, { action: "/projects?index" }); +} +``` + +```tsx +function Component() { + const fetcher = useFetcher(); + fetcher.submit({}, { action: "/projects" }); + fetcher.submit({}, { action: "/projects?index" }); + <fetcher.Form action="/projects" />; + <fetcher.Form action="/projects?index" />; + <fetcher.Form />; // defaults to the route in context +} +``` + +[loader]: ../api/data-routers/loader +[form_element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form +[form_component]: ../api/components/Form +[action]: ../api/data-routers/action diff --git a/docs/explanation/lazy-route-discovery.md b/docs/explanation/lazy-route-discovery.md new file mode 100644 index 0000000000..d99f09fa67 --- /dev/null +++ b/docs/explanation/lazy-route-discovery.md @@ -0,0 +1,78 @@ +--- +title: Lazy Route Discovery +--- + +# Lazy Route Discovery + +[MODES: framework] + +<br/> +<br/> + +Lazy Route Discovery is a performance optimization that loads route information progressively as users navigate through your application, rather than loading the complete route manifest upfront. + +With Lazy Route Discovery enabled (the default), React Router sends only the routes needed for the initial server-side render in the manifest. As users navigate to new parts of your application, additional route information is fetched dynamically and added to the client-side manifest. + +The route manifest contains metadata about your routes (JavaScript/CSS imports, whether routes have `loaders`/`actions`, etc.) but not the actual route module implementations. This allows React Router to understand your application's structure without downloading unnecessary route information. + +## Route Discovery Process + +When a user navigates to a new route that isn't in the current manifest: + +1. **Route Discovery Request** - React Router makes a request to the internal `/__manifest` endpoint +2. **Manifest Patch** - The server responds with the required route information +3. **Route Loading** - React Router loads the necessary route modules and data +4. **Navigation** - The user navigates to the new route + +## Eager Discovery Optimization + +To prevent navigation waterfalls, React Router implements eager route discovery. All [`<Link>`](../api/components/Link) and [`<NavLink>`](../api/components/NavLink) components rendered on the current page are automatically discovered via a batched request to the server. + +This discovery request typically completes before users click any links, making subsequent navigation feel synchronous even with lazy route discovery enabled. + +```tsx +// Links are automatically discovered by default +<Link to="/dashboard">Dashboard</Link> + +// Opt out of discovery for specific links +<Link to="/admin" discover="none">Admin</Link> +``` + +## Performance Benefits + +Lazy Route Discovery provides several performance improvements: + +- **Faster Initial Load** - Smaller initial bundle size by excluding unused route metadata +- **Reduced Memory Usage** - Route information is loaded only when needed +- **Scalability** - Applications with hundreds of routes see more significant benefits + +## Configuration + +You can configure route discovery behavior in your `react-router.config.ts`: + +```tsx filename=react-router.config.ts +export default { + // Default: lazy discovery with /__manifest endpoint + routeDiscovery: { + mode: "lazy", + manifestPath: "/__manifest", + }, + + // Custom manifest path (useful for multiple apps on same domain) + routeDiscovery: { + mode: "lazy", + manifestPath: "/my-app-manifest", + }, + + // Disable lazy discovery (include all routes initially) + routeDiscovery: { mode: "initial" }, +} satisfies Config; +``` + +## Deployment Considerations + +When using lazy route discovery, ensure your deployment setup handles manifest requests properly: + +- **Route Handling** - Ensure `/__manifest` requests reach your React Router handler +- **CDN Caching** - If using CDN/edge caching, include `version` and `p` query parameters in your cache key for the manifest endpoint +- **Multiple Applications** - Use a custom `manifestPath` if running multiple React Router applications on the same domain diff --git a/docs/explanation/picking-a-router.md b/docs/explanation/picking-a-router.md deleted file mode 100644 index 761005138b..0000000000 --- a/docs/explanation/picking-a-router.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Picking a Router -hidden: true ---- - -## TODO: diff --git a/docs/explanation/progressive-enhancement.md b/docs/explanation/progressive-enhancement.md index 8a65f38b0c..b502d3ffe6 100644 --- a/docs/explanation/progressive-enhancement.md +++ b/docs/explanation/progressive-enhancement.md @@ -4,9 +4,14 @@ title: Progressive Enhancement # Progressive Enhancement -> Progressive enhancement is a strategy in web design that puts emphasis on web content first, allowing everyone to access the basic content and functionality of a web page, whilst users with additional browser features or faster Internet access receive the enhanced version instead. +[MODES: framework] + +<br/> +<br/> -<cite>- [Wikipedia][wikipedia]</cite> +> Progressive enhancement is a strategy in web design that puts emphasis on web content first, allowing everyone to access the basic content and functionality of a web page, whilst users with additional browser features or faster Internet access receive the enhanced version instead. +> +> <cite>- [Wikipedia][wikipedia]</cite> When using React Router with Server-Side Rendering (the default in framework mode), you can automatically leverage the benefits of progressive enhancement. diff --git a/docs/explanation/race-conditions.md b/docs/explanation/race-conditions.md index 9289da4520..dbeb2fc5d0 100644 --- a/docs/explanation/race-conditions.md +++ b/docs/explanation/race-conditions.md @@ -4,6 +4,11 @@ title: Race Conditions # Race Conditions +[MODES: framework, data] + +<br/> +<br/> + While impossible to eliminate every possible race condition in your application, React Router automatically handles the most common race conditions found in web user interfaces. ## Browser Behavior diff --git a/docs/explanation/sessions-and-cookies.md b/docs/explanation/sessions-and-cookies.md index a0e37c9820..8d82dd7d11 100644 --- a/docs/explanation/sessions-and-cookies.md +++ b/docs/explanation/sessions-and-cookies.md @@ -4,6 +4,8 @@ title: Sessions and Cookies # Sessions and Cookies +[MODES: framework, data] + ## Sessions Sessions are an important part of websites that allow the server to identify requests coming from the same person, especially when it comes to server-side form validation or when JavaScript is not on the page. Sessions are a fundamental building block of many sites that let users "log in", including social, e-commerce, business, and educational websites. @@ -45,7 +47,7 @@ const { getSession, commitSession, destroySession } = secrets: ["s3cret1"], secure: true, }, - } + }, ); export { getSession, commitSession, destroySession }; @@ -64,7 +66,7 @@ export async function action({ request, }: ActionFunctionArgs) { const session = await getSession( - request.headers.get("Cookie") + request.headers.get("Cookie"), ); session.get("foo"); session.has("bar"); @@ -91,7 +93,7 @@ export async function loader({ request, }: Route.LoaderArgs) { const session = await getSession( - request.headers.get("Cookie") + request.headers.get("Cookie"), ); if (session.has("userId")) { @@ -105,7 +107,7 @@ export async function loader({ headers: { "Set-Cookie": await commitSession(session), }, - } + }, ); } @@ -113,7 +115,7 @@ export async function action({ request, }: Route.ActionArgs) { const session = await getSession( - request.headers.get("Cookie") + request.headers.get("Cookie"), ); const form = await request.formData(); const username = form.get("username"); @@ -121,7 +123,7 @@ export async function action({ const userId = await validateCredentials( username, - password + password, ); if (userId == null) { @@ -183,7 +185,7 @@ export async function action({ request, }: Route.ActionArgs) { const session = await getSession( - request.headers.get("Cookie") + request.headers.get("Cookie"), ); return redirect("/login", { headers: { diff --git a/docs/explanation/special-files.md b/docs/explanation/special-files.md index 9447977159..ca8192f2e8 100644 --- a/docs/explanation/special-files.md +++ b/docs/explanation/special-files.md @@ -1,358 +1,16 @@ --- title: Special Files +hidden: true --- # Special Files -There are a few special files that React Router looks for in your project. Not all of these files are required +The content of this page has been moved to the following: -## react-router.config.ts - -**This file is optional** - -The config file is used to configure certain aspects of your app, such as whether you are using server-side rendering, where certain directories are located, and more. - -```tsx filename=react-router.config.ts -import type { Config } from "@react-router/dev/config"; - -export default { - // Config options... -} satisfies Config; -``` - -See the details on [react-router config API][react-router-config] for more information. - -## root.tsx - -**This file is required** - -The "root" route (`app/root.tsx`) is the only _required_ route in your React Router application because it is the parent to all routes in your `routes/` directory and is in charge of rendering the root `<html>` document. - -Because the root route manages your document, it is the proper place to render a handful of "document-level" components React Router provides. These components are to be used once inside your root route and they include everything React Router figured out or built in order for your page to render properly. - -```tsx filename=app/root.tsx -import type { LinksFunction } from "react-router"; -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "react-router"; - -import "./global-styles.css"; - -export default function App() { - return ( - <html lang="en"> - <head> - <meta charSet="utf-8" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1" - /> - - {/* All `meta` exports on all routes will render here */} - <Meta /> - - {/* All `link` exports on all routes will render here */} - <Links /> - <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head> - <body> - {/* Child routes render here */} - <Outlet /> - - {/* Manages scroll position for client-side transitions */} - {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} - <ScrollRestoration /> - - {/* Script tags go here */} - {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} - <Scripts /> - </body> - </html> - ); -} -``` - -### Layout export - -The root route supports all [route module exports][route-module]. - -The root route also supports an additional optional `Layout` export. The `Layout` component serves 2 purposes: - -1. Avoid duplicating your document's "app shell" across your root component, `HydrateFallback`, and `ErrorBoundary` -2. Prevent React from re-mounting your app shell elements when switching between the root component/`HydrateFallback`/`ErrorBoundary` which can cause a FOUC if React removes and re-adds `<link rel="stylesheet">` tags from your `<Links>` component. - -```tsx filename=app/root.tsx lines=[10-31] -export function Layout({ children }) { - return ( - <html lang="en"> - <head> - <meta charSet="utf-8" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1" - /> - <Meta /> - <Links /> - <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head> - <body> - {/* children will be the root Component, ErrorBoundary, or HydrateFallback */} - {children} - <Scripts /> - <ScrollRestoration /> - </body> - </html> - ); -} - -export default function App() { - return <Outlet />; -} - -export function ErrorBoundary() {} -``` - -**A note on `useLoaderData`in the `Layout` Component** - -`useLoaderData` is not permitted to be used in `ErrorBoundary` components because it is intended for the happy-path route rendering, and its typings have a built-in assumption that the `loader` ran successfully and returned something. That assumption doesn't hold in an `ErrorBoundary` because it could have been the `loader` that threw and triggered the boundary! In order to access loader data in `ErrorBoundary`'s, you can use `useRouteLoaderData` which accounts for the loader data potentially being `undefined`. - -Because your `Layout` component is used in both success and error flows, this same restriction holds. If you need to fork logic in your `Layout` depending on if it was a successful request or not, you can use `useRouteLoaderData("root")` and `useRouteError()`. - -<docs-warn>Because your `<Layout>` component is used for rendering the `ErrorBoundary`, you should be _very defensive_ to ensure that you can render your `ErrorBoundary` without encountering any render errors. If your `Layout` throws another error trying to render the boundary, then it can't be used and your UI will fall back to the very minimal built-in default `ErrorBoundary`.</docs-warn> - -```tsx filename=app/root.tsx lines=[6-7,19-29,32-34] -export function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const data = useRouteLoaderData("root"); - const error = useRouteError(); - - return ( - <html lang="en"> - <head> - <meta charSet="utf-8" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1" - /> - <Meta /> - <Links /> - <style - dangerouslySetInnerHTML={{ - __html: ` - :root { - --themeVar: ${ - data?.themeVar || defaultThemeVar - } - } - `, - }} - /> - <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head> - <body> - {data ? ( - <Analytics token={data.analyticsToken} /> - ) : null} - {children} - <ScrollRestoration /> - <Scripts /> - </body> - </html> - ); -} -``` - -## routes.ts - -**This file is required** - -The `routes.ts` file is used to configure which url patterns are matched to which route modules. - -```tsx filename=app/routes.ts -import { - type RouteConfig, - route, -} from "@react-router/dev/routes"; - -export default [ - route("some/path", "./some/file.tsx"), - // pattern ^ ^ module file -] satisfies RouteConfig; -``` - -See the [routing guide][routing] for more information. - -## entry.client.tsx - -**This file is optional** - -By default, React Router will handle hydrating your app on the client for you. You can reveal the default entry client file with the following: - -```shellscript nonumber -react-router reveal -``` - -This file is the entry point for the browser and is responsible for hydrating the markup generated by the server in your [server entry module][server-entry], however you can also initialize any other client-side code here. - -```tsx filename=app/entry.client.tsx -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; -import { HydratedRouter } from "react-router/dom"; - -startTransition(() => { - hydrateRoot( - document, - <StrictMode> - <HydratedRouter /> - </StrictMode> - ); -}); -``` - -This is the first piece of code that runs in the browser. You can initialize client side libraries, add client only providers, etc. - -## entry.server.tsx - -**This file is optional** - -By default, React Router will handle generating the HTTP Response for you. You can reveal the default entry server file with the following: - -```shellscript nonumber -react-router reveal -``` - -The `default` export of this module is a function that lets you create the response, including HTTP status, headers, and HTML, giving you full control over the way the markup is generated and sent to the client. - -This module should render the markup for the current page using a `<ServerRouter>` element with the `context` and `url` for the current request. This markup will (optionally) be re-hydrated once JavaScript loads in the browser using the [client entry module][client-entry]. - -### `streamTimeout` - -If you are [streaming] responses, you can export an optional `streamTimeout` value (in milliseconds) that will control the amount of time the server will wait for streamed promises to settle before rejecting outstanding promises them and closing the stream. - -It's recommended to decouple this value from the timeout in which you abort the React renderer. You should always set the React rendering timeout to a higher value so it has time to stream down the underlying rejections from your `streamTimeout`. - -```tsx lines=[1-2,13-15] -// Reject all pending promises from handler functions after 10 seconds -export const streamTimeout = 10000; - -export default function handleRequest(...) { - return new Promise((resolve, reject) => { - // ... - - const { pipe, abort } = renderToPipeableStream( - <ServerRouter context={routerContext} url={request.url} />, - { /* ... */ } - ); - - // Abort the streaming render pass after 11 seconds to allow the rejected - // boundaries to be flushed - setTimeout(abort, streamTimeout + 1000); - }); -} -``` - -### `handleDataRequest` - -You can export an optional `handleDataRequest` function that will allow you to modify the response of a data request. These are the requests that do not render HTML, but rather return the loader and action data to the browser once client-side hydration has occurred. - -```tsx -export function handleDataRequest( - response: Response, - { - request, - params, - context, - }: LoaderFunctionArgs | ActionFunctionArgs -) { - response.headers.set("X-Custom-Header", "value"); - return response; -} -``` - -### `handleError` - -By default, React Router will log encountered server-side errors to the console. If you'd like more control over the logging, or would like to also report these errors to an external service, then you can export an optional `handleError` function which will give you control (and will disable the built-in error logging). - -```tsx -export function handleError( - error: unknown, - { - request, - params, - context, - }: LoaderFunctionArgs | ActionFunctionArgs -) { - if (!request.signal.aborted) { - sendErrorToErrorReportingService(error); - console.error(formatErrorForJsonLogging(error)); - } -} -``` - -_Note that you generally want to avoid logging when the request was aborted, since React Router's cancellation and race-condition handling can cause a lot of requests to be aborted._ - -### Streaming Rendering Errors - -When you are streaming your HTML responses via [`renderToPipeableStream`][rendertopipeablestream] or [`renderToReadableStream`][rendertoreadablestream], your own `handleError` implementation will only handle errors encountered during the initial shell render. If you encounter a rendering error during subsequent streamed rendering you will need to handle these errors manually since the React Router server has already sent the Response by that point. - -For `renderToPipeableStream`, you can handle these errors in the `onError` callback function. You will need to toggle a boolean in `onShellReady` so you know if the error was a shell rendering error (and can be ignored) or an async - -For an example, please refer to the default [`entry.server.tsx`][node-streaming-entry-server] for Node. - -**Thrown Responses** - -Note that this does not handle thrown `Response` instances from your `loader`/`action` functions. The intention of this handler is to find bugs in your code which result in unexpected thrown errors. If you are detecting a scenario and throwing a 401/404/etc. `Response` in your `loader`/`action` then it's an expected flow that is handled by your code. If you also wish to log, or send those to an external service, that should be done at the time you throw the response. - -## `.server` modules - -While not strictly necessary, `.server` modules are a good way to explicitly mark entire modules as server-only. -The build will fail if any code in a `.server` file or `.server` directory accidentally ends up in the client module graph. - -```txt -app -โ”œโ”€โ”€ .server ๐Ÿ‘ˆ marks all files in this directory as server-only -โ”‚ โ”œโ”€โ”€ auth.ts -โ”‚ โ””โ”€โ”€ db.ts -โ”œโ”€โ”€ cms.server.ts ๐Ÿ‘ˆ marks this file as server-only -โ”œโ”€โ”€ root.tsx -โ””โ”€โ”€ routes.ts -``` - -`.server` modules must be within your app directory. - -Refer to the Route Module section in the sidebar for more information. - -## `.client` modules - -While uncommon, you may have a file or dependency that uses module side effects in the browser. You can use `*.client.ts` on file names or nest files within `.client` directories to force them out of server bundles. - -```ts filename=feature-check.client.ts -// this would break the server -export const supportsVibrationAPI = - "vibrate" in window.navigator; -``` - -Note that values exported from this module will all be `undefined` on the server, so the only places to use them are in [`useEffect`][use_effect] and user events like click handlers. - -```ts -import { supportsVibrationAPI } from "./feature-check.client.ts"; - -console.log(supportsVibrationAPI); -// server: undefined -// client: true | false -``` - -[react-router-config]: https://api.reactrouter.com/v7/types/_react_router_dev.config.Config.html -[route-module]: ../start/framework/route-module -[routing]: ../start/framework/routing -[server-entry]: #entryservertsx -[client-entry]: #entryclienttsx -[rendertopipeablestream]: https://react.dev/reference/react-dom/server/renderToPipeableStream -[rendertoreadablestream]: https://react.dev/reference/react-dom/server/renderToReadableStream -[node-streaming-entry-server]: https://github.com/remix-run/react-router/blob/dev/packages/react-router-dev/config/defaults/entry.server.node.tsx -[streaming]: ../how-to/suspense -[use_effect]: https://react.dev/reference/react/useEffect +- [`react-router.config.ts`](../api/framework-conventions/react-router.config.ts) - Optional configuration file for your app +- [`root.tsx`](../api/framework-conventions/root.tsx) - Required root route that renders the HTML document +- [`routes.ts`](../api/framework-conventions/routes.ts) - Required route configuration mapping URLs to components +- [`entry.client.tsx`](../api/framework-conventions/entry.client.tsx) - Optional client-side entry point for hydration +- [`entry.server.tsx`](../api/framework-conventions/entry.server.tsx) - Optional server-side entry point for rendering +- [`.server` modules](../api/framework-conventions/server-modules) - Server-only modules excluded from client bundles +- [`.client` modules](../api/framework-conventions/client-modules) - Client-only modules excluded from server bundles diff --git a/docs/explanation/state-management.md b/docs/explanation/state-management.md index 7df9ffd6ef..ae1284c775 100644 --- a/docs/explanation/state-management.md +++ b/docs/explanation/state-management.md @@ -4,6 +4,11 @@ title: State Management # State Management +[MODES: framework, data] + +<br/> +<br/> + State management in React typically involves maintaining a synchronized cache of server data on the client side. However, when using React Router as your framework, most of the traditional caching solutions become redundant because of how it inherently handles data synchronization. ## Understanding State Management in React @@ -25,14 +30,12 @@ React Router seamlessly bridges the gap between the backend and frontend via mec Here's why using typical React state patterns might be an anti-pattern in React Router: 1. **Network-related State:** If your React state is managing anything related to the networkโ€”such as data from loaders, pending form submissions, or navigational statesโ€”it's likely that you're managing state that React Router already manages: - - **[`useNavigation`][use_navigation]**: This hook gives you access to `navigation.state`, `navigation.formData`, `navigation.location`, etc. - **[`useFetcher`][use_fetcher]**: This facilitates interaction with `fetcher.state`, `fetcher.formData`, `fetcher.data` etc. - **[`loaderData`][loader_data]**: Access the data for a route. - **[`actionData`][action_data]**: Access the data from the latest action. 2. **Storing Data in React Router:** A lot of data that developers might be tempted to store in React state has a more natural home in React Router, such as: - - **URL Search Params:** Parameters within the URL that hold state. - **[Cookies][cookies]:** Small pieces of data stored on the user's device. - **[Server Sessions][sessions]:** Server-managed user sessions. @@ -80,7 +83,7 @@ export function List() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [view, setView] = useState( - searchParams.get("view") || "list" + searchParams.get("view") || "list", ); return ( @@ -226,7 +229,7 @@ In this approach, state must be initialized within an effect. This is crucial to function Sidebar() { const [isOpen, setIsOpen] = useState( // error: window is not defined - window.localStorage.getItem("sidebar") + window.localStorage.getItem("sidebar"), ); // ... diff --git a/docs/explanation/type-safety.md b/docs/explanation/type-safety.md index 844cc1bd0b..734a24a033 100644 --- a/docs/explanation/type-safety.md +++ b/docs/explanation/type-safety.md @@ -4,6 +4,11 @@ title: Type Safety # Type Safety +[MODES: framework] + +<br/> +<br/> + If you haven't done so already, check out our guide for [setting up type safety][route-module-type-safety] in a new project. React Router generates types for each route in your app to provide type safety for the route module exports. diff --git a/docs/how-to/accessibility.md b/docs/how-to/accessibility.md new file mode 100644 index 0000000000..287dd95ceb --- /dev/null +++ b/docs/how-to/accessibility.md @@ -0,0 +1,44 @@ +--- +title: Accessibility +--- + +# Accessibility + +Accessibility in a React Router app looks a lot like accessibility on the web in general. Using proper semantic markup and following the [Web Content Accessibility Guidelines (WCAG)][wcag] will get you most of the way there. + +React Router makes certain accessibility practices the default where possible and provides APIs to help where it's not. + +## Links + +[MODES: framework, data, declarative] + +<br/> +<br/> + +The [`<Link>` component][link] renders a standard anchor tag, meaning that you get its accessibility behaviors from the browser for free! + +React Router also provides the [`<NavLink/>`][navlink] which behaves the same as `<Link>`, but it also provides context for assistive technology when the link points to the current page. This is useful for building navigation menus or breadcrumbs. + +## Routing + +[MODES: framework] + +<br/> +<br/> + +If you are rendering [`<Scripts>`][scripts] in your app, there are some important things to consider to make client-side routing more accessible for your users. + +With a traditional multi-page website we don't have to think about route changes too much. Your app renders an anchor tag, and the browser handles the rest. If your users disable JavaScript, your React Router app should already work this way by default! + +When the client scripts in React Router are loaded, React Router takes control of routing and prevents the browser's default behavior. React Router doesn't make any assumptions about your UI as the route changes. There are some important features you'll want to consider as a result, including: + +- **Focus management:** What element receives focus when the route changes? This is important for keyboard users and can be helpful for screen-reader users. +- **Live-region announcements:** Screen-reader users also benefit from announcements when a route has changed. You may want to also notify them during certain transition states depending on the nature of the change and how long loading is expected to take. + +In 2019, [Marcy Sutton led and published findings from user research][marcy-sutton-led-and-published-findings-from-user-research] to help developers build accessible client-side routing experiences. + +[link]: ../api/components/Link +[navlink]: ../api/components/NavLink +[scripts]: ../api/components/Scripts +[wcag]: https://www.w3.org/WAI/standards-guidelines/wcag/ +[marcy-sutton-led-and-published-findings-from-user-research]: https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing diff --git a/docs/how-to/client-data.md b/docs/how-to/client-data.md index feef583030..18f211b2cf 100644 --- a/docs/how-to/client-data.md +++ b/docs/how-to/client-data.md @@ -4,6 +4,11 @@ title: Client Data # Client Data +[MODES: framework] + +<br/> +<br/> + You can fetch and mutate data directly in the browser using `clientLoader` and `clientAction` functions. These functions are the primary mechanism for data handling when using [SPA mode][spa]. This guide demonstrates common use cases for leveraging client data in Server-Side Rendering (SSR). diff --git a/docs/how-to/error-boundary.md b/docs/how-to/error-boundary.md index 3bd4daa6e8..c8e7f79daa 100644 --- a/docs/how-to/error-boundary.md +++ b/docs/how-to/error-boundary.md @@ -4,6 +4,11 @@ title: Error Boundaries # Error Boundaries +[MODES: framework, data] + +<br/> +<br/> + To avoid rendering an empty page to users, route modules will automatically catch errors in your code and render the closest `ErrorBoundary`. Error boundaries are not intended for error reporting or rendering form validation errors. Please see [Form Validation](./form-validation) and [Error Reporting](./error-reporting) instead. diff --git a/docs/how-to/error-reporting.md b/docs/how-to/error-reporting.md index 685d0ea0f6..bd42d2b5fa 100644 --- a/docs/how-to/error-reporting.md +++ b/docs/how-to/error-reporting.md @@ -4,6 +4,11 @@ title: Error Reporting # Error Reporting +[MODES: framework] + +<br/> +<br/> + React Router catches errors in your route modules and sends them to [error boundaries](./error-boundary) to prevent blank pages when errors occur. However, ErrorBoundary isn't sufficient for logging and reporting errors. To access these caught errors, use the handleError export of the server entry module. ## 1. Reveal the server entry @@ -23,7 +28,7 @@ import { type HandleErrorFunction } from "react-router"; export const handleError: HandleErrorFunction = ( error, - { request } + { request }, ) => { // React Router may abort some interrupted requests, don't log those if (!request.signal.aborted) { diff --git a/docs/how-to/fetchers.md b/docs/how-to/fetchers.md index d956f12abe..c6ac988d93 100644 --- a/docs/how-to/fetchers.md +++ b/docs/how-to/fetchers.md @@ -4,6 +4,11 @@ title: Using Fetchers # Using Fetchers +[MODES: framework, data] + +<br/> +<br/> + Fetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation. Fetchers track their own, independent state and can be used to load data, mutate data, submit forms, and generally interact with loaders and actions. @@ -190,7 +195,7 @@ export async function loader({ request }) { let url = new URL(request.url); let query = url.searchParams.get("q"); return users.filter((user) => - user.name.toLowerCase().includes(query.toLowerCase()) + user.name.toLowerCase().includes(query.toLowerCase()), ); } ``` @@ -219,10 +224,10 @@ export function UserSearchCombobox() { ```tsx lines=[2,5] import { useFetcher } from "react-router"; -import type { Search } from "./search-users"; +import type { loader } from "./search-users"; export function UserSearchCombobox() { - let fetcher = useFetcher<typeof Search.action>(); + let fetcher = useFetcher<typeof loader>(); // ... } ``` @@ -235,7 +240,7 @@ Ensure you use `import type` so you only import the types. import { useFetcher } from "react-router"; export function UserSearchCombobox() { - let fetcher = useFetcher<typeof Search.action>(); + let fetcher = useFetcher<typeof loader>(); return ( <div> <fetcher.Form method="get" action="/search-users"> @@ -261,7 +266,7 @@ Note you will need to hit "enter" to submit the form and see the results. import { useFetcher } from "react-router"; export function UserSearchCombobox() { - let fetcher = useFetcher<typeof Search.action>(); + let fetcher = useFetcher<typeof loader>(); return ( <div> <fetcher.Form method="get" action="/search-users"> diff --git a/docs/how-to/file-route-conventions.md b/docs/how-to/file-route-conventions.md index 9903cbe81f..43f937b5d8 100644 --- a/docs/how-to/file-route-conventions.md +++ b/docs/how-to/file-route-conventions.md @@ -4,6 +4,11 @@ title: File Route Conventions # File Route Conventions +[MODES: framework] + +<br/> +<br/> + The `@react-router/fs-routes` package enables file-convention based route config. ## Setting up @@ -117,7 +122,7 @@ Usually your URLs aren't static but data-driven. Dynamic segments allow you to m The value will be parsed from the URL and passed to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders] and [actions]. ```tsx -export async function serverLoader({ params }) { +export async function loader({ params }) { return fakeDb.getAllConcertsForCity(params.city); } ``` @@ -127,7 +132,7 @@ You'll note the property name on the `params` object maps directly to the name o Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name: ```tsx -export async function serverLoader({ params }) { +export async function loader({ params }) { return fake.db.getConcerts({ date: params.date, city: params.city, @@ -285,12 +290,30 @@ While [dynamic segments][dynamic_segments] match a single path segment (the stuf Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key. ```tsx filename=app/routes/files.$.tsx -export async function serverLoader({ params }) { +export async function loader({ params }) { const filePath = params["*"]; return fake.getFileInfo(filePath); } ``` +## Catch-all Route + +To create a route that will match any requests that don't match other defined routes (such as a 404 page), create a file named `$.tsx` within your routes directory: + +| URL | Matched Route | +| ------------------------------ | ----------------------- | +| `/` | `app/routes/_index.tsx` | +| `/about` | `app/routes/about.tsx` | +| `/any-invalid-path-will-match` | `app/routes/$.tsx` | + +By default the matched route will return a 200 response, so be sure to modify your catchall route to return a 404 instead: + +```tsx filename=app/routes/$.tsx +export async function loader() { + return data({}, 404); +} +``` + ## Escaping Special Characters If you want one of the special characters used for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. This can be especially helpful for [resource routes][resource_routes] that include an extension in the URL. diff --git a/docs/how-to/file-uploads.md b/docs/how-to/file-uploads.md index d77f064a06..d3f049bd4b 100644 --- a/docs/how-to/file-uploads.md +++ b/docs/how-to/file-uploads.md @@ -4,7 +4,10 @@ title: File Uploads # File Uploads -Handle file uploads in your React Router applications. This guide uses some packages from the [Remix The Web][remix-the-web] project to make file uploads easier. +[MODES: framework] + +<br/> +<br/> _Thank you to David Adams for [writing an original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) on which this doc is based. You can refer to it for even more examples._ @@ -33,7 +36,7 @@ export default [ `form-data-parser` is a wrapper around `request.formData()` that provides streaming support for handling file uploads. ```shellscript -npm i @mjackson/form-data-parser +npm i @remix-run/form-data-parser ``` [See the `form-data-parser` docs for more information][form-data-parser] @@ -52,11 +55,12 @@ You must set the form's `enctype` to `multipart/form-data` for file uploads to w import { type FileUpload, parseFormData, -} from "@mjackson/form-data-parser"; +} from "@remix-run/form-data-parser"; +import type { Route } from "./+types/user-profile"; export async function action({ request, -}: ActionFunctionArgs) { +}: Route.ActionArgs) { const uploadHandler = async (fileUpload: FileUpload) => { if (fileUpload.fieldName === "avatar") { // process the upload and return a File @@ -65,7 +69,7 @@ export async function action({ const formData = await parseFormData( request, - uploadHandler + uploadHandler, ); // 'avatar' has already been processed at this point const file = formData.get("avatar"); @@ -88,7 +92,7 @@ export default function Component() { `file-storage` is a key/value interface for storing [File objects][file] in JavaScript. Similar to how `localStorage` allows you to store key/value pairs of strings in the browser, file-storage allows you to store key/value pairs of files on the server. ```shellscript -npm i @mjackson/file-storage +npm i @remix-run/file-storage ``` [See the `file-storage` docs for more information][file-storage] @@ -98,10 +102,10 @@ npm i @mjackson/file-storage Create a file that exports a `LocalFileStorage` instance to be used by different routes. ```ts filename=avatar-storage.server.ts -import { LocalFileStorage } from "@mjackson/file-storage/local"; +import { LocalFileStorage } from "@remix-run/file-storage/local"; export const fileStorage = new LocalFileStorage( - "./uploads/avatars" + "./uploads/avatars", ); export function getStorageKey(userId: string) { @@ -117,7 +121,7 @@ Update the form's `action` to store files in the `fileStorage` instance. import { type FileUpload, parseFormData, -} from "@mjackson/form-data-parser"; +} from "@remix-run/form-data-parser"; import { fileStorage, getStorageKey, @@ -148,7 +152,7 @@ export async function action({ const formData = await parseFormData( request, - uploadHandler + uploadHandler, ); } @@ -207,8 +211,7 @@ export async function loader({ params }: Route.LoaderArgs) { } ``` -[remix-the-web]: https://github.com/mjackson/remix-the-web -[form-data-parser]: https://github.com/mjackson/remix-the-web/tree/main/packages/form-data-parser -[file-storage]: https://github.com/mjackson/remix-the-web/tree/main/packages/file-storage +[form-data-parser]: https://www.npmjs.com/package/@remix-run/form-data-parser +[file-storage]: https://www.npmjs.com/package/@remix-run/file-storage [file]: https://developer.mozilla.org/en-US/docs/Web/API/File [resource-route]: ../how-to/resource-routes diff --git a/docs/how-to/form-validation.md b/docs/how-to/form-validation.md index 7376c13f19..0c9236aea2 100644 --- a/docs/how-to/form-validation.md +++ b/docs/how-to/form-validation.md @@ -4,6 +4,11 @@ title: Form Validation # Form Validation +[MODES: framework, data] + +<br/> +<br/> + This guide walks through a simple signup form implementation. You will likely want to pair these concepts with third-party validation libraries and error components, but this guide only focuses on the moving pieces for React Router. ## 1. Setting Up @@ -84,7 +89,7 @@ export async function action({ If any validation errors are found, they are returned from the `action` to the fetcher. This is our way of signaling to the UI that something needs to be corrected, otherwise the user will be redirected to the dashboard. -Note the `data({ errors }, { status: 400 })` call. Setting a 400 status is the web standard way to signal to the client that there was a validation error (Bad Request). In React Router, only 200 status codes trigger page data revalidation so a 400 prevent that. +Note the `data({ errors }, { status: 400 })` call. Setting a 400 status is the web standard way to signal to the client that there was a validation error (Bad Request). In React Router, only 2xx status codes trigger page data revalidation, so sending a 400 status prevents the normal revalidation that would occur after an `action`. ## 3. Displaying Validation Errors diff --git a/docs/how-to/headers.md b/docs/how-to/headers.md index 19e6588ec4..5b5ee9ad48 100644 --- a/docs/how-to/headers.md +++ b/docs/how-to/headers.md @@ -4,6 +4,11 @@ title: HTTP Headers # HTTP Headers +[MODES: framework] + +<br/> +<br/> + Headers are primarily defined with the route module `headers` export. You can also set headers in `entry.server.tsx`. ## From Route Modules @@ -34,7 +39,7 @@ import { data } from "react-router"; export async function loader({ params }: LoaderArgs) { let [page, ms] = await fakeTimeCall( - await getPage(params.id) + await getPage(params.id), ); return data(page, { @@ -50,11 +55,17 @@ export async function loader({ params }: LoaderArgs) { Headers from loaders and actions are not sent automatically. You must explicitly return them from the `headers` export. ```tsx +function hasAnyHeaders(headers: Headers): boolean { + return [...headers].length > 0; +} + export function headers({ actionHeaders, loaderHeaders, }: HeadersArgs) { - return actionHeaders ? actionHeaders : loaderHeaders; + return hasAnyHeaders(actionHeaders) + ? actionHeaders + : loaderHeaders; } ``` @@ -81,7 +92,7 @@ The easiest way is to simply append to the parent headers. This avoids overwriti ```tsx export function headers({ parentHeaders }: HeadersArgs) { parentHeaders.append( - "Permissions-Policy: geolocation=()" + "Permissions-Policy: geolocation=()", ); return parentHeaders; } @@ -95,7 +106,7 @@ Sometimes it's important to overwrite the parent header. Do this with `set` inst export function headers({ parentHeaders }: HeadersArgs) { parentHeaders.set( "Cache-Control", - "max-age=3600, s-maxage=86400" + "max-age=3600, s-maxage=86400", ); return parentHeaders; } @@ -108,17 +119,17 @@ You can avoid the need to merge headers by only defining headers in "leaf routes The `handleRequest` export receives the headers from the route module as an argument. You can append global headers here. ```tsx -export default function handleRequest( +export default async function handleRequest( request, responseStatusCode, responseHeaders, routerContext, - loadContext + loadContext, ) { // set, append global headers responseHeaders.set( "X-App-Version", - routerContext.manifest.version + routerContext.manifest.version, ); return new Response(await getStream(), { diff --git a/docs/how-to/middleware.md b/docs/how-to/middleware.md new file mode 100644 index 0000000000..be7e84acfa --- /dev/null +++ b/docs/how-to/middleware.md @@ -0,0 +1,736 @@ +--- +title: Middleware +unstable: true +--- + +# Middleware + +[MODES: framework, data] + +<br/> +<br/> + +<docs-warning>The middleware feature is currently experimental and subject to breaking changes. Use the `future.unstable_middleware` flag to enable it.</docs-warning> + +Middleware allows you to run code before and after the [`Response`][Response] generation for the matched path. This enables [common patterns][common-patterns] like authentication, logging, error handling, and data preprocessing in a reusable way. + +Middleware runs in a nested chain, executing from parent routes to child routes on the way "down" to your route handlers, then from child routes back to parent routes on the way "up" after a [`Response`][Response] is generated. + +For example, on a `GET /parent/child` request, the middleware would run in the following order: + +```text +- Root middleware start + - Parent middleware start + - Child middleware start + - Run loaders, generate HTML Response + - Child middleware end + - Parent middleware end +- Root middleware end +``` + +<docs-info>There are some slight differences between middleware on the server (framework mode) versus the client (framework/data mode). For the purposes of this document, we'll be referring to Server Middleware in most of our examples as it's the most familiar to users who've used middleware in other HTTP servers in the past. Please refer to the [Server vs Client Middleware][server-client] section below for more information.</docs-info> + +## Quick Start (Framework mode) + +### 1. Enable the middleware flag + +First, enable middleware in your [React Router config][rr-config]: + +```ts filename=react-router.config.ts +import type { Config } from "@react-router/dev/config"; + +export default { + future: { + unstable_middleware: true, + }, +} satisfies Config; +``` + +<docs-warning>By enabling the middleware feature, you change the type of the `context` parameter to your [`action`][framework-action]s and [`loader`][framework-loader]s. Please pay attention to the section on [`getLoadContext`][getloadcontext] below if you are actively using `context` today.</docs-warning> + +### 2. Create a context + +Middleware uses a `context` provider instance to provide data down the middleware chain. +You can create type-safe context objects using [`unstable_createContext`][createContext]: + +```ts filename=app/context.ts +import { unstable_createContext } from "react-router"; +import type { User } from "~/types"; + +export const userContext = + unstable_createContext<User | null>(null); +``` + +### 3. Export middleware from your routes + +```tsx filename=app/routes/dashboard.tsx +import { redirect } from "react-router"; +import { userContext } from "~/context"; + +// Server-side Authentication Middleware +async function authMiddleware({ request, context }) { + const user = await getUserFromSession(request); + if (!user) { + throw redirect("/login"); + } + context.set(userContext, user); +} + +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [authMiddleware]; + +// Client-side timing middleware +async function timingMiddleware({ context }, next) { + const start = performance.now(); + await next(); + const duration = performance.now() - start; + console.log(`Navigation took ${duration}ms`); +} + +export const unstable_clientMiddleware: Route.unstable_ClientMiddlewareFunction[] = + [timingMiddleware]; + +export async function loader({ + context, +}: Route.LoaderArgs) { + const user = context.get(userContext); + const profile = await getProfile(user); + return { profile }; +} + +export default function Dashboard({ + loaderData, +}: Route.ComponentProps) { + return ( + <div> + <h1>Welcome {loaderData.profile.fullName}!</h1> + <Profile profile={loaderData.profile} /> + </div> + ); +} +``` + +### 4. Update your `getLoadContext` function (if applicable) + +If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of [`unstable_RouterContextProvider`][RouterContextProvider], instead of a JavaScript object: + +```diff ++import { ++ unstable_createContext, ++ unstable_RouterContextProvider, ++} from "react-router"; +import { createDb } from "./db"; + ++const dbContext = unstable_createContext<Database>(); + +function getLoadContext(req, res) { +- return { db: createDb() }; ++ const context = new unstable_RouterContextProvider(); ++ context.set(dbContext, createDb()); ++ return context; +} +``` + +## Quick Start (Data Mode) + +<docs-info>Note there is no future flag in Data Mode because you can opt-into middleware by adding it to your routes, no breaking changes exist that require a future flag.</docs-info> + +### 1. Create a context + +Middleware uses a `context` provider instance to provide data down the middleware chain. +You can create type-safe context objects using [`unstable_createContext`][createContext]: + +```ts +import { unstable_createContext } from "react-router"; +import type { User } from "~/types"; + +export const userContext = + unstable_createContext<User | null>(null); +``` + +### 2. Add middleware to your routes + +```tsx +import { redirect } from "react-router"; +import { userContext } from "~/context"; + +const routes = [ + { + path: "/", + unstable_middleware: [timingMiddleware], // ๐Ÿ‘ˆ + Component: Root, + children: [ + { + path: "profile", + unstable_middleware: [authMiddleware], // ๐Ÿ‘ˆ + loader: profileLoader, + Component: Profile, + }, + { + path: "login", + Component: Login, + }, + ], + }, +]; + +async function timingMiddleware({ context }, next) { + const start = performance.now(); + await next(); + const duration = performance.now() - start; + console.log(`Navigation took ${duration}ms`); +} + +async function authMiddleware({ context }) { + const user = await getUser(); + if (!user) { + throw redirect("/login"); + } + context.set(userContext, user); +} + +export async function profileLoader({ + context, +}: Route.LoaderArgs) { + const user = context.get(userContext); + const profile = await getProfile(user); + return { profile }; +} + +export default function Profile() { + let loaderData = useLoaderData(); + return ( + <div> + <h1>Welcome {loaderData.profile.fullName}!</h1> + <Profile profile={loaderData.profile} /> + </div> + ); +} +``` + +### 3. Add an `unstable_getContext` function (optional) + +If you wish to include a base context on all navigations/fetches, you can add an [`unstable_getContext`][getContext] function to your router. This will be called to populate a fresh context on every navigation/fetch. + +```tsx +let sessionContext = unstable_createContext(); + +const router = createBrowserRouter(routes, { + unstable_getContext() { + let context = new unstable_RouterContextProvider(); + context.set(sessionContext, getSession()); + return context; + }, +}); +``` + +<docs-info>This API exists to mirror the `getLoadContext` API on the server in Framework Mode, which exists as a way to hand off values from your HTTP server to the React Router handler. This [`unstable_getContext`][getContext] API can be used to hand off global values from the [`window`][window]/[`document`][document] to React Router, but because they're all running in the same context (the browser), you can achieve effectively the same behavior with root route middleware. Therefore, you may not need this API the same way you would on the server - but it's provided for consistency.</docs-warning> + +## Core Concepts + +### Server vs Client Middleware + +Server middleware runs on the server in Framework mode for HTML Document requests and `.data` requests for subsequent navigations and fetcher calls. Because server middleware runs on the server in response to an HTTP [`Request`][request], it returns an HTTP [`Response`][Response] back up the middleware chain via the `next` function: + +```ts +async function serverMiddleware({ request }, next) { + console.log(request.method, request.url); + let response = await next(); + console.log(response.status, request.method, request.url); + return response; +} + +// Framework mode only +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [serverMiddleware]; +``` + +Client middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware differs from server middleware because there's no HTTP Request, so it doesn't have a `Response` to bubble up. In most cases, you can just ignore the return value from `next` and return nothing from your middleware on the client: + +```ts +async function clientMiddleware({ request }, next) { + console.log(request.method, request.url); + await next(); + console.log(response.status, request.method, request.url); +} + +// Framework mode +export const unstable_clientMiddleware: Route.unstable_MiddlewareFunction[] = + [clientMiddleware]; + +// Or, Data mode +const route = { + path: "/", + unstable_middleware: [clientMiddleware], + loader: rootLoader, + Component: Root, +}; +``` + +There may be _some_ cases where you want to do some post-processing based on the result of the loaders/action. In lieu of a `Response`, client middleware bubbles up the value returned from the active [`dataStrategy`][datastrategy] (`Record<string, DataStrategyResult>` - keyed by route id). This allows you to take conditional action in your middleware based on the outcome of the executed `loader`/`action` functions. + +Here's an example of the [CMS Redirect on 404][cms-redirect] use case implemented as a client side middleware: + +```tsx +async function cmsFallbackMiddleware({ request }, next) { + const results = await next(); + + // Check if we got a 404 from any of our routes and if so, look for a + // redirect in our CMS + const found404 = Object.values(results).some( + (r) => + isRouteErrorResponse(r.result) && + r.result.status === 404, + ); + if (found404) { + const cmsRedirect = await checkCMSRedirects( + request.url, + ); + if (cmsRedirect) { + throw redirect(cmsRedirect, 302); + } + } +} +``` + +<docs-warning>In a server middleware, you shouldn't be messing with the `Response` body and should only be reading status/headers and setting headers. Similarly, this value should be considered read-only in client middleware because it represents the "body" or "data" for the resulting navigation which should be driven by loaders/actions - not middleware. This also means that in client middleware, there's usually no need to return the results even if you needed to capture it from `await next()`;</docs-warning> + +### When Middleware Runs + +It is very important to understand _when_ your middlewares will run to make sure your application is behaving as you intend. + +#### Server Middleware + +In a hydrated Framework Mode app, server middleware is designed such that it prioritizes SPA behavior and does not create new network activity by default. Middleware wraps _existing_ requests and only runs when you _need_ to hit the server. + +This raises the question of what is a "handler" in React Router? Is it the route? Or the `loader`? We think "it depends": + +- On document requests (`GET /route`), the handler is the route โ€” because the response encompasses both the `loader` and the route component +- On data requests (`GET /route.data`) for client-side navigations, the handler is the [`action`][data-action]/[`loader`][data-loader], because that's all that is included in the response + +Therefore: + +- Document requests run server middleware whether `loader`s exist or not because we're still in a "handler" to render the UI +- Client-side navigations will only run server middleware if a `.data` request is made to the server for a [`action`][framework-action]/[`loader`][framework-loader] + +This is important behavior for request-annotation middlewares such as logging request durations, checking/setting sessions, setting outgoing caching headers, etc. It would be useless to go to the server and run those types of middlewares when there was no reason to go to the server in the first place. This would result in increased server load and noisy server logs. + +```tsx filename=app/root.tsx +// This middleware won't run on client-side navigations without a `.data` request +async function loggingMiddleware({ request }, next) { + console.log(`Request: ${request.method} ${request.url}`); + let response = await next(); + console.log( + `Response: ${response.status} ${request.method} ${request.url}`, + ); + return response; +} + +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [loggingMiddleware]; +``` + +However, there may be cases where you _want_ to run certain server middlewares on _every_ client-navigation - even if no `loader` exists. For example, a form in the authenticated section of your site that doesn't require a `loader` but you'd rather use auth middleware to redirect users away before they fill out the form โ€” rather than when they submit to the `action`. If your middleware meets these criteria, then you can put a `loader` on the route that contains the middleware to force it to always call the server for client-side navigations involving that route. + +```tsx filename=app/_auth.tsx +function authMiddleware({ request }, next) { + if (!isLoggedIn(request)) { + throw redirect("/login"); + } +} + +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [authMiddleware]; + +// By adding a `loader`, we force the `authMiddleware` to run on every +// client-side navigation involving this route. +export async function loader() { + return null; +} +``` + +#### Client Middleware + +Client middleware is simpler because since we are already on the client and are always making a "request" to the router when navigating. Client middlewares will run on every client navigation, regardless of whether there are `loader`s to run. + +### Context API + +The new context system provides type safety and prevents naming conflicts and allows you to provide data to nested middlewares and `action`/`loader` functions. In Framework Mode, this replaces the previous `AppLoadContext` API. + +```ts +// โœ… Type-safe +import { unstable_createContext } from "react-router"; +const userContext = unstable_createContext<User>(); + +// Later in middleware/`loader`s +context.set(userContext, user); // Must be `User` type +const user = context.get(userContext); // Returns `User` type + +// โŒ Old way (no type safety) +context.user = user; // Could be anything +``` + +#### `Context` and `AsyncLocalStorage` + +Node provides an [`AsyncLocalStorage`][asynclocalstorage] API which gives you a way to provide values through asynchronous execution contexts. While this is a Node API, most modern runtimes have made it (mostly) available (i.e., [Cloudflare][cloudflare], [Bun][bun], [Deno][deno]). + +In theory, we could have leveraged [`AsyncLocalStorage`][asynclocalstorage] directly as the way to pass values from middlewares to child routes, but the lack of 100% cross-platform compatibility was concerning enough that we wanted to still ship a first-class `context` API so there would be a way to publish reusable middleware packages guaranteed to work in a runtime-agnostic manner. + +That said, this API still works great with React Router middleware and can be used in place of, or alongside of the `context` API: + +<docs-info>[`AsyncLocalStorage`][asynclocalstorage] is _especially_ powerful when using [React Server Components](../how-to/react-server-components) because it allows you to provide information from `middleware` to your Server Components and Server Actions because they run in the same server execution context ๐Ÿคฏ</docs-info> + +```tsx filename=app/user-context.ts +import { AsyncLocalStorage } from "node:async_hooks"; + +const USER = new AsyncLocalStorage<User>(); + +export async function provideUser( + request: Request, + cb: () => Promise<Response>, +) { + let user = await getUser(request); + return USER.run(user, cb); +} + +export function getUser() { + return USER.getStore(); +} +``` + +```tsx filename=app/root.tsx +import { provideUser } from "./user-context"; + +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [ + async ({ request, context }, next) => { + return provideUser(request, async () => { + let res = await next(); + return res; + }); + }, + ]; +``` + +```tsx filename=app/routes/_index.tsx +import { getUser } from "../user-context"; + +export async function loader() { + let user = getUser(); + //... +} +``` + +### The `next` function + +The `next` function logic depends on which route middleware it's being called from: + +- When called from a non-leaf middleware, it runs the next middleware in the chain +- When called from the leaf middleware, it executes any route handlers and generates the resulting [`Response`][Response] for the request + +```ts +const middleware = async ({ context }, next) => { + // Code here runs BEFORE handlers + console.log("Before"); + + const response = await next(); + + // Code here runs AFTER handlers + console.log("After"); + + return response; // Optional on client, required on server +}; +``` + +<docs-warning>You can only call `next()` once per middleware. Calling it multiple times will throw an error</docs-warning> + +### Skipping `next()` + +If you don't need to run code after your handlers, you can skip calling `next()`: + +```ts +const authMiddleware = async ({ request, context }) => { + const user = await getUser(request); + if (!user) { + throw redirect("/login"); + } + context.set(userContext, user); + // next() is called automatically +}; +``` + +### `next()` and Error Handling + +React Router contains built-in error handling via the route [`ErrorBoundary`][ErrorBoundary] export. Just like when a `action`/`loader` throws, if a `middleware` throws it will be caught and handled at the appropriate [`ErrorBoundary`][ErrorBoundary] and a [`Response`][Response] will be returned through the ancestor `next()` call. This means that the `next()` function should never throw and should always return a [`Response`][Response], so you don't need to worry about wrapping it in a try/catch. + +This behavior is important to allow middleware patterns such as automatically setting required headers on outgoing responses (i.e., committing a session) from a root `middleware`. If any error from a `middleware` caused `next()` to `throw`, we'd miss the execution of ancestor middlewares on the way out and those required headers wouldn't be set. + +```tsx filename=routes/parent.tsx +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [ + async (_, next) => { + let res = await next(); + // ^ res.status = 500 + // This response contains the ErrorBoundary + return res; + }, + ]; +``` + +```tsx filename=routes/parent.child.tsx +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [ + async (_, next) => { + let res = await next(); + // ^ res.status = 200 + // This response contains the successful UI render + throw new Error("Uh oh, something went wrong!"); + }, + ]; +``` + +## Changes to `getLoadContext`/`AppLoadContext` + +<docs-info>This only applies if you are using a custom server and a custom `getLoadContext` function</docs-info> + +Middleware introduces a breaking change to the `context` parameter generated by `getLoadContext` and passed to your `action`s and `loader`s. The current approach of a module-augmented `AppLoadContext` isn't really type-safe and instead just sort of tells TypeScript to "trust me". + +Middleware needs an equivalent `context` on the client for `clientMiddleware`, but we didn't want to duplicate this pattern from the server that we already weren't thrilled with, so we decided to introduce a new API where we could tackle type-safety. + +When opting into middleware, the `context` parameter changes to an instance of [`unstable_RouterContextProvider`][RouterContextProvider]: + +```ts +let dbContext = unstable_createContext<Database>(); +let context = new unstable_RouterContextProvider(); +context.set(dbContext, getDb()); +// ^ type-safe +let db = context.get(dbContext); +// ^ Database +``` + +If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of [`unstable_RouterContextProvider`][RouterContextProvider], instead of a plain JavaScript object: + +```diff ++import { ++ unstable_createContext, ++ unstable_RouterContextProvider, ++} from "react-router"; +import { createDb } from "./db"; + ++const dbContext = unstable_createContext<Database>(); + +function getLoadContext(req, res) { +- return { db: createDb() }; ++ const context = new unstable_RouterContextProvider(); ++ context.set(dbContext, createDb()); ++ return context; +} +``` + +### Migration from `AppLoadContext` + +If you're currently using `AppLoadContext`, you can migrate incrementally by using your existing module augmentation to augment [`unstable_RouterContextProvider`][RouterContextProvider] instead of `AppLoadContext`. Then, update your `getLoadContext` function to return an instance of [`unstable_RouterContextProvider`][RouterContextProvider]: + +```diff +declare module "react-router" { +- interface AppLoadContext { ++ interface unstable_RouterContextProvider { + db: Database; + user: User; + } +} + +function getLoadContext() { + const loadContext = {...}; +- return loadContext; ++ let context = new unstable_RouterContextProvider(); ++ Object.assign(context, loadContext); ++ return context; +} +``` + +This allows you to leave your `action`s/`loader`s untouched during initial adoption of middleware, since they can still read values directly (i.e., `context.db`). + +<docs-warning>This approach is only intended to be used as a migration strategy when adopting middleware in React Router v7, allowing you to incrementally migrate to `context.set`/`context.get`. It is not safe to assume this approach will work in the next major version of React Router.</docs-warning> + +<docs-warning>The [`unstable_RouterContextProvider`][RouterContextProvider] class is also used for the client-side `context` parameter via `<HydratedRouter unstable_getContext>` and `<RouterProvider unstable_getContext>`. Since `AppLoadContext` is primarily intended as a hand-off from your HTTP server into the React Router handlers, you need to be aware that these augmented fields will not be available in `clientMiddleware`, `clientLoader`, or `clientAction` functions even thought TypeScript will tell you they are (unless, of course, you provide the fields via `unstable_getContext` on the client).</docs-warning> + +## Common Patterns + +### Authentication + +```tsx filename=app/middleware/auth.ts +import { redirect } from "react-router"; +import { userContext } from "~/context"; +import { getSession } from "~/sessions.server"; + +export const authMiddleware = async ({ + request, + context, +}) => { + const session = await getSession(request); + const userId = session.get("userId"); + + if (!userId) { + throw redirect("/login"); + } + + const user = await getUserById(userId); + context.set(userContext, user); +}; +``` + +```tsx filename=app/routes/protected.tsx +import { authMiddleware } from "~/middleware/auth"; + +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [authMiddleware]; + +export async function loader({ + context, +}: Route.LoaderArgs) { + const user = context.get(userContext); // Guaranteed to exist + return { user }; +} +``` + +### Logging + +```tsx filename=app/middleware/logging.ts +import { requestIdContext } from "~/context"; + +export const loggingMiddleware = async ( + { request, context }, + next, +) => { + const requestId = crypto.randomUUID(); + context.set(requestIdContext, requestId); + + console.log( + `[${requestId}] ${request.method} ${request.url}`, + ); + + const start = performance.now(); + const response = await next(); + const duration = performance.now() - start; + + console.log( + `[${requestId}] Response ${response.status} (${duration}ms)`, + ); + + return response; +}; +``` + +### CMS Redirect on 404 + +```tsx filename=app/middleware/cms-fallback.ts +export const cmsFallbackMiddleware = async ( + { request }, + next, +) => { + const response = await next(); + + // Check if we got a 404 + if (response.status === 404) { + // Check CMS for a redirect + const cmsRedirect = await checkCMSRedirects( + request.url, + ); + if (cmsRedirect) { + throw redirect(cmsRedirect, 302); + } + } + + return response; +}; +``` + +### Response Headers + +```tsx filename=app/middleware/headers.ts +export const headersMiddleware = async ( + { context }, + next, +) => { + const response = await next(); + + // Add security headers + response.headers.set("X-Frame-Options", "DENY"); + response.headers.set("X-Content-Type-Options", "nosniff"); + + return response; +}; +``` + +### Conditional Middleware + +```tsx +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [ + async ({ request, context }, next) => { + // Only run auth for POST requests + if (request.method === "POST") { + await ensureAuthenticated(request, context); + } + return next(); + }, + ]; +``` + +### Sharing Context Between `action` and `loader` + +```tsx +const sharedDataContext = unstable_createContext<any>(); + +export const unstable_middleware: Route.unstable_MiddlewareFunction[] = + [ + async ({ request, context }, next) => { + if (request.method === "POST") { + // Set data during action phase + context.set( + sharedDataContext, + await getExpensiveData(), + ); + } + return next(); + }, + ]; + +export async function action({ + context, +}: Route.ActionArgs) { + const data = context.get(sharedDataContext); + // Use the data... +} + +export async function loader({ + context, +}: Route.LoaderArgs) { + const data = context.get(sharedDataContext); + // Same data is available here +} +``` + +[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response +[common-patterns]: #common-patterns +[server-client]: #server-vs-client-middleware +[rr-config]: ../api/framework-conventions/react-router.config.ts +[framework-action]: ../start/framework/route-module#action +[framework-loader]: ../start/framework/route-module#loader +[getloadcontext]: #changes-to-getloadcontextapploadcontext +[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy +[cms-redirect]: #cms-redirect-on-404 +[createContext]: ../api/utils/createContext +[RouterContextProvider]: ../api/utils/RouterContextProvider +[getContext]: ../api/data-routers/createBrowserRouter#optsunstable_getContext +[window]: https://developer.mozilla.org/en-US/docs/Web/API/Window +[document]: https://developer.mozilla.org/en-US/docs/Web/API/Document +[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request +[data-action]: ../start/data/route-object#action +[data-loader]: ../start/data/route-object#loader +[asynclocalstorage]: https://nodejs.org/api/async_context.html#class-asynclocalstorage +[cloudflare]: https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/ +[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support +[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage +[ErrorBoundary]: ../start/framework/route-module#errorboundary diff --git a/docs/how-to/navigation-blocking.md b/docs/how-to/navigation-blocking.md index 72545c0251..5c6bdf6cc6 100644 --- a/docs/how-to/navigation-blocking.md +++ b/docs/how-to/navigation-blocking.md @@ -6,7 +6,8 @@ title: Navigation Blocking [MODES: framework, data] -## Overview +<br/> +<br/> When users are in the middle of a workflow, like filling out an important form, you may want to prevent them from navigating away from the page. @@ -105,7 +106,7 @@ export default function Contact() { let [isDirty, setIsDirty] = useState(false); let fetcher = useFetcher(); let blocker = useBlocker( - useCallback(() => isDirty, [isDirty]) + useCallback(() => isDirty, [isDirty]), ); // ... existing code @@ -123,7 +124,7 @@ export default function Contact() { let [isDirty, setIsDirty] = useState(false); let fetcher = useFetcher(); let blocker = useBlocker( - useCallback(() => isDirty, [isDirty]) + useCallback(() => isDirty, [isDirty]), ); return ( diff --git a/docs/how-to/pre-rendering.md b/docs/how-to/pre-rendering.md index 28bf95e51d..71e182c423 100644 --- a/docs/how-to/pre-rendering.md +++ b/docs/how-to/pre-rendering.md @@ -4,6 +4,11 @@ title: Pre-Rendering # Pre-Rendering +[MODES: framework] + +<br/> +<br/> + Pre-Rendering allows you to speed up page loads for static content by rendering pages at build time instead of at runtime. Pre-rendering is enabled via the `prerender` config in `react-router.config.ts` and can be used in two ways based on the `ssr` config value: - Alongside a runtime SSR server with `ssr:true` (the default value) diff --git a/docs/how-to/presets.md b/docs/how-to/presets.md new file mode 100644 index 0000000000..100b34d7d2 --- /dev/null +++ b/docs/how-to/presets.md @@ -0,0 +1,103 @@ +--- +title: Presets +--- + +# Presets + +[MODES: framework] + +<br/> +<br/> + +The [React Router config][react-router-config] supports a `presets` option to ease integration with other tools and hosting providers. + +[Presets][preset-type] can only do two things: + +- Configure React Router config options on your behalf +- Validate the resolved config + +The config returned by each preset is merged in the order the presets were defined. Any config directly specified in your React Router config will be merged last. This means that your config will always take precedence over any presets. + +## Defining preset config + +As a basic example, let's create a preset that configures a [server bundles function][server-bundles]: + +```ts filename=my-cool-preset.ts +import type { Preset } from "@react-router/dev/config"; + +export function myCoolPreset(): Preset { + return { + name: "my-cool-preset", + reactRouterConfig: () => ({ + serverBundles: ({ branch }) => { + const isAuthenticatedRoute = branch.some((route) => + route.id.split("/").includes("_authenticated"), + ); + + return isAuthenticatedRoute + ? "authenticated" + : "unauthenticated"; + }, + }), + }; +} +``` + +## Validating config + +Keep in mind that other presets and user config can still override the values returned from your preset. + +In our example preset, the `serverBundles` function could be overridden with a different, conflicting implementation. If we want to validate that the final resolved config contains the `serverBundles` function from our preset, we can use the `reactRouterConfigResolved` hook: + +```ts filename=my-cool-preset.ts lines=[22-27] +import type { + Preset, + ServerBundlesFunction, +} from "@react-router/dev/config"; + +const serverBundles: ServerBundlesFunction = ({ + branch, +}) => { + const isAuthenticatedRoute = branch.some((route) => + route.id.split("/").includes("_authenticated"), + ); + + return isAuthenticatedRoute + ? "authenticated" + : "unauthenticated"; +}; + +export function myCoolPreset(): Preset { + return { + name: "my-cool-preset", + reactRouterConfig: () => ({ serverBundles }), + reactRouterConfigResolved: ({ reactRouterConfig }) => { + if ( + reactRouterConfig.serverBundles !== serverBundles + ) { + throw new Error("`serverBundles` was overridden!"); + } + }, + }; +} +``` + +The `reactRouterConfigResolved` hook should only be used when it would be an error to merge or override your preset's config. + +## Using a preset + +Presets are designed to be published to npm and used within your React Router config. + +```ts filename=react-router.config.ts lines=[6] +import type { Config } from "@react-router/dev/config"; +import { myCoolPreset } from "react-router-preset-cool"; + +export default { + // ... + presets: [myCoolPreset()], +} satisfies Config; +``` + +[react-router-config]: https://api.reactrouter.com/v7/types/_react_router_dev.config.Config.html +[preset-type]: https://api.reactrouter.com/v7/types/_react_router_dev.config.Preset.html +[server-bundles]: ./server-bundles diff --git a/docs/how-to/react-server-components.md b/docs/how-to/react-server-components.md index 975dd99b98..ab256ce0fd 100644 --- a/docs/how-to/react-server-components.md +++ b/docs/how-to/react-server-components.md @@ -1,50 +1,786 @@ --- title: React Server Components -# need to ship it first! -hidden: true +unstable: true --- # React Server Components -<docs-info>This feature is still in development and not yet available.</docs-info> +[MODES: data] -In the future, async components can be rendered in loaders like any other data: +<br/> +<br/> -```tsx filename=app/product-page.tsx -// route("products/:pid", "./product-page.tsx"); -import type { Route } from "./+types/product"; -import Product from "./product"; -import Reviews from "./reviews"; +<docs-warning>React Server Components support is experimental and subject to breaking changes.</docs-warning> -export async function loader({ params }: Route.LoaderArgs) { - return { - product: <Product id={params.pid} />, - reviews: <Reviews productId={params.pid} />, - }; +React Server Components (RSC) refers generally to an architecture and set of APIs provided by React since version 19. + +From the docs: + +> Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server. +> +> <cite>- [React "Server Components" docs][react-server-components-doc]</cite> + +React Router provides a set of APIs for integrating with RSC-compatible bundlers, allowing you to leverage [Server Components][react-server-components-doc] and [Server Functions][react-server-functions-doc] in your React Router applications. + +## Quick Start + +The quickest way to get started is with one of our templates. + +These templates come with React Router RSC APIs already configured with the respective bundler, offering you out of the box features such as: + +- Server Component Routes +- Server Side Rendering (SSR) +- Client Components (via [`"use client"`][use-client-docs] directive) +- Server Functions (via [`"use server"`][use-server-docs] directive) + +**Parcel Template** + +The [parcel template][parcel-rsc-template] uses the official React `react-server-dom-parcel` plugin. + +```shellscript +npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-parcel +``` + +**Vite Template** + +The [vite template][vite-rsc-template] uses the experimental Vite `@vitejs/plugin-rsc` plugin. + +```shellscript +npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-vite +``` + +## Using RSC with React Router + +### Configuring Routes + +Routes are configured as an argument to [`matchRSCServerRequest`][match-rsc-server-request]. At a minimum, you need a path and component: + +```tsx +function Root() { + return <h1>Hello world</h1>; } -export default function ProductPage({ - loaderData, -}: Route.ComponentProps) { +matchRSCServerRequest({ + // ...other options + routes: [{ path: "/", Component: Root }], +}); +``` + +While you can define components inline, we recommend using the `lazy()` option and defining [Route Modules][route-module] for both startup performance and code organization + +<docs-info> + +The [Route Module API][route-module] up until now has been a [Framework Mode][framework-mode] only feature. However, the `lazy` field of the RSC route config expects the same exports as the Route Module exports, unifying the APIs even further. + +</docs-info> + +```tsx filename=app/routes.ts +import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router"; + +export function routes() { + return [ + { + id: "root", + path: "", + lazy: () => import("./root/route"), + children: [ + { + id: "home", + index: true, + lazy: () => import("./home/route"), + }, + { + id: "about", + path: "about", + lazy: () => import("./about/route"), + }, + ], + }, + ] satisfies RSCRouteConfig; +} +``` + +### Server Component Routes + +By default each route's `default` export renders a Server Component + +```tsx +export default function Home() { return ( - <div> - {loaderData.product} - <Suspense fallback={<div>loading...</div>}> - {loaderData.reviews} - </Suspense> - </div> + <main> + <article> + <h1>Welcome to React Router RSC</h1> + <p> + You won't find me running any JavaScript in the + browser! + </p> + </article> + </main> ); } ``` -```tsx filename=app/product.tsx -export async function Product({ id }: { id: string }) { - const product = await fakeDb.getProduct(id); +A nice feature of Server Components is that you can fetch data directly from your component by making it asynchronous. + +```tsx +export default async function Home() { + let user = await getUserData(); + return ( - <div> - <h1>{product.title}</h1> - <p>{product.description}</p> - </div> + <main> + <article> + <h1>Welcome to React Router RSC</h1> + <p> + You won't find me running any JavaScript in the + browser! + </p> + <p> + Hello, {user ? user.name : "anonymous person"}! + </p> + </article> + </main> ); } ``` + +<docs-info> + +Server Components can also be returned from your loaders and actions. In general, if you are using RSC to build your application, loaders are primarily useful for things like setting `status` codes or returning a `redirect`. + +Using Server Components in loaders can be helpful for incremental adoption of RSC. + +</docs-info> + +### Server Functions + +[Server Functions][react-server-functions-doc] are a React feature that allow you to call async functions executed on the server. They're defined with the [`"use server"`][use-server-docs] directive. + +```tsx +"use server"; + +export async function updateFavorite(formData: FormData) { + let movieId = formData.get("id"); + let intent = formData.get("intent"); + if (intent === "add") { + await addFavorite(Number(movieId)); + } else { + await removeFavorite(Number(movieId)); + } +} +``` + +```tsx +import { updateFavorite } from "./action.ts"; +export async function AddToFavoritesForm({ + movieId, +}: { + movieId: number; +}) { + let isFav = await isFavorite(movieId); + return ( + <form action={updateFavorite}> + <input type="hidden" name="id" value={movieId} /> + <input + type="hidden" + name="intent" + value={isFav ? "remove" : "add"} + /> + <AddToFavoritesButton isFav={isFav} /> + </form> + ); +} +``` + +Note that after server functions are called, React Router will automatically revalidate the route and update the UI with the new server content. You don't have to mess around with any cache invalidation. + +### Client Properties + +Routes are defined on the server at runtime, but we can still provide `clientLoader`, `clientAction`, and `shouldRevalidate` through the utilization of client references and `"use client"`. + +```tsx filename=src/routes/root/client.tsx +"use client"; + +export function clientAction() {} + +export function clientLoader() {} + +export function shouldRevalidate() {} +``` + +We can then re-export these from our lazy loaded route module: + +```tsx filename=src/routes/root/route.tsx +export { + clientAction, + clientLoader, + shouldRevalidate, +} from "./route.client"; + +export default function Root() { + // ... +} +``` + +This is also the way we would make an entire route a Client Component. + +```tsx filename=src/routes/root/route.tsx lines=[1,11] +import { default as ClientRoot } from "./route.client"; +export { + clientAction, + clientLoader, + shouldRevalidate, +} from "./route.client"; + +export default function Root() { + // Adding a Server Component at the root is required by bundlers + // if you're using css side-effects imports. + return <ClientRoot />; +} +``` + +## Configuring RSC with React Router + +React Router provides several APIs that allow you to easily integrate with RSC-compatible bundlers, useful if you are using React Router Data Mode to make your own [custom framework][custom-framework]. + +The following steps show how to setup a React Router application to use Server Components (RSC) to server-render (SSR) pages and hydrate them for single-page app (SPA) navigations. You don't have to use SSR (or even client-side hydration) if you don't want to. You can also leverage the HTML generation for Static Site Generation (SSG) or Incremental Static Regeneration (ISR) if you prefer. This guide is meant merely to explain how to wire up all the different APIs for a typically RSC-based application. + +### Entry points + +Besides our [route definitions](#configuring-routes), we will need to configure the following: + +1. A server to handle the incoming request, fetch the RSC payload, and convert it into HTML +2. A React server to generate RSC payloads +3. A browser handler to hydrate the generated HTML and set the `callServer` function to support post-hydration server actions + +The following naming conventions have been chosen for familiarity and simplicity. Feel free to name and configure your entry points as you see fit. + +See the relevant bundler documentation below for specific code examples for each of the following entry points. + +These examples all use [express][express] and [@remix-run/node-fetch-server][node-fetch-server] for the server and request handling. + +**Routes** + +See [Configuring Routes](#configuring-routes). + +**Server** + +<docs-info> + +You don't have to use SSR at all. You can choose to use RSC to "prerender" HTML for Static Site Generation (SSG) or something like Incremental Static Regeneration (ISR). + +</docs-info> + +`entry.ssr.tsx` is the entry point for the server. It is responsible for handling the request, calling the RSC server, and converting the RSC payload into HTML on document requests (server-side rendering). + +Relevant APIs: + +- [`routeRSCServerRequest`][route-rsc-server-request] +- [`RSCStaticRouter`][rsc-static-router] + +**RSC Server** + +<docs-info> + +Even though you have a "React Server" and a server responsible for request handling/SSR, you don't actually need to have 2 separate servers. You can simply have 2 separate module graphs within the same server. This is important because React behaves differently when generating RSC payloads vs. when generating HTML to be hydrated on the client. + +</docs-info> + +`entry.rsc.tsx` is the entry point for the React Server. It is responsible for matching the request to a route and generating RSC payloads. + +Relevant APIs: + +- [`matchRSCServerRequest`][match-rsc-server-request] + +**Browser** + +`entry.browser.tsx` is the entry point for the client. It is responsible for hydrating the generated HTML and setting the `callServer` function to support post-hydration server actions. + +Relevant APIs: + +- [`createCallServer`][create-call-server] +- [`getRSCStream`][get-rsc-stream] +- [`RSCHydratedRouter`][rsc-hydrated-router] + +### Parcel + +See the [Parcel RSC docs][parcel-rsc-doc] for more information. You can also refer to our [Parcel RSC Parcel template][parcel-rsc-template] to see a working version. + +In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies: + +```shellscript +# install runtime dependencies +npm i @parcel/runtime-rsc react-server-dom-parcel + +# install dev dependencies +npm i -D parcel +``` + +#### `package.json` + +To configure Parcel, add the following to your `package.json`: + +```json filename=package.json +{ + "scripts": { + "build": "parcel build --no-autoinstall", + "dev": "cross-env NODE_ENV=development parcel --no-autoinstall --no-cache", + "start": "cross-env NODE_ENV=production node dist/server/entry.rsc.js" + }, + "targets": { + "react-server": { + "context": "react-server", + "source": "src/entry.rsc.tsx", + "scopeHoist": false, + "includeNodeModules": { + "@remix-run/node-fetch-server": false, + "compression": false, + "express": false + } + } + } +} +``` + +#### `routes/config.ts` + +You must add `"use server-entry"` to the top of the file where you define your routes. Additionally, you need to import the client entry point, since it will use the `"use client-entry"` directive (see below). + +```tsx filename=src/routes/config.ts +"use server-entry"; + +import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router"; + +import "../entry.browser"; + +// This needs to be a function so Parcel can add a `bootstrapScript` property. +export function routes() { + return [ + { + id: "root", + path: "", + lazy: () => import("./root/route"), + children: [ + { + id: "home", + index: true, + lazy: () => import("./home/route"), + }, + { + id: "about", + path: "about", + lazy: () => import("./about/route"), + }, + ], + }, + ] satisfies RSCRouteConfig; +} +``` + +#### `entry.ssr.tsx` + +The following is a simplified example of a Parcel SSR Server. + +```tsx filename=src/entry.ssr.tsx +import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge"; +import { + unstable_routeRSCServerRequest as routeRSCServerRequest, + unstable_RSCStaticRouter as RSCStaticRouter, +} from "react-router"; +import { createFromReadableStream } from "react-server-dom-parcel/client.edge"; + +export async function generateHTML( + request: Request, + fetchServer: (request: Request) => Promise<Response>, + bootstrapScriptContent: string | undefined, +): Promise<Response> { + return await routeRSCServerRequest({ + // The incoming request. + request, + // How to call the React Server. + fetchServer, + // Provide the React Server touchpoints. + createFromReadableStream, + // Render the router to HTML. + async renderHTML(getPayload) { + const payload = await getPayload(); + const formState = + payload.type === "render" + ? await payload.formState + : undefined; + + return await renderHTMLToReadableStream( + <RSCStaticRouter getPayload={getPayload} />, + { + bootstrapScriptContent, + formState, + }, + ); + }, + }); +} +``` + +#### `entry.rsc.tsx` + +The following is a simplified example of a Parcel RSC Server. + +```tsx filename=src/entry.rsc.tsx +import { createRequestListener } from "@remix-run/node-fetch-server"; +import express from "express"; +import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router"; +import { + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction, + renderToReadableStream, +} from "react-server-dom-parcel/server.edge"; + +// Import the generateHTML function from the react-client environment +import { generateHTML } from "./entry.ssr" with { env: "react-client" }; +import { routes } from "./routes/config"; + +function fetchServer(request: Request) { + return matchRSCServerRequest({ + // Provide the React Server touchpoints. + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction, + // The incoming request. + request, + // The app routes. + routes: routes(), + // Encode the match with the React Server implementation. + generateResponse(match) { + return new Response( + renderToReadableStream(match.payload), + { + status: match.statusCode, + headers: match.headers, + }, + ); + }, + }); +} + +const app = express(); + +// Serve static assets with compression and long cache lifetime. +app.use( + "/client", + compression(), + express.static("dist/client", { + immutable: true, + maxAge: "1y", + }), +); +// Hook up our application. +app.use( + createRequestListener((request) => + generateHTML( + request, + fetchServer, + (routes as unknown as { bootstrapScript?: string }) + .bootstrapScript, + ), + ), +); + +app.listen(3000, () => { + console.log("Server listening on port 3000"); +}); +``` + +#### `entry.browser.tsx` + +```tsx filename=src/entry.browser.tsx +"use client-entry"; + +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { + unstable_createCallServer as createCallServer, + unstable_getRSCStream as getRSCStream, + unstable_RSCHydratedRouter as RSCHydratedRouter, + type unstable_RSCPayload as RSCServerPayload, +} from "react-router"; +import { + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, +} from "react-server-dom-parcel/client"; + +// Create and set the callServer function to support post-hydration server actions. +setServerCallback( + createCallServer({ + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + }), +); + +// Get and decode the initial server payload. +createFromReadableStream(getRSCStream()).then( + (payload: RSCServerPayload) => { + startTransition(async () => { + const formState = + payload.type === "render" + ? await payload.formState + : undefined; + + hydrateRoot( + document, + <StrictMode> + <RSCHydratedRouter + createFromReadableStream={ + createFromReadableStream + } + payload={payload} + /> + </StrictMode>, + { + formState, + }, + ); + }); + }, +); +``` + +### Vite + +See the [Vite RSC docs][vite-rsc-doc] for more information. You can also refer to our [Vite RSC template][vite-rsc-template] to see a working version. + +In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies: + +```shellscript +npm i -D vite @vitejs/plugin-react @vitejs/plugin-rsc +``` + +#### `vite.config.ts` + +To configure Vite, add the following to your `vite.config.ts`: + +```ts filename=vite.config.ts +import rsc from "@vitejs/plugin-rsc/plugin"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + react(), + rsc({ + entries: { + client: "src/entry.browser.tsx", + rsc: "src/entry.rsc.tsx", + ssr: "src/entry.ssr.tsx", + }, + }), + ], +}); +``` + +```tsx filename=src/routes/config.ts +import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router"; + +export function routes() { + return [ + { + id: "root", + path: "", + lazy: () => import("./root/route"), + children: [ + { + id: "home", + index: true, + lazy: () => import("./home/route"), + }, + { + id: "about", + path: "about", + lazy: () => import("./about/route"), + }, + ], + }, + ] satisfies RSCRouteConfig; +} +``` + +#### `entry.ssr.tsx` + +The following is a simplified example of a Vite SSR Server. + +```tsx filename=src/entry.ssr.tsx +import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; +import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge"; +import { + unstable_routeRSCServerRequest as routeRSCServerRequest, + unstable_RSCStaticRouter as RSCStaticRouter, +} from "react-router"; + +export async function generateHTML( + request: Request, + fetchServer: (request: Request) => Promise<Response>, +): Promise<Response> { + return await routeRSCServerRequest({ + // The incoming request. + request, + // How to call the React Server. + fetchServer, + // Provide the React Server touchpoints. + createFromReadableStream, + // Render the router to HTML. + async renderHTML(getPayload) { + const payload = await getPayload(); + const formState = + payload.type === "render" + ? await payload.formState + : undefined; + + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent( + "index", + ); + + return await renderHTMLToReadableStream( + <RSCStaticRouter getPayload={getPayload} />, + { + bootstrapScriptContent, + formState, + }, + ); + }, + }); +} +``` + +#### `entry.rsc.tsx` + +The following is a simplified example of a Vite RSC Server. + +```tsx filename=src/entry.rsc.tsx +import { + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction, + renderToReadableStream, +} from "@vitejs/plugin-rsc/rsc"; +import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router"; + +import { routes } from "./routes/config"; + +function fetchServer(request: Request) { + return matchRSCServerRequest({ + // Provide the React Server touchpoints. + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction, + // The incoming request. + request, + // The app routes. + routes: routes(), + // Encode the match with the React Server implementation. + generateResponse(match) { + return new Response( + renderToReadableStream(match.payload), + { + status: match.statusCode, + headers: match.headers, + }, + ); + }, + }); +} + +export default async function handler(request: Request) { + // Import the generateHTML function from the client environment + const ssr = await import.meta.viteRsc.loadModule< + typeof import("./entry.ssr") + >("ssr", "index"); + + return ssr.generateHTML(request, fetchServer); +} +``` + +#### `entry.browser.tsx` + +```tsx filename=src/entry.browser.tsx +import { + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, +} from "@vitejs/plugin-rsc/browser"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { + unstable_createCallServer as createCallServer, + unstable_getRSCStream as getRSCStream, + unstable_RSCHydratedRouter as RSCHydratedRouter, + type unstable_RSCPayload as RSCServerPayload, +} from "react-router"; + +// Create and set the callServer function to support post-hydration server actions. +setServerCallback( + createCallServer({ + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + }), +); + +// Get and decode the initial server payload. +createFromReadableStream<RSCServerPayload>( + getRSCStream(), +).then((payload) => { + startTransition(async () => { + const formState = + payload.type === "render" + ? await payload.formState + : undefined; + + hydrateRoot( + document, + <StrictMode> + <RSCHydratedRouter + createFromReadableStream={ + createFromReadableStream + } + payload={payload} + /> + </StrictMode>, + { + formState, + }, + ); + }); +}); +``` + +[react-server-components-doc]: https://react.dev/reference/rsc/server-components +[react-server-functions-doc]: https://react.dev/reference/rsc/server-functions +[use-client-docs]: https://react.dev/reference/rsc/use-client +[use-server-docs]: https://react.dev/reference/rsc/use-server +[route-module]: ../start/framework/route-module +[framework-mode]: ../start/modes#framework +[custom-framework]: ../start/data/custom +[parcel-rsc-doc]: https://parceljs.org/recipes/rsc/ +[vite-rsc-doc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc +[match-rsc-server-request]: ../api/rsc/matchRSCServerRequest +[route-rsc-server-request]: ../api/rsc/routeRSCServerRequest +[rsc-static-router]: ../api/rsc/RSCStaticRouter +[create-call-server]: ../api/rsc/createCallServer +[get-rsc-stream]: ../api/rsc/getRSCStream +[rsc-hydrated-router]: ../api/rsc/RSCHydratedRouter +[express]: https://expressjs.com/ +[node-fetch-server]: https://www.npmjs.com/package/@remix-run/node-fetch-server +[parcel-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-parcel +[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-vite diff --git a/docs/how-to/resource-routes.md b/docs/how-to/resource-routes.md index 78924fdd27..d3ccb1a52f 100644 --- a/docs/how-to/resource-routes.md +++ b/docs/how-to/resource-routes.md @@ -4,6 +4,11 @@ title: Resource Routes # Resource Routes +[MODES: framework, data] + +<br/> +<br/> + When server rendering, routes can serve "resources" instead of rendering components, like images, PDFs, JSON payloads, webhooks, etc. ## Defining a Resource Route diff --git a/docs/how-to/route-module-type-safety.md b/docs/how-to/route-module-type-safety.md index 9c6648a59b..06a3a4a3a3 100644 --- a/docs/how-to/route-module-type-safety.md +++ b/docs/how-to/route-module-type-safety.md @@ -4,6 +4,11 @@ title: Route Module Type Safety # Route Module Type Safety +[MODES: framework] + +<br/> +<br/> + React Router generates route-specific types to power type inference for URL params, loader data, and more. This guide will help you set it up if you didn't start with a template. diff --git a/docs/how-to/security.md b/docs/how-to/security.md index c4d3cd56ad..9ac29e4a2d 100644 --- a/docs/how-to/security.md +++ b/docs/how-to/security.md @@ -4,6 +4,11 @@ title: Security # Security +[MODES: framework] + +<br/> +<br/> + This is by no means a comprehensive guide, but React Router provides features to help address a few aspects under the _very large_ umbrella that is _Security_. ## `Content-Security-Policy` diff --git a/docs/how-to/server-bundles.md b/docs/how-to/server-bundles.md new file mode 100644 index 0000000000..2f03c214a0 --- /dev/null +++ b/docs/how-to/server-bundles.md @@ -0,0 +1,66 @@ +--- +title: Server Bundles +--- + +# Server Bundles + +[MODES: framework] + +<br/> +<br/> + +<docs-warning>This is an advanced feature designed for hosting provider integrations. When compiling your app into multiple server bundles, there will need to be a custom routing layer in front of your app directing requests to the correct bundle.</docs-warning> + +React Router typically builds your server code into a single bundle that exports a request handler function. However, there are scenarios where you might want to split your route tree into multiple server bundles, each exposing a request handler function for a subset of routes. To provide this flexibility, [`react-router.config.ts`][react-router-config] supports a `serverBundles` option, which is a function for assigning routes to different server bundles. + +The [`serverBundles` function][server-bundles-function] is called for each route in the tree (except for routes that aren't addressable, e.g., pathless layout routes) and returns a server bundle ID that you'd like to assign that route to. These bundle IDs will be used as directory names in your server build directory. + +For each route, this function receives an array of routes leading to and including that route, referred to as the route `branch`. This allows you to create server bundles for different portions of the route tree. For example, you could use this to create a separate server bundle containing all routes within a particular layout route: + +```ts filename=react-router.config.ts lines=[5-13] +import type { Config } from "@react-router/dev/config"; + +export default { + // ... + serverBundles: ({ branch }) => { + const isAuthenticatedRoute = branch.some((route) => + route.id.split("/").includes("_authenticated"), + ); + + return isAuthenticatedRoute + ? "authenticated" + : "unauthenticated"; + }, +} satisfies Config; +``` + +Each `route` in the `branch` array contains the following properties: + +- `id` โ€” The unique ID for this route, named like its `file` but relative to the app directory and without the extension, e.g., `app/routes/gists.$username.tsx` will have an `id` of `routes/gists.$username` +- `path` โ€” The path this route uses to match the URL pathname +- `file` โ€” The absolute path to the entry point for this route +- `index` โ€” Whether this route is an index route + +## Build manifest + +When the build is complete, React Router will call the `buildEnd` hook, passing a `buildManifest` object. This is useful if you need to inspect the build manifest to determine how to route requests to the correct server bundle. + +```ts filename=react-router.config.ts lines=[5-7] +import type { Config } from "@react-router/dev/config"; + +export default { + // ... + buildEnd: async ({ buildManifest }) => { + // ... + }, +} satisfies Config; +``` + +When using server bundles, the build manifest contains the following properties: + +- `serverBundles` โ€” An object that maps bundle IDs to the bundle's `id` and `file` +- `routeIdToServerBundleId` โ€” An object that maps route IDs to their server bundle ID +- `routes` โ€” A route manifest that maps route IDs to route metadata. This can be used to drive a custom routing layer in front of your React Router request handlers + +[react-router-config]: https://api.reactrouter.com/v7/types/_react_router_dev.config.Config.html +[server-bundles-function]: https://api.reactrouter.com/v7/types/_react_router_dev.config.ServerBundlesFunction.html diff --git a/docs/how-to/spa.md b/docs/how-to/spa.md index 5b496f4093..28684ac654 100644 --- a/docs/how-to/spa.md +++ b/docs/how-to/spa.md @@ -4,12 +4,12 @@ title: Single Page App (SPA) # Single Page App (SPA) -There are two ways to ship a single page app with React Router +[MODES: framework] -- **as a library** - Instead of using React Router's framework features, you can use it as a library in your own SPA architecture. Refer to [React Router as a Library](../start/library/installation) guides. -- **as a framework** - This guide will focus here +<br/> +<br/> -## Overview +<docs-info>This guide focuses on how to build Single Page Apps with React Router Framework mode. If you're using React Router in declarative or data mode, you can design your own SPA architecture.</docs-info> When using React Router as a framework, you can enable "SPA Mode" by setting `ssr:false` in your `react-router.config.ts` file. This will disable runtime server rendering and generate an `index.html` at build time that you can serve and hydrate as a SPA. @@ -20,8 +20,6 @@ Typical Single Page apps send a mostly blank `index.html` template with little m - Use React components to generate the initial page users see (root `HydrateFallback`) - Re-enable server rendering later without changing anything about your UI -It's important to note that setting `ssr:false` only disables _runtime server rendering_. React Router will still server render your root route at _build time_ to generate the `index.html` file. This is why your project still needs a dependency on `@react-router/node` and your routes need to be SSR-safe. That means you can't call `window` or other browser-only APIs during the initial render, even when server rendering is disabled. - <docs-info>SPA Mode is a special form of "Pre-Rendering" that allows you to serve all paths in your application from the same HTML file. Please refer to the [Pre-Rendering](./pre-rendering) guide if you want to do more extensive pre-rendering.</docs-info> ## 1. Disable Runtime Server Rendering diff --git a/docs/how-to/status.md b/docs/how-to/status.md index 23a21423a1..906f4007cc 100644 --- a/docs/how-to/status.md +++ b/docs/how-to/status.md @@ -4,6 +4,11 @@ title: Status Codes # Status Codes +[MODES: framework ,data] + +<br/> +<br/> + Set status codes from loaders and actions with `data`. ```tsx filename=app/project.tsx lines=[3,12-15,20,23] @@ -20,7 +25,7 @@ export async function action({ if (!title) { return data( { message: "Invalid title" }, - { status: 400 } + { status: 400 }, ); } diff --git a/docs/how-to/suspense.md b/docs/how-to/suspense.md index d3bcd1443f..25402164b1 100644 --- a/docs/how-to/suspense.md +++ b/docs/how-to/suspense.md @@ -4,6 +4,11 @@ title: Streaming with Suspense # Streaming with Suspense +[MODES: framework, data] + +<br/> +<br/> + Streaming with React Suspense allows apps to speed up initial renders by deferring non-critical data and unblocking UI rendering. React Router supports React Suspense by returning promises from loaders and actions. @@ -18,11 +23,11 @@ import type { Route } from "./+types/my-route"; export async function loader({}: Route.LoaderArgs) { // note this is NOT awaited let nonCriticalData = new Promise((res) => - setTimeout(() => res("non-critical"), 5000) + setTimeout(() => res("non-critical"), 5000), ); let criticalData = await new Promise((res) => - setTimeout(() => res("critical"), 300) + setTimeout(() => res("critical"), 300), ); return { nonCriticalData, criticalData }; diff --git a/docs/how-to/view-transitions.md b/docs/how-to/view-transitions.md index 8d87515fb2..d5ace0ab31 100644 --- a/docs/how-to/view-transitions.md +++ b/docs/how-to/view-transitions.md @@ -4,6 +4,11 @@ title: View Transitions # View Transitions +[MODES: framework, data] + +<br/> +<br/> + Enable smooth animations between page transitions in your React Router applications using the [View Transitions API][view-transitions-api]. This feature allows you to create seamless visual transitions during client-side navigation. ## Basic View Transition @@ -20,13 +25,37 @@ The simplest way to enable view transitions is by adding the `viewTransition` pr Without any additional CSS, this provides a basic cross-fade animation between pages. +### 2. Enable view transitions with programmatic navigation + +When using programmatic navigation with the `useNavigate` hook, you can enable view transitions by passing the `viewTransition: true` option: + +```tsx +import { useNavigate } from "react-router"; + +function NavigationButton() { + const navigate = useNavigate(); + + return ( + <button + onClick={() => + navigate("/about", { viewTransition: true }) + } + > + About + </button> + ); +} +``` + +This provides the same cross-fade animation as using the `viewTransition` prop on Link components. + For more information on using the View Transitions API, please refer to the ["Smooth transitions with the View Transition API" guide][view-transitions-guide] from the Google Chrome team. ## Image Gallery Example Let's build an image gallery that demonstrates how to trigger and use view transitions. We'll create a list of images that expand into a detail view with smooth animations. -### 2. Create the image gallery route +### 1. Create the image gallery route ```tsx filename=routes/image-gallery.tsx import { NavLink } from "react-router"; @@ -62,7 +91,7 @@ export default function ImageGalleryRoute() { } ``` -### 3. Add transition styles +### 2. Add transition styles Define view transition names and animations for elements that should transition smoothly between routes. @@ -98,7 +127,7 @@ Define view transition names and animations for elements that should transition } ``` -### 4. Create the image detail route +### 3. Create the image detail route The detail view needs to use the same view transition names to create a seamless animation. @@ -122,7 +151,7 @@ export default function ImageDetailsRoute({ } ``` -### 5. Add matching transition styles for the detail view +### 4. Add matching transition styles for the detail view ```css filename=app.css /* Match transition names from the list view */ diff --git a/docs/how-to/webhook.md b/docs/how-to/webhook.md index f29db056d3..ff964032d5 100644 --- a/docs/how-to/webhook.md +++ b/docs/how-to/webhook.md @@ -21,14 +21,14 @@ export const action = async ({ { message: "Method not allowed" }, { status: 405, - } + }, ); } const payload = await request.json(); /* Validate the webhook */ const signature = request.headers.get( - "X-Hub-Signature-256" + "X-Hub-Signature-256", ); const generatedSignature = `sha256=${crypto .createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET) @@ -39,7 +39,7 @@ export const action = async ({ { message: "Signature mismatch" }, { status: 401, - } + }, ); } diff --git a/docs/start/data/actions.md b/docs/start/data/actions.md index 47ebcf1154..2495ddb83a 100644 --- a/docs/start/data/actions.md +++ b/docs/start/data/actions.md @@ -65,7 +65,7 @@ function useQuizTimer() { let cb = useCallback(() => { submit( { quizTimedOut: true }, - { action: "/end-quiz", method: "post" } + { action: "/end-quiz", method: "post" }, ); }, []); @@ -103,7 +103,7 @@ They also have the imperative `submit` method. ```tsx fetcher.submit( { title: "New Title" }, - { action: "/update-task/123", method: "post" } + { action: "/update-task/123", method: "post" }, ); ``` diff --git a/docs/start/data/custom.md b/docs/start/data/custom.md index 01a17a5d59..2a938e1c6c 100644 --- a/docs/start/data/custom.md +++ b/docs/start/data/custom.md @@ -9,7 +9,7 @@ order: 8 ## Introduction -Instead of using `@react-router/dev`, you can integrate React Router's framework features (like loaders, actions, fetchers, etc.) into your own bundler and server abstractions with Data Mode.. +Instead of using `@react-router/dev`, you can integrate React Router's framework features (like loaders, actions, fetchers, etc.) into your own bundler and server abstractions with Data Mode. ## Client Rendering @@ -31,7 +31,7 @@ let router = createBrowserRouter([ path: "shows/:showId", Component: Show, loader: ({ request, params }) => - fetch(`/api/show/${params.id}.json`, { + fetch(`/api/show/${params.showId}.json`, { signal: request.signal, }), }, @@ -52,7 +52,7 @@ import { import { createRoot } from "react-dom/client"; createRoot(document.getElementById("root")).render( - <RouterProvider router={router} /> + <RouterProvider router={router} />, ); ``` @@ -150,7 +150,7 @@ export async function handler(request: Request) { <StaticRouterProvider router={router} context={context} - /> + />, ); // Setup headers from action and loaders from deepest match @@ -193,6 +193,6 @@ hydrateRoot( document, <StrictMode> <RouterProvider router={router} /> - </StrictMode> + </StrictMode>, ); ``` diff --git a/docs/start/data/data-loading.md b/docs/start/data/data-loading.md index 1ee3a34d9c..2f561248d2 100644 --- a/docs/start/data/data-loading.md +++ b/docs/start/data/data-loading.md @@ -15,7 +15,7 @@ Data is provided to route components from route loaders: createBrowserRouter([ { path: "/", - loader: () => { + loader: async () => { // return data from here return { records: await getSomeRecords() }; }, diff --git a/docs/start/data/installation.md b/docs/start/data/installation.md index 78fbe3384d..81acc6d8f6 100644 --- a/docs/start/data/installation.md +++ b/docs/start/data/installation.md @@ -27,14 +27,11 @@ npm i react-router Create a router and pass it to `RouterProvider`: -```tsx lines=[1-4,9-14,19] -import { - createBrowserRouter, - RouterProvider, -} from "react-router"; - +```tsx lines=[3-4,6-11,16] import React from "react"; import ReactDOM from "react-dom/client"; +import { createBrowserRouter } from "react-router"; +import { RouterProvider } from "react-router/dom"; const router = createBrowserRouter([ { @@ -46,7 +43,7 @@ const router = createBrowserRouter([ const root = document.getElementById("root"); ReactDOM.createRoot(root).render( - <RouterProvider router={router} /> + <RouterProvider router={router} />, ); ``` diff --git a/docs/start/data/route-object.md b/docs/start/data/route-object.md index 0ecb56ff8f..f933388ad7 100644 --- a/docs/start/data/route-object.md +++ b/docs/start/data/route-object.md @@ -54,6 +54,53 @@ function MyRouteComponent() { } ``` +## `unstable_middleware` + +Route [middleware][middleware] runs sequentially before and after navigations. This gives you a singular place to do things like logging and authentication. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation. + +```tsx +createBrowserRouter([ + { + path: "/", + unstable_middleware: [loggingMiddleware], + loader: rootLoader, + Component: Root, + children: [{ + path: 'auth', + unstable_middleware: [authMiddleware], + loader: authLoader, + Component: Auth, + children: [...] + }] + }, +]); + +async function loggingMiddleware({ request }, next) { + let url = new URL(request.url); + console.log(`Starting navigation: ${url.pathname}${url.search}`); + const start = performance.now(); + await next(); + const duration = performance.now() - start; + console.log(`Navigation completed in ${duration}ms`); +} + +const userContext = unstable_createContext<User>(); + +async function authMiddleware ({ context }) { + const userId = getUserId(); + + if (!userId) { + throw redirect("/login"); + } + + context.set(userContext, await getUserById(userId)); +}; +``` + +See also: + +- [Middleware][middleware] + ## `loader` Route loaders provide data to route components before they are rendered. @@ -147,7 +194,7 @@ A route loader is revalidated when: - its own route params change - any change to URL search params -- after any actions are called +- after an action is called and returns a non-error status code By defining this function, you opt out of the default behavior completely and can manually control when loader data is revalidated for navigations and form submissions. @@ -155,7 +202,7 @@ By defining this function, you opt out of the default behavior completely and ca import type { ShouldRevalidateFunctionArgs } from "react-router"; function shouldRevalidate( - arg: ShouldRevalidateFunctionArgs + arg: ShouldRevalidateFunctionArgs, ) { return true; // false } @@ -198,3 +245,4 @@ createBrowserRouter([ Next: [Data Loading](./data-loading) [loader-params]: https://api.reactrouter.com/v7/interfaces/react_router.LoaderFunctionArgs +[middleware]: ../../how-to/middleware diff --git a/docs/start/data/testing.md b/docs/start/data/testing.md index fbfb7998f5..b8faacb4cc 100644 --- a/docs/start/data/testing.md +++ b/docs/start/data/testing.md @@ -5,4 +5,4 @@ order: 9 # Testing -You can use `createRoutesStub` in data and framework modes. Please refer to the [Testing Guide](../framework/testing.md). +You can use `createRoutesStub` in data and framework modes. Please refer to the [Testing Guide](../framework/testing). diff --git a/docs/start/declarative/installation.md b/docs/start/declarative/installation.md index bf4dc80825..0860bfe28c 100644 --- a/docs/start/declarative/installation.md +++ b/docs/start/declarative/installation.md @@ -34,7 +34,7 @@ const root = document.getElementById("root"); ReactDOM.createRoot(root).render( <BrowserRouter> <App /> - </BrowserRouter> + </BrowserRouter>, ); ``` diff --git a/docs/start/declarative/routing.md b/docs/start/declarative/routing.md index d2aa8f0191..026e4696d0 100644 --- a/docs/start/declarative/routing.md +++ b/docs/start/declarative/routing.md @@ -24,7 +24,7 @@ ReactDOM.createRoot(root).render( <Routes> <Route path="/" element={<App />} /> </Routes> - </BrowserRouter> + </BrowserRouter>, ); ``` diff --git a/docs/start/framework/actions.md b/docs/start/framework/actions.md index 02db541d28..7e65c563f0 100644 --- a/docs/start/framework/actions.md +++ b/docs/start/framework/actions.md @@ -123,7 +123,7 @@ function useQuizTimer() { let cb = useCallback(() => { submit( { quizTimedOut: true }, - { action: "/end-quiz", method: "post" } + { action: "/end-quiz", method: "post" }, ); }, []); @@ -161,7 +161,7 @@ They also have the imperative `submit` method. ```tsx fetcher.submit( { title: "New Title" }, - { action: "/update-task/123", method: "post" } + { action: "/update-task/123", method: "post" }, ); ``` diff --git a/docs/start/framework/data-loading.md b/docs/start/framework/data-loading.md index b2fa38471b..b35995ef1b 100644 --- a/docs/start/framework/data-loading.md +++ b/docs/start/framework/data-loading.md @@ -15,6 +15,8 @@ Loader data is automatically serialized from loaders and deserialized in compone The type for the `loaderData` prop is [automatically generated][type-safety]. +<docs-info>We try to support the same set of [serializable types][serializable-types] that React permits server components to pass as props to client components. This future proofs your application for any eventual migration to [RSC][rsc].</docs-info> + ## Client Data Loading `clientLoader` is used to fetch data on the client. This is useful for pages or full projects that you'd prefer to fetch data from the browser only. @@ -113,7 +115,7 @@ export default { async prerender() { let products = await readProductsFromCSVFile(); return products.map( - (product) => `/products/${product.id}` + (product) => `/products/${product.id}`, ); }, } satisfies Config; @@ -182,14 +184,18 @@ export default function Product() { --- -Next: [Actions](./actions) +Next: [Actions][actions] See also: -- [Streaming with Suspense](../../how-to/suspense) -- [Client Data](../../how-to/client-data) -- [Using Fetchers](../../how-to/fetchers#loading-data) +- [Streaming with Suspense][streaming] +- [Client Data][client-data] +- [Using Fetchers][fetchers] -[advanced_data_fetching]: ../tutorials/advanced-data-fetching -[data]: ../../api/react-router/data [type-safety]: ../../explanation/type-safety +[serializable-types]: https://react.dev/reference/rsc/use-client#serializable-types +[rsc]: ../../how-to/react-server-components +[actions]: ./actions +[streaming]: ../../how-to/suspense +[client-data]: ../../how-to/client-data +[fetchers]: ../../how-to/fetchers#loading-data diff --git a/docs/start/framework/pending-ui.md b/docs/start/framework/pending-ui.md index cb4c01842e..d68568e0bd 100644 --- a/docs/start/framework/pending-ui.md +++ b/docs/start/framework/pending-ui.md @@ -63,7 +63,7 @@ function Navbar() { ## Pending Form Submission -When a form is submitted, the UI should immediately respond to the user's actions with a pending state. This is easiest to do with a [fetcher][use_fetcher] form because it has it's own independent state (whereas normal forms cause a global navigation). +When a form is submitted, the UI should immediately respond to the user's actions with a pending state. This is easiest to do with a [fetcher][use_fetcher] form because it has its own independent state (whereas normal forms cause a global navigation). ```tsx filename=app/project.tsx lines=[10-12] import { useFetcher } from "react-router"; diff --git a/docs/start/framework/route-module.md b/docs/start/framework/route-module.md index 9f22d36b19..f613d83a32 100644 --- a/docs/start/framework/route-module.md +++ b/docs/start/framework/route-module.md @@ -53,7 +53,7 @@ When the component is rendered, it is provided the props defined in `Route.Compo 3. `params`: An object containing the route parameters (if any). 4. `matches`: An array of all the matches in the current route tree. -You can use these props in place of hooks like `useLoaderData` or `useParams`. This may be preferrable because they will be automatically typed correctly for the route. +You can use these props in place of hooks like `useLoaderData` or `useParams`. This may be preferable because they will be automatically typed correctly for the route. ### Using props @@ -78,6 +78,95 @@ export default function MyRouteComponent({ } ``` +## `unstable_middleware` + +Route [middleware][middleware] runs sequentially on the server before and after document and +data requests. This gives you a singular place to do things like logging, +authentication, and post-processing of responses. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation. + +Here's an example middleware to log requests on the server: + +```tsx filename=root.tsx +async function loggingMiddleware( + { request, context }, + next, +) { + console.log( + `${new Date().toISOString()} ${request.method} ${request.url}`, + ); + const start = performance.now(); + const response = await next(); + const duration = performance.now() - start; + console.log( + `${new Date().toISOString()} Response ${response.status} (${duration}ms)`, + ); + return response; +} + +export const unstable_middleware = [loggingMiddleware]; +``` + +Here's an example middleware to check for logged in users and set the user in +`context` you can then access from loaders: + +```tsx filename=routes/_auth.tsx +async function authMiddleware ({ + request, + context, +}) => { + const session = await getSession(request); + const userId = session.get("userId"); + + if (!userId) { + throw redirect("/login"); + } + + const user = await getUserById(userId); + context.set(userContext, user); +}; + +export const unstable_middleware = [authMiddleware]; +``` + +<docs-warning>Please make sure you understand [when middleware runs][when-middleware-runs] to make sure your application will behave the way you intend when adding middleware to your routes.</docs-warning> + +See also: + +- [`unstable_middleware` params][middleware-params] +- [Middleware][middleware] + +## `unstable_clientMiddleware` + +This is the client-side equivalent of `unstable_middleware` and runs in the browser during client navigations. The only difference from server middleware is that client middleware doesn't return Responses because they're not wrapping an HTTP request on the server. + +Here's an example middleware to log requests on the client: + +```tsx filename=root.tsx +async function loggingMiddleware( + { request, context }, + next, +) { + console.log( + `${new Date().toISOString()} ${request.method} ${request.url}`, + ); + const start = performance.now(); + await next(); // ๐Ÿ‘ˆ No Response returned + const duration = performance.now() - start; + console.log( + `${new Date().toISOString()} Response ${response.status} (${duration}ms)`, + ); + // โœ… No need to return anything +} + +export const unstable_clientMiddleware = [ + loggingMiddleware, +]; +``` + +See also: + +- [Middleware][middleware] + ## `loader` Route loaders provide data to route components before they are rendered. They are only called on the server when server rendering or during the build with pre-rendering. @@ -168,6 +257,10 @@ export async function action({ request }) { } ``` +See also: + +- [`action` params][action-params] + ## `clientAction` Like route actions but only called in the browser. @@ -222,6 +315,11 @@ export function ErrorBoundary() { } ``` +See also: + +- [`useRouteError`][use-route-error] +- [`isRouteErrorResponse`][is-route-error-response] + ## `HydrateFallback` On initial page load, the route component renders only after the client loader is finished. If exported, a `HydrateFallback` can render immediately in place of the route component. @@ -243,7 +341,7 @@ export default function Component({ loaderData }) { ## `headers` -Route headers define HTTP headers to be sent with the response when server rendering. +The route `headers` function defines the HTTP headers to be sent with the response when server rendering. ```tsx export function headers() { @@ -254,6 +352,10 @@ export function headers() { } ``` +See also: + +- [`Headers`][headers] + ## `handle` Route handle allows apps to add anything to a route match in `useMatches` to create abstractions (like breadcrumbs, etc.). @@ -264,6 +366,10 @@ export const handle = { }; ``` +See also: + +- [`useMatches`][use-matches] + ## `links` Route links define [`<link>` element][link-element]s to be rendered in the document `<head>`. @@ -309,9 +415,33 @@ export default function Root() { ## `meta` -Route meta defines meta tags to be rendered in the `<head>` of the document. +Route meta defines [meta tags][meta-element] to be rendered in the `<Meta />` component, usually placed in the `<head>`. + +<docs-warning> + +Since React 19, [using the built-in `<meta>` element](https://react.dev/reference/react-dom/components/meta) is recommended over the use of the route module's `meta` export. + +Here is an example of how to use it and the `<title>` element: ```tsx +export default function MyRoute() { + return ( + <div> + <title>Very cool app + + + {/* The rest of your route content... */} + + ); +} +``` + + + +```tsx filename=app/product.tsx export function meta() { return [ { title: "Very cool app" }, @@ -327,9 +457,7 @@ export function meta() { } ``` -All routes' meta will be aggregated and rendered through the `` component, usually rendered in your app root: - -```tsx +```tsx filename=app/root.tsx import { Meta } from "react-router"; export default function Root() { @@ -345,9 +473,12 @@ export default function Root() { } ``` +The meta of the last matching route is used, allowing you to override parent routes' meta. It's important to note that the entire meta descriptor array is replaced, not merged. This gives you the flexibility to build your own meta composition logic across pages at different levels. + **See also** - [`meta` params][meta-params] +- [`meta` function return types][meta-function] ## `shouldRevalidate` @@ -359,7 +490,7 @@ Defining this function allows you to opt out of revalidation for a route loader import type { ShouldRevalidateFunctionArgs } from "react-router"; export function shouldRevalidate( - arg: ShouldRevalidateFunctionArgs + arg: ShouldRevalidateFunctionArgs, ) { return true; } @@ -371,18 +502,18 @@ export function shouldRevalidate( Next: [Rendering Strategies](./rendering) -[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API +[middleware-params]: https://api.reactrouter.com/v7/types/react_router.unstable_MiddlewareFunction.html +[middleware]: ../../how-to/middleware +[when-middleware-runs]: ../../how-to/middleware#when-middleware-runs [loader-params]: https://api.reactrouter.com/v7/interfaces/react_router.LoaderFunctionArgs [client-loader-params]: https://api.reactrouter.com/v7/types/react_router.ClientLoaderFunctionArgs [action-params]: https://api.reactrouter.com/v7/interfaces/react_router.ActionFunctionArgs [client-action-params]: https://api.reactrouter.com/v7/types/react_router.ClientActionFunctionArgs -[error-boundaries]: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary -[use-route-error]: https://api.reactrouter.com/v7/functions/react_router.useRouteError -[is-route-error-response]: https://api.reactrouter.com/v7/functions/react_router.isRouteErrorResponse -[cache-control-header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control -[headers]: https://developer.mozilla.org/en-US/docs/Web/API/Response -[use-matches]: https://api.reactrouter.com/v7/functions/react_router.useMatches +[use-route-error]: ../../api/hooks/useRouteError +[is-route-error-response]: ../../api/utils/isRouteErrorResponse +[headers]: https://developer.mozilla.org/en-US/docs/Web/API/Response/headers +[use-matches]: ../../api/hooks/useMatches [link-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link [meta-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta [meta-params]: https://api.reactrouter.com/v7/interfaces/react_router.MetaArgs -[use-revalidator]: https://api.reactrouter.com/v7/functions/react_router.useRevalidator.html +[meta-function]: https://api.reactrouter.com/v7/types/react_router.MetaDescriptor.html diff --git a/docs/start/framework/routing.md b/docs/start/framework/routing.md index e55ea7c5cb..796c571e37 100644 --- a/docs/start/framework/routing.md +++ b/docs/start/framework/routing.md @@ -301,6 +301,18 @@ You can destructure the `*`, you just have to assign it a new name. A common nam const { "*": splat } = params; ``` +You can also use a splat to catch requests that don't match any route: + +```ts filename=app/routes.ts +route("*", "./catchall.tsx"); // catchall route, +``` + +```tsx filename=app/catchall.tsx +export function loader() { + throw new Response("Page not found", { status: 404 }); +} +``` + ## Component Routes You can also use components that match the URL to elements anywhere in the component tree: diff --git a/docs/start/framework/testing.md b/docs/start/framework/testing.md index 4aa1767013..dd2059f020 100644 --- a/docs/start/framework/testing.md +++ b/docs/start/framework/testing.md @@ -17,7 +17,8 @@ Consider a login form component that relies on `useActionData` import { useActionData } from "react-router"; export function LoginForm() { - const { errors } = useActionData(); + const actionData = useActionData(); + const errors = actionData?.errors; return (