Repository: remix-run/remix
Pull request #4918 (2022-12-19 22:27:47)

Add future.v2_errorBoundary flag
Adds `future.v2_errorBoundary` flag to opt into Remix v2 `ErrorBoundary` behavior


Patch:
diff --git a/.changeset/dry-humans-press.md b/.changeset/dry-humans-press.md
new file mode 100644
index 000000000..14109ca49
--- /dev/null
+++ b/.changeset/dry-humans-press.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/dev": patch
+---
+
+Bump peer dependency on `@remix-run/serve` to match current release
diff --git a/.changeset/odd-numbers-film.md b/.changeset/odd-numbers-film.md
new file mode 100644
index 000000000..1e129194f
--- /dev/null
+++ b/.changeset/odd-numbers-film.md
@@ -0,0 +1,32 @@
+---
+"@remix-run/react": patch
+"@remix-run/server-runtime": patch
+---
+
+Add `future.v2_errorBoundary` flag to opt-into v2 `ErrorBoundary` behavior.  This removes the separate `CatchBoundary` and `ErrorBoundary` and consolidates them into a single `ErrorBoundary` following the logic used by `errorElement` in React Router. You can then use `isRouteErrorResponse` to differentiate between thrown `Response`/`Error` instances.
+
+```jsx
+// Current (Remix v1 default)
+import { useCatch } from "@remix-run/react";
+
+export function CatchBoundary() {
+  let caught = useCatch();
+  return <p>{caught.status} {caught.data}</p>;
+}
+
+export function ErrorBoundary({ error }) {
+  return <p>{error.message}</p>;
+}
+
+
+// Using future.v2_errorBoundary
+import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
+
+export function ErrorBoundary() {
+  let error = useRouteError();
+
+  return isRouteErrorResponse(error) ?
+    <p>{error.status} {error.data}</p> :
+    <p>{error.message}</p>;
+}
+```
\ No newline at end of file
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml
index 58f3ab056..29c975f99 100644
--- a/.github/workflows/no-response.yml
+++ b/.github/workflows/no-response.yml
@@ -15,7 +15,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: 🥺 Handle Ghosting
-        uses: actions/stale@v6
+        uses: actions/stale@v7
         with:
           days-before-close: 10
           close-issue-message: >
diff --git a/.github/workflows/postrelease.yml b/.github/workflows/postrelease.yml
deleted file mode 100644
index dcd1168a1..000000000
--- a/.github/workflows/postrelease.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: 🕊 Post-release
-
-on:
-  push:
-    tags:
-      # only run on `remix` tags
-      - remix@**
-
-jobs:
-  comment:
-    name: 📝 Comment on related issues and pull requests
-    if: github.repository == 'remix-run/remix'
-    uses: remix-run/remix/.github/workflows/release-comments.yml@main
-    with:
-      ref: ${{ github.ref }}
-      # this should match the above tag to watch excluding the trailing "@"
-      packageVersionToFollow: "remix"
-
-  deployments:
-    name: 🚀 Deployment Tests
-    if: github.repository == 'remix-run/remix'
-    uses: remix-run/remix/.github/workflows/deployments.yml@main
-    secrets:
-      TEST_AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }}
-      TEST_AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }}
-      TEST_CF_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }}
-      TEST_CF_GLOBAL_API_KEY: ${{ secrets.TEST_CF_GLOBAL_API_KEY }}
-      TEST_CF_EMAIL: ${{ secrets.TEST_CF_EMAIL }}
-      TEST_CF_PAGES_API_TOKEN: ${{ secrets.TEST_CF_PAGES_API_TOKEN }}
-      TEST_CF_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }}
-      TEST_DENO_DEPLOY_TOKEN: ${{ secrets.TEST_DENO_DEPLOY_TOKEN }}
-      TEST_FLY_TOKEN: ${{ secrets.TEST_FLY_TOKEN }}
-      TEST_NETLIFY_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }}
-      TEST_VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }}
-      TEST_VERCEL_USER_ID: ${{ secrets.TEST_VERCEL_USER_ID }}
-
-  stacks:
-    name: 🥞 Remix Stacks Test
-    if: github.repository == 'remix-run/remix'
-    uses: remix-run/remix/.github/workflows/stacks.yml@main
-    with:
-      version: ${{ github.ref_name }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 46425923b..bb479cc9a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,6 +20,9 @@ jobs:
       github.repository == 'remix-run/remix' &&
       !contains(github.ref, 'nightly')
     runs-on: ubuntu-latest
+    outputs:
+      publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
+      published: ${{ steps.changesets.outputs.published }}
     steps:
       - name: 🛑 Cancel Previous Runs
         uses: styfle/cancel-workflow-action@0.11.0
@@ -61,3 +64,67 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.NIGHTLY_PAT }}
           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+  findPackage:
+    name: 🦋 Find Package
+    needs: [release]
+    runs-on: ubuntu-latest
+    if: github.repository == 'remix-run/remix' && needs.release.outputs.published == 'true'
+    outputs:
+      package: ${{ steps.findPackage.outputs.package }}
+    steps:
+      - name: 🛑 Cancel Previous Runs
+        uses: styfle/cancel-workflow-action@0.11.0
+
+      - name: ⬇️ Checkout repo
+        uses: actions/checkout@v3
+
+      - name: ⎔ Setup node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 16
+          cache: "npm"
+
+      - id: findPackage
+        run: |
+          package=$(node ./scripts/release/find-release-from-changeset.js)
+          echo "package=${package}" >> $GITHUB_OUTPUT
+        env:
+          packageVersionToFollow: "remix"
+          publishedPackages: ${{ needs.release.outputs.publishedPackages }}
+
+  comment:
+    name: 📝 Comment on related issues and pull requests
+    if: github.repository == 'remix-run/remix' && needs.findPackage.outputs.package != ''
+    needs: [release, findPackage]
+    uses: ./.github/workflows/release-comments.yml
+    with:
+      ref: refs/tags/${{ needs.findPackage.outputs.package }}
+      packageVersionToFollow: "remix"
+
+  deployments:
+    name: 🚀 Deployment Tests
+    if: github.repository == 'remix-run/remix'
+    needs: [release, findPackage]
+    uses: ./.github/workflows/deployments.yml
+    secrets:
+      TEST_AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }}
+      TEST_AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }}
+      TEST_CF_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }}
+      TEST_CF_GLOBAL_API_KEY: ${{ secrets.TEST_CF_GLOBAL_API_KEY }}
+      TEST_CF_EMAIL: ${{ secrets.TEST_CF_EMAIL }}
+      TEST_CF_PAGES_API_TOKEN: ${{ secrets.TEST_CF_PAGES_API_TOKEN }}
+      TEST_CF_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }}
+      TEST_DENO_DEPLOY_TOKEN: ${{ secrets.TEST_DENO_DEPLOY_TOKEN }}
+      TEST_FLY_TOKEN: ${{ secrets.TEST_FLY_TOKEN }}
+      TEST_NETLIFY_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }}
+      TEST_VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }}
+      TEST_VERCEL_USER_ID: ${{ secrets.TEST_VERCEL_USER_ID }}
+
+  stacks:
+    name: 🥞 Remix Stacks Test
+    if: github.repository == 'remix-run/remix'
+    needs: [release, findPackage]
+    uses: ./.github/workflows/stacks.yml
+    with:
+      version: ${{ needs.findPackage.outputs.package }}
diff --git a/README.md b/README.md
index fee71ad0f..2ecd384fa 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 We are happy you're here!
 
-[Remix](https://remix.run/) is a full stack web framework that lets you focus on the user interface and work back through web fundamentals to deliver a fast, slick, and resilient user experience that deploys to any Node.js server and even non-Node.js environments at the edge like Cloudflare Workers.
+[Remix](https://remix.run) is a full stack web framework that lets you focus on the user interface and work back through web fundamentals to deliver a fast, slick, and resilient user experience that deploys to any Node.js server and even non-Node.js environments at the edge like Cloudflare Workers.
 
 Want to know more? Read the [Technical Explanation of Remix](https://remix.run/pages/technical-explanation)
 
diff --git a/babel.config.js b/babel.config.js
index 2da992422..05f42ce1a 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -4,7 +4,7 @@ module.exports = {
       "@babel/preset-env",
       {
         targets: {
-          node: "current",
+          node: "14",
         },
       },
     ],
diff --git a/contributors.yml b/contributors.yml
index 66fd669d0..58269fbfa 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -12,6 +12,7 @@
 - airjp73
 - airondumael
 - aissa-bouguern
+- akamfoad
 - Alarid
 - alex-ketch
 - alextes
@@ -46,6 +47,7 @@
 - AustinGil
 - awthwathje
 - axel-habermaier
+- barclayd
 - BasixKOR
 - BenMcH
 - benwis
@@ -86,6 +88,8 @@
 - craigglennie
 - crismali
 - cysp
+- dabdine
+- daganomri
 - damiensedgwick
 - dan-gamble
 - danielfgray
@@ -94,7 +98,7 @@
 - davecranwell-vocovo
 - DavidHollins6
 - davongit
-- deanmv
+- Deanmv
 - denissb
 - derekr
 - developit
@@ -163,9 +167,11 @@
 - hicksy
 - himorishige
 - hkan
+- hokaccha
 - Holben888
 - hollandThomas
 - Hopsken
+- humphd
 - hzhu
 - IAmLuisJ
 - ianduvall
@@ -365,6 +371,7 @@
 - realjokele
 - redabacha
 - reggie3
+- reichhartd
 - remix-run-bot
 - riencoertjens
 - rkulinski
@@ -464,5 +471,4 @@
 - zachdtaylor
 - zainfathoni
 - zhe
-- dabdine
-- akamfoad
+- mitchelldirt
diff --git a/docs/components/outlet.md b/docs/components/outlet.md
index 359760c79..0105b0819 100644
--- a/docs/components/outlet.md
+++ b/docs/components/outlet.md
@@ -6,4 +6,4 @@ title: Outlet
 
 Re-export of [React Router Outlet][rr-outlet].
 
-[rr-outlet]: https://reactrouter.com/en/main/components/outlet
+[rr-outlet]: https://reactrouter.com/components/outlet
diff --git a/docs/file-conventions/routes-files.md b/docs/file-conventions/routes-files.md
index d9d0d2ba6..61db54252 100644
--- a/docs/file-conventions/routes-files.md
+++ b/docs/file-conventions/routes-files.md
@@ -11,7 +11,7 @@ Please note that you can use either `.js`, `.jsx` or `.tsx` file extensions depe
 ## Root Route
 
 <!-- prettier-ignore -->
-```markdown [3]
+```markdown lines=[3]
 app/
 ├── routes/
 └── root.tsx
@@ -28,7 +28,7 @@ The file in `app/root.tsx` is your root layout, or "root route" (very sorry for
 Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `index.tsx` which maps to the root pathname.
 
 <!-- prettier-ignore -->
-```markdown [3-4]
+```markdown lines=[3-4]
 app/
 ├── routes/
 │   ├── about.tsx
@@ -46,14 +46,14 @@ The default export in this file is the component that is rendered at that route
 ## Dynamic Route Parameters
 
 <!-- prettier-ignore -->
-```markdown [4]
+```markdown lines=[4]
 app/
 ├── routes/
 │   ├── blog/
 │   │   ├── $postId.tsx
 │   │   ├── categories.tsx
-│   │   ├── index.tsx
-│   └── about.tsx
+│   │   └── index.tsx
+│   ├── about.tsx
 │   └── index.tsx
 └── root.tsx
 ```
@@ -112,15 +112,15 @@ See the [routing guide][routing-guide] for more information.
 ## Layout Routes
 
 <!-- prettier-ignore -->
-```markdown [3,8]
+```markdown lines=[3,8]
 app/
 ├── routes/
 │   ├── blog/
 │   │   ├── $postId.tsx
 │   │   ├── categories.tsx
-│   │   ├── index.tsx
-│   └── about.tsx
-│   └── blog.tsx
+│   │   └── index.tsx
+│   ├── about.tsx
+│   ├── blog.tsx
 │   └── index.tsx
 └── root.tsx
 ```
@@ -144,18 +144,18 @@ In the example above, the `blog.tsx` is a "layout route" for everything within t
 ## Pathless Layout Routes
 
 <!-- prettier-ignore -->
-```markdown [3,7,10-11]
+```markdown lines=[3,7,10-11]
 app/
 ├── routes/
 │   ├── __app/
 │   │   ├── dashboard.tsx
 │   │   └── $userId/
-│   │   │   └── profile.tsx
+│   │       └── profile.tsx
 │   └── __marketing
 │   │   ├── index.tsx
 │   │   └── product.tsx
 │   ├── __app.tsx
-│   ├── __marketing.tsx
+│   └── __marketing.tsx
 └── root.tsx
 ```
 
@@ -181,16 +181,16 @@ For example, all of your marketing pages could be in `app/routes/__marketing/*`
 ## Dot Delimiters
 
 <!-- prettier-ignore -->
-```markdown [8]
+```markdown lines=[8]
 app/
 ├── routes/
 │   ├── blog/
 │   │   ├── $postId.tsx
 │   │   ├── categories.tsx
-│   │   ├── index.tsx
-│   └── about.tsx
-│   └── blog.authors.tsx
-│   └── blog.tsx
+│   │   └── index.tsx
+│   ├── about.tsx
+│   ├── blog.authors.tsx
+│   ├── blog.tsx
 │   └── index.tsx
 └── root.tsx
 ```
@@ -212,17 +212,17 @@ By creating a file with `.` characters between segments, you can create a nested
 ## Splat Routes
 
 <!-- prettier-ignore -->
-```markdown [7]
+```markdown lines=[7]
 app/
 ├── routes/
 │   ├── blog/
 │   │   ├── $postId.tsx
 │   │   ├── categories.tsx
-│   │   ├── index.tsx
-│   └── $.tsx
-│   └── about.tsx
-│   └── blog.authors.tsx
-│   └── blog.tsx
+│   │   └── index.tsx
+│   ├── $.tsx
+│   ├── about.tsx
+│   ├── blog.authors.tsx
+│   ├── blog.tsx
 │   └── index.tsx
 └── root.tsx
 ```
@@ -285,7 +285,7 @@ Because some characters have special meaning, you must use our escaping syntax i
 [catch-boundary]: ../route/catch-boundary
 [outlet]: ../components/outlet
 [view-example-app]: https://github.com/remix-run/examples/tree/main/multiple-params
-[use-params]: https://reactrouter.com/docs/hooks/use-params
+[use-params]: https://reactrouter.com/hooks/use-params
 [params]: ../route/loader#params
 [routing-guide]: ../guides/routing
 [root-route]: #root-route
diff --git a/docs/guides/migrating-react-router-app.md b/docs/guides/migrating-react-router-app.md
index c22403259..3acb6cba8 100644
--- a/docs/guides/migrating-react-router-app.md
+++ b/docs/guides/migrating-react-router-app.md
@@ -613,8 +613,8 @@ Now then, go off and _remix your app_. We think you'll like what you build along
 - [Common "gotchas"][common-gotchas]
 
 [react-router]: https://reactrouter.com
-[react-router-docs]: https://reactrouter.com/docs/start/concepts
-[migration-guide-from-v5-to-v6]: https://reactrouter.com/docs/upgrading/v5
+[react-router-docs]: https://reactrouter.com/start/concepts
+[migration-guide-from-v5-to-v6]: https://reactrouter.com/upgrading/v5
 [backwards-compatibility-package]: https://www.npmjs.com/package/react-router-dom-v5-compat
 [a-few-tweaks-to-improve-progressive-enhancement]: ../pages/philosophy#progressive-enhancement
 [routing-conventions]: ./routing
diff --git a/docs/pages/gotchas.md b/docs/pages/gotchas.md
index e98b68c44..837b55ae4 100644
--- a/docs/pages/gotchas.md
+++ b/docs/pages/gotchas.md
@@ -168,4 +168,4 @@ This is a hydration warning from React, and is most likely due to one of your br
 
 Check out the page in incognito mode, the warning should disappear.
 
-[remix-upload-handlers-like-unstable-create-file-upload-handler-and-unstable-create-memory-upload-handler]: ../api/remix#uploadhandler
+[remix-upload-handlers-like-unstable-create-file-upload-handler-and-unstable-create-memory-upload-handler]: ../utils/parse-multipart-form-data#uploadhandler
diff --git a/docs/route/handle.md b/docs/route/handle.md
index f640ded26..a7bd03c97 100644
--- a/docs/route/handle.md
+++ b/docs/route/handle.md
@@ -12,6 +12,6 @@ export const handle = {
 };
 ```
 
-This is almost always used on conjunction with `useMatches`. To see what kinds of things you can do with it, refer to [`useMatches`][use-matches] for more information.
+This is almost always used in conjunction with `useMatches`. To see what kinds of things you can do with it, refer to [`useMatches`][use-matches] for more information.
 
 [use-matches]: ../hooks/use-matches
diff --git a/docs/route/headers.md b/docs/route/headers.md
index a998a4e25..f9c448b98 100644
--- a/docs/route/headers.md
+++ b/docs/route/headers.md
@@ -113,4 +113,4 @@ export default function handleRequest(
 Just keep in mind that doing this will apply to _all_ document requests, but does not apply to `data` requests (for client-side transitions for example). For those, use [`handleDataRequest`][handledatarequest].
 
 [headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers
-[handledatarequest]: ../file-conventions/entry.server.tsx
+[handledatarequest]: ../file-conventions/entry.server
diff --git a/docs/route/index.md b/docs/route/index.md
index 4e05bab55..0838432e9 100644
--- a/docs/route/index.md
+++ b/docs/route/index.md
@@ -9,4 +9,4 @@ A route in Remix can be used for many things. Usually they’re used for the use
 
 It's important to read [Route Module Constraints][route-module-constraints].
 
-[route-module-constraints]: ../guides/constraints
+[route-module-constraints]: ./guides/constraints
diff --git a/docs/tutorials/jokes.md b/docs/tutorials/jokes.md
index 4491672ca..bb67c08fc 100644
--- a/docs/tutorials/jokes.md
+++ b/docs/tutorials/jokes.md
@@ -3125,7 +3125,7 @@ You may also notice that our solution makes use of the `login` route's `redirect
 
 <summary>app/routes/jokes/new.tsx</summary>
 
-```tsx filename=app/routes/jokes/new.tsx lines=[7,22,61]
+```tsx filename=app/routes/jokes/new.tsx lines=[7,22,51]
 import type { ActionArgs } from "@remix-run/node";
 import { json, redirect } from "@remix-run/node";
 import { useActionData } from "@remix-run/react";
@@ -3400,7 +3400,7 @@ export async function createUserSession(
 
 <summary>app/routes/jokes.tsx</summary>
 
-```tsx filename=app/routes/jokes.tsx lines=[14,22,27,34,38,60-71]
+```tsx filename=app/routes/jokes.tsx lines=[14,22,27,52-63]
 import type { User } from "@prisma/client";
 import type {
   LinksFunction,
@@ -5543,7 +5543,7 @@ There are reasons to include JavaScript on the page. For example, some common UI
 
 Ok, so let's load JavaScript on this page now 😆
 
-💿 Use Remix's [`<Scripts />` component][meta-component] component to load all the JavaScript files in `app/root.tsx`.
+💿 Use Remix's [`<Scripts />` component][scripts-component] component to load all the JavaScript files in `app/root.tsx`.
 
 <details>
 
@@ -6302,6 +6302,7 @@ Phew! And there we have it. If you made it through this whole thing then I'm rea
 [escaping-special-characters-here]: ../file-conventions/routes-files#escaping-special-characters
 [xml-document-for-rss-feed]: /jokes-tutorial/img/jokes-rss-feed.png
 [network-tab-indicating-no-java-script-is-loaded]: /jokes-tutorial/img/no-javascript.png
+[scripts-component]: ../components/scripts
 [network-tab-showing-java-script-loaded]: /jokes-tutorial/img/yes-javascript.png
 [browser-console-showing-the-log-of-a-server-side-error]: /jokes-tutorial/img/server-side-error-in-browser.png
 [form]: ../components/form
diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts
index 7cda81891..4238d6a3b 100644
--- a/integration/helpers/create-fixture.ts
+++ b/integration/helpers/create-fixture.ts
@@ -7,6 +7,7 @@ import stripIndent from "strip-indent";
 import { sync as spawnSync } from "cross-spawn";
 import type { JsonObject } from "type-fest";
 import type { ServerMode } from "@remix-run/server-runtime/mode";
+import type { FutureConfig } from "@remix-run/server-runtime/entry";
 
 import type { ServerBuild } from "../../build/node_modules/@remix-run/server-runtime";
 import { createRequestHandler } from "../../build/node_modules/@remix-run/server-runtime";
@@ -20,6 +21,7 @@ interface FixtureInit {
   files?: { [filename: string]: string };
   template?: "cf-template" | "deno-template" | "node-template";
   setup?: "node" | "cloudflare";
+  future?: Partial<FutureConfig>;
 }
 
 export type Fixture = Awaited<ReturnType<typeof createFixture>>;
@@ -174,6 +176,22 @@ export async function createFixtureProject(
       );
     }
   }
+
+  if (init.future) {
+    let contents = fse.readFileSync(
+      path.join(projectDir, "remix.config.js"),
+      "utf-8"
+    );
+    if (!contents.includes("future: {},")) {
+      throw new Error("Invalid formatted remix.config.js in template");
+    }
+    contents = contents.replace(
+      "future: {},",
+      "future: " + JSON.stringify(init.future) + ","
+    );
+    fse.writeFileSync(path.join(projectDir, "remix.config.js"), contents);
+  }
+
   await writeTestFiles(init, projectDir);
   build(projectDir, init.buildStdio, init.sourcemap);
 
diff --git a/integration/helpers/node-template/app/entry.client.tsx b/integration/helpers/node-template/app/entry.client.tsx
index f1fed31af..8338545d1 100644
--- a/integration/helpers/node-template/app/entry.client.tsx
+++ b/integration/helpers/node-template/app/entry.client.tsx
@@ -13,10 +13,10 @@ function hydrate() {
   });
 }
 
-if (window.requestIdleCallback) {
-  window.requestIdleCallback(hydrate);
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
 } else {
   // Safari doesn't support requestIdleCallback
   // https://caniuse.com/requestidlecallback
-  window.setTimeout(hydrate, 1);
+  setTimeout(hydrate, 1);
 }
diff --git a/integration/helpers/node-template/remix.config.js b/integration/helpers/node-template/remix.config.js
index adf2a0b5d..b7f693265 100644
--- a/integration/helpers/node-template/remix.config.js
+++ b/integration/helpers/node-template/remix.config.js
@@ -5,4 +5,8 @@ module.exports = {
   // assetsBuildDirectory: "public/build",
   // serverBuildPath: "build/index.js",
   // publicPath: "/build/",
+
+  // !!! Don't adust this without changing the code that overwrites this
+  // in createFixtureProject()
+  future: {},
 };
diff --git a/packages/create-remix/CHANGELOG.md b/packages/create-remix/CHANGELOG.md
index 5338316bd..78577a79a 100644
--- a/packages/create-remix/CHANGELOG.md
+++ b/packages/create-remix/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `create-remix`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/dev@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/create-remix/package.json b/packages/create-remix/package.json
index ac7fcea79..129087554 100644
--- a/packages/create-remix/package.json
+++ b/packages/create-remix/package.json
@@ -1,6 +1,6 @@
 {
   "name": "create-remix",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Create a new Remix app",
   "homepage": "https://remix.run",
   "bugs": {
@@ -17,7 +17,7 @@
     "create-remix": "dist/cli.js"
   },
   "dependencies": {
-    "@remix-run/dev": "1.10.0"
+    "@remix-run/dev": "1.10.1"
   },
   "engines": {
     "node": ">=14"
diff --git a/packages/remix-architect/CHANGELOG.md b/packages/remix-architect/CHANGELOG.md
index c451d381d..5b362d1c4 100644
--- a/packages/remix-architect/CHANGELOG.md
+++ b/packages/remix-architect/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/architect`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/node@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-architect/package.json b/packages/remix-architect/package.json
index 2f2861ac9..bc40a103a 100644
--- a/packages/remix-architect/package.json
+++ b/packages/remix-architect/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/architect",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Architect server request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -15,7 +15,7 @@
   "typings": "dist/index.d.ts",
   "dependencies": {
     "@architect/functions": "^5.2.0",
-    "@remix-run/node": "1.10.0",
+    "@remix-run/node": "1.10.1",
     "@types/aws-lambda": "^8.10.82"
   },
   "devDependencies": {
diff --git a/packages/remix-cloudflare-pages/CHANGELOG.md b/packages/remix-cloudflare-pages/CHANGELOG.md
index c28a97c2c..681ecd2ae 100644
--- a/packages/remix-cloudflare-pages/CHANGELOG.md
+++ b/packages/remix-cloudflare-pages/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/cloudflare-pages`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/cloudflare@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-cloudflare-pages/package.json b/packages/remix-cloudflare-pages/package.json
index 4d0cba562..f2b9c7e62 100644
--- a/packages/remix-cloudflare-pages/package.json
+++ b/packages/remix-cloudflare-pages/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/cloudflare-pages",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Cloudflare Pages request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -15,7 +15,7 @@
   "typings": "dist/index.d.ts",
   "module": "dist/esm/index.js",
   "dependencies": {
-    "@remix-run/cloudflare": "1.10.0"
+    "@remix-run/cloudflare": "1.10.1"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^3.4.0",
diff --git a/packages/remix-cloudflare-workers/CHANGELOG.md b/packages/remix-cloudflare-workers/CHANGELOG.md
index aff1c84d1..f1f94ca00 100644
--- a/packages/remix-cloudflare-workers/CHANGELOG.md
+++ b/packages/remix-cloudflare-workers/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/cloudflare-workers`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/cloudflare@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-cloudflare-workers/package.json b/packages/remix-cloudflare-workers/package.json
index f5fa6756f..66be8bae3 100644
--- a/packages/remix-cloudflare-workers/package.json
+++ b/packages/remix-cloudflare-workers/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/cloudflare-workers",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Cloudflare worker request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -16,7 +16,7 @@
   "module": "dist/esm/index.js",
   "dependencies": {
     "@cloudflare/kv-asset-handler": "^0.1.3",
-    "@remix-run/cloudflare": "1.10.0"
+    "@remix-run/cloudflare": "1.10.1"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^3.4.0"
diff --git a/packages/remix-cloudflare/CHANGELOG.md b/packages/remix-cloudflare/CHANGELOG.md
index c79983a55..ee626e70b 100644
--- a/packages/remix-cloudflare/CHANGELOG.md
+++ b/packages/remix-cloudflare/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/cloudflare`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/server-runtime@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-cloudflare/package.json b/packages/remix-cloudflare/package.json
index cefe0d51c..15f89ba0b 100644
--- a/packages/remix-cloudflare/package.json
+++ b/packages/remix-cloudflare/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/cloudflare",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Cloudflare platform abstractions for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -15,7 +15,7 @@
   "typings": "dist/index.d.ts",
   "dependencies": {
     "@cloudflare/kv-asset-handler": "^0.1.3",
-    "@remix-run/server-runtime": "1.10.0"
+    "@remix-run/server-runtime": "1.10.1"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^3.4.0"
diff --git a/packages/remix-deno/CHANGELOG.md b/packages/remix-deno/CHANGELOG.md
index 2e3a13b91..4111180b0 100644
--- a/packages/remix-deno/CHANGELOG.md
+++ b/packages/remix-deno/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/deno`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/server-runtime@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-deno/README.md b/packages/remix-deno/README.md
index a8877461c..5fe16dbcd 100644
--- a/packages/remix-deno/README.md
+++ b/packages/remix-deno/README.md
@@ -2,7 +2,7 @@
 
 This package contains a Deno module that provides abstractions for Remix.
 
-For more, see the [Remix docs](https://remix.run/docs/).
+For more, see the [Remix docs](https://remix.run/docs).
 
 ## Install
 
diff --git a/packages/remix-deno/package.json b/packages/remix-deno/package.json
index 472f9c73b..f30ca88ab 100644
--- a/packages/remix-deno/package.json
+++ b/packages/remix-deno/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/deno",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Deno platform abstractions for Remix",
   "homepage": "https://remix.run",
   "main": "./index.ts",
@@ -15,7 +15,7 @@
   "license": "MIT",
   "sideEffects": false,
   "dependencies": {
-    "@remix-run/server-runtime": "1.10.0",
+    "@remix-run/server-runtime": "1.10.1",
     "mime": "^3.0.0"
   },
   "engines": {
diff --git a/packages/remix-dev/CHANGELOG.md b/packages/remix-dev/CHANGELOG.md
index e49d2b94b..c7fe92c19 100644
--- a/packages/remix-dev/CHANGELOG.md
+++ b/packages/remix-dev/CHANGELOG.md
@@ -1,5 +1,13 @@
 # `@remix-run/dev`
 
+## 1.10.1
+
+### Patch Changes
+
+- Update babel config to transpile down to node 14 ([#5047](https://github.com/remix-run/remix/pull/5047))
+- Updated dependencies:
+  - `@remix-run/server-runtime@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index 1733a2926..143c9f6fe 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -36,6 +36,7 @@ interface FutureConfig {
   unstable_cssModules: boolean;
   unstable_cssSideEffectImports: boolean;
   unstable_vanillaExtract: boolean;
+  v2_errorBoundary: boolean;
   v2_meta: boolean;
   v2_routeConvention: boolean;
 }
@@ -495,6 +496,7 @@ export async function readConfig(
     unstable_cssSideEffectImports:
       appConfig.future?.unstable_cssSideEffectImports === true,
     unstable_vanillaExtract: appConfig.future?.unstable_vanillaExtract === true,
+    v2_errorBoundary: appConfig.future?.v2_errorBoundary === true,
     v2_meta: appConfig.future?.v2_meta === true,
     v2_routeConvention: appConfig.future?.v2_routeConvention === true,
   };
diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json
index f0e646559..8d8f8fb56 100644
--- a/packages/remix-dev/package.json
+++ b/packages/remix-dev/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/dev",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Dev tools and CLI for Remix",
   "homepage": "https://remix.run",
   "bugs": {
@@ -29,7 +29,7 @@
     "@babel/types": "^7.20.2",
     "@esbuild-plugins/node-modules-polyfill": "^0.1.4",
     "@npmcli/package-json": "^2.0.0",
-    "@remix-run/server-runtime": "1.10.0",
+    "@remix-run/server-runtime": "1.10.1",
     "@vanilla-extract/integration": "^6.0.1",
     "arg": "^5.0.1",
     "cacache": "^15.0.5",
@@ -71,7 +71,7 @@
     "xdm": "^2.0.0"
   },
   "devDependencies": {
-    "@remix-run/serve": "1.10.0",
+    "@remix-run/serve": "1.10.1",
     "@types/cacache": "^15.0.0",
     "@types/gunzip-maybe": "^1.4.0",
     "@types/inquirer": "^8.2.0",
@@ -91,7 +91,7 @@
     "type-fest": "^2.16.0"
   },
   "peerDependencies": {
-    "@remix-run/serve": "^1.10.0-pre.0"
+    "@remix-run/serve": "^1.10.1"
   },
   "peerDependenciesMeta": {
     "@remix-run/serve": {
diff --git a/packages/remix-eslint-config/CHANGELOG.md b/packages/remix-eslint-config/CHANGELOG.md
index f2e185bd9..b42427858 100644
--- a/packages/remix-eslint-config/CHANGELOG.md
+++ b/packages/remix-eslint-config/CHANGELOG.md
@@ -1,5 +1,9 @@
 # `@remix-run/eslint-config`
 
+## 1.10.1
+
+No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.10.1) for an overview of all changes in v1.10.1.
+
 ## 1.10.0
 
 No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.10.0) for an overview of all changes in v1.10.0.
diff --git a/packages/remix-eslint-config/package.json b/packages/remix-eslint-config/package.json
index 38d323f5b..e86fbd4bf 100644
--- a/packages/remix-eslint-config/package.json
+++ b/packages/remix-eslint-config/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/eslint-config",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "ESLint configuration for Remix projects",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
diff --git a/packages/remix-express/CHANGELOG.md b/packages/remix-express/CHANGELOG.md
index 60bc12a42..d7632e13b 100644
--- a/packages/remix-express/CHANGELOG.md
+++ b/packages/remix-express/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/express`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/node@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json
index 9ffdb862b..17062b795 100644
--- a/packages/remix-express/package.json
+++ b/packages/remix-express/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/express",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Express server request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -14,7 +14,7 @@
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "dependencies": {
-    "@remix-run/node": "1.10.0"
+    "@remix-run/node": "1.10.1"
   },
   "devDependencies": {
     "@types/express": "^4.17.9",
diff --git a/packages/remix-netlify/CHANGELOG.md b/packages/remix-netlify/CHANGELOG.md
index d32484f99..ba9400559 100644
--- a/packages/remix-netlify/CHANGELOG.md
+++ b/packages/remix-netlify/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/netlify`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/node@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-netlify/package.json b/packages/remix-netlify/package.json
index ca3c0ec74..204466d7d 100644
--- a/packages/remix-netlify/package.json
+++ b/packages/remix-netlify/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/netlify",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Netlify server request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -14,7 +14,7 @@
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "dependencies": {
-    "@remix-run/node": "1.10.0"
+    "@remix-run/node": "1.10.1"
   },
   "devDependencies": {
     "@netlify/functions": "^1.0.0"
diff --git a/packages/remix-node/CHANGELOG.md b/packages/remix-node/CHANGELOG.md
index e2fe011c0..254b2afb5 100644
--- a/packages/remix-node/CHANGELOG.md
+++ b/packages/remix-node/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/node`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/server-runtime@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json
index 335fc411b..99920db19 100644
--- a/packages/remix-node/package.json
+++ b/packages/remix-node/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/node",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Node.js platform abstractions for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -15,7 +15,7 @@
   "typings": "dist/index.d.ts",
   "sideEffects": false,
   "dependencies": {
-    "@remix-run/server-runtime": "1.10.0",
+    "@remix-run/server-runtime": "1.10.1",
     "@remix-run/web-fetch": "^4.3.2",
     "@remix-run/web-file": "^3.0.2",
     "@remix-run/web-stream": "^1.0.3",
diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts
index cdb7cf74c..bc03da253 100644
--- a/packages/remix-node/sessions/fileStorage.ts
+++ b/packages/remix-node/sessions/fileStorage.ts
@@ -27,7 +27,7 @@ interface FileSessionStorageOptions {
  * The advantage of using this instead of cookie session storage is that
  * files may contain much more data than cookies.
  *
- * @see https://remix.run/api/remix#createfilesessionstorage-node
+ * @see https://remix.run/utils/sessions#createfilesessionstorage-node
  */
 export function createFileSessionStorage({
   cookie,
diff --git a/packages/remix-react/CHANGELOG.md b/packages/remix-react/CHANGELOG.md
index 09eb7cd9b..a67f64458 100644
--- a/packages/remix-react/CHANGELOG.md
+++ b/packages/remix-react/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/react`
 
+## 1.10.1
+
+### Patch Changes
+
+- Fetchers should persist data through reload/resubmit ([#5065](https://github.com/remix-run/remix/pull/5065))
+- Update babel config to transpile down to node 14 ([#5047](https://github.com/remix-run/remix/pull/5047))
+
 ## 1.10.0
 
 ### Minor Changes
diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx
index ce280c23c..38110fe4c 100644
--- a/packages/remix-react/browser.tsx
+++ b/packages/remix-react/browser.tsx
@@ -39,7 +39,8 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement {
   if (!router) {
     let routes = createClientRoutes(
       window.__remixManifest.routes,
-      window.__remixRouteModules
+      window.__remixRouteModules,
+      window.__remixContext.future
     );
 
     let hydrationData = window.__remixContext.state;
diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx
index fadac712a..4c7c983f1 100644
--- a/packages/remix-react/components.tsx
+++ b/packages/remix-react/components.tsx
@@ -42,9 +42,9 @@ import type { AppData } from "./data";
 import type { EntryContext, RemixContextObject } from "./entry";
 import {
   RemixRootDefaultErrorBoundary,
-  RemixErrorBoundary,
   RemixRootDefaultCatchBoundary,
   RemixCatchBoundary,
+  V2_RemixRootDefaultErrorBoundary,
 } from "./errorBoundaries";
 import invariant from "./invariant";
 import {
@@ -141,7 +141,7 @@ export function RemixRoute({ id }: { id: string }) {
 }
 
 export function RemixRouteError({ id }: { id: string }) {
-  let { routeModules } = useRemixContext();
+  let { future, routeModules } = useRemixContext();
 
   // This checks prevent cryptic error messages such as: 'Cannot read properties of undefined (reading 'root')'
   invariant(
@@ -151,17 +151,20 @@ export function RemixRouteError({ id }: { id: string }) {
   );
 
   let error = useRouteError();
-  let location = useLocation();
   let { CatchBoundary, ErrorBoundary } = routeModules[id];
 
-  // POC for potential v2 error boundary handling
-  // if (future.v2_errorBoundary) {
-  //   // Provide defaults for the root route if they are not present
-  //   if (id === "root") {
-  //     ErrorBoundary ||= RemixRootDefaultNewErrorBoundary;
-  //   }
-  //   return <ErrorBoundary />
-  // }
+  if (future.v2_errorBoundary) {
+    // Provide defaults for the root route if they are not present
+    if (id === "root") {
+      ErrorBoundary ||= V2_RemixRootDefaultErrorBoundary;
+    }
+    if (ErrorBoundary) {
+      // TODO: Unsure if we can satisfy the typings here
+      // @ts-expect-error
+      return <ErrorBoundary />;
+    }
+    throw error;
+  }
 
   // Provide defaults for the root route if they are not present
   if (id === "root") {
@@ -177,13 +180,7 @@ export function RemixRouteError({ id }: { id: string }) {
       ErrorBoundary
     ) {
       // Internal framework-thrown ErrorResponses
-      return (
-        <RemixErrorBoundary
-          location={location}
-          component={ErrorBoundary!}
-          error={tError.error}
-        />
-      );
+      return <ErrorBoundary error={tError.error} />;
     }
     if (CatchBoundary) {
       // User-thrown ErrorResponses
@@ -198,13 +195,7 @@ export function RemixRouteError({ id }: { id: string }) {
 
   if (error instanceof Error && ErrorBoundary) {
     // User- or framework-thrown Errors
-    return (
-      <RemixErrorBoundary
-        location={location}
-        component={ErrorBoundary}
-        error={error as Error}
-      />
-    );
+    return <ErrorBoundary error={error} />;
   }
 
   throw error;
@@ -291,7 +282,7 @@ function usePrefetchBehavior(
 /**
  * A special kind of `<Link>` that knows whether or not it is "active".
  *
- * @see https://remix.run/api/remix#navlink
+ * @see https://remix.run/components/nav-link
  */
 let NavLink = React.forwardRef<HTMLAnchorElement, RemixNavLinkProps>(
   ({ to, prefetch = "none", ...props }, forwardedRef) => {
@@ -319,7 +310,7 @@ export { NavLink };
  * This component renders an anchor tag and is the primary way the user will
  * navigate around your website.
  *
- * @see https://remix.run/api/remix#link
+ * @see https://remix.run/components/link
  */
 let Link = React.forwardRef<HTMLAnchorElement, RemixLinkProps>(
   ({ to, prefetch = "none", ...props }, forwardedRef) => {
@@ -361,7 +352,7 @@ export function composeEventHandlers<
 /**
  * Renders the `<link>` tags for the current routes.
  *
- * @see https://remix.run/api/remix#meta-links-scripts
+ * @see https://remix.run/components/links
  */
 export function Links() {
   let { manifest, routeModules } = useRemixContext();
@@ -425,7 +416,7 @@ export function Links() {
  *
  * @param props
  * @param props.page
- * @see https://remix.run/api/remix#prefetchpagelinks-
+ * @see https://remix.run/components/prefetch-page-links
  */
 export function PrefetchPageLinks({
   page,
@@ -540,7 +531,7 @@ function PrefetchPageLinksImpl({
 /**
  * Renders the `<title>` and `<meta>` tags for the current routes.
  *
- * @see https://remix.run/api/remix#meta-links-scripts
+ * @see https://remix.run/components/meta
  */
 function V1Meta() {
   let { routeModules } = useRemixContext();
@@ -575,9 +566,9 @@ function V1Meta() {
             " returns an array. This is only supported with the `v2_meta` future flag " +
             "in the Remix config. Either set the flag to `true` or update the route's " +
             "meta function to return an object." +
-            "\n\nTo reference the v1 meta function API, see https://remix.run/api/conventions#meta"
+            "\n\nTo reference the v1 meta function API, see https://remix.run/route/meta"
           // TODO: Add link to the docs once they are written
-          // + "\n\nTo reference future flags and the v2 meta API, see https://remix.run/api/remix#future-v2-meta."
+          // + "\n\nTo reference future flags and the v2 meta API, see https://remix.run/file-conventions/remix-config#future-v2-meta."
         );
       }
       Object.assign(meta, routeMeta);
@@ -693,8 +684,8 @@ function V2Meta() {
           " returns an invalid value. In v2, all route meta functions must " +
           "return an array of meta objects." +
           // TODO: Add link to the docs once they are written
-          // "\n\nTo reference future flags and the v2 meta API, see https://remix.run/api/remix#future-v2-meta." +
-          "\n\nTo reference the v1 meta function API, see https://remix.run/api/conventions#meta"
+          // "\n\nTo reference future flags and the v2 meta API, see https://remix.run/file-conventions/remix-config#future-v2-meta." +
+          "\n\nTo reference the v1 meta function API, see https://remix.run/route/meta"
       );
     }
 
@@ -762,7 +753,7 @@ export type ScriptProps = Omit<
  * In addition to scripts, \<link rel="modulepreload"> tags receive the crossOrigin
  * property if provided.
  *
- * @see https://remix.run/api/remix#meta-links-scripts
+ * @see https://remix.run/components/scripts
  */
 export function Scripts(props: ScriptProps) {
   let { manifest, serverHandoffString } = useRemixContext();
@@ -884,7 +875,7 @@ export interface RouteMatch {
   /**
    * The dynamic parameters of the matched route
    *
-   * @see https://remix.run/docs/api/conventions#dynamic-route-parameters
+   * @see https://remix.run/file-conventions/routes-files#dynamic-route-parameters
    */
   params: Params<string>;
   /**
@@ -894,7 +885,7 @@ export interface RouteMatch {
   /**
    * The exported `handle` object of the matched route.
    *
-   * @see https://remix.run/docs/api/conventions#handle
+   * @see https://remix.run/route/handle
    */
   handle: undefined | { [key: string]: any };
 }
@@ -919,7 +910,7 @@ export function useMatches(): RouteMatch[] {
 /**
  * Returns the JSON parsed data from the current route's `loader`.
  *
- * @see https://remix.run/api/remix#useloaderdata
+ * @see https://remix.run/hooks/use-loader-data
  */
 export function useLoaderData<T = AppData>(): SerializeFrom<T> {
   return useLoaderDataRR() as SerializeFrom<T>;
@@ -928,7 +919,7 @@ export function useLoaderData<T = AppData>(): SerializeFrom<T> {
 /**
  * Returns the JSON parsed data from the current route's `action`.
  *
- * @see https://remix.run/api/remix#useactiondata
+ * @see https://remix.run/hooks/use-action-data
  */
 export function useActionData<T = AppData>(): SerializeFrom<T> | undefined {
   return useActionDataRR() as SerializeFrom<T> | undefined;
@@ -938,7 +929,7 @@ export function useActionData<T = AppData>(): SerializeFrom<T> | undefined {
  * Returns everything you need to know about a page transition to build pending
  * navigation indicators and optimistic UI on data mutations.
  *
- * @see https://remix.run/api/remix#usetransition
+ * @see https://remix.run/hooks/use-transition
  */
 export function useTransition(): Transition {
   let navigation = useNavigation();
@@ -1138,7 +1129,7 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
  * Interacts with route loaders and actions without causing a navigation. Great
  * for any interaction that stays on the same page.
  *
- * @see https://remix.run/api/remix#usefetcher
+ * @see https://remix.run/hooks/use-fetcher
  */
 export function useFetcher<TData = any>(): FetcherWithComponents<
   SerializeFrom<TData>
@@ -1209,7 +1200,7 @@ function convertRouterFetcherToRemixFetcher(
           formData: formData,
           key: "",
         },
-        data: undefined,
+        data,
       };
       return fetcher;
     } else {
@@ -1292,7 +1283,7 @@ function convertRouterFetcherToRemixFetcher(
             formData: formData,
             key: "",
           },
-          data: undefined,
+          data,
         };
         return fetcher;
       }
@@ -1304,7 +1295,7 @@ function convertRouterFetcherToRemixFetcher(
     state: "loading",
     type: "normalLoad",
     submission: undefined,
-    data: undefined,
+    data,
   };
   return fetcher;
 }
diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts
index 77f17b498..33bef6168 100644
--- a/packages/remix-react/entry.ts
+++ b/packages/remix-react/entry.ts
@@ -21,6 +21,7 @@ export interface FutureConfig {
   unstable_cssModules: boolean;
   unstable_cssSideEffectImports: boolean;
   unstable_vanillaExtract: boolean;
+  v2_errorBoundary: boolean;
   v2_meta: boolean;
 }
 
diff --git a/packages/remix-react/errorBoundaries.tsx b/packages/remix-react/errorBoundaries.tsx
index f2f8916e8..faa50dd41 100644
--- a/packages/remix-react/errorBoundaries.tsx
+++ b/packages/remix-react/errorBoundaries.tsx
@@ -1,6 +1,6 @@
 import React, { useContext } from "react";
-import type { ErrorResponse } from "@remix-run/router";
-import type { Location } from "react-router-dom";
+import type { ErrorResponse, Location } from "@remix-run/router";
+import { isRouteErrorResponse, useRouteError } from "react-router-dom";
 
 import type {
   CatchBoundaryComponent,
@@ -109,6 +109,23 @@ export function RemixRootDefaultErrorBoundary({ error }: { error: Error }) {
   );
 }
 
+export function V2_RemixRootDefaultErrorBoundary() {
+  let error = useRouteError();
+  if (isRouteErrorResponse(error)) {
+    return <RemixRootDefaultCatchBoundaryImpl caught={error} />;
+  } else if (error instanceof Error) {
+    return <RemixRootDefaultErrorBoundary error={error} />;
+  } else {
+    let errorString =
+      error == null
+        ? "Unknown Error"
+        : typeof error === "object" && "toString" in error
+        ? error.toString()
+        : JSON.stringify(error);
+    return <RemixRootDefaultErrorBoundary error={new Error(errorString)} />;
+  }
+}
+
 let RemixCatchContext = React.createContext<ThrownResponse | undefined>(
   undefined
 );
@@ -116,7 +133,7 @@ let RemixCatchContext = React.createContext<ThrownResponse | undefined>(
 /**
  * Returns the status code and thrown response data.
  *
- * @see https://remix.run/api/conventions#catchboundary
+ * @see https://remix.run/route/catch-boundary
  */
 export function useCatch<
   Result extends ThrownResponse = ThrownResponse
@@ -150,6 +167,14 @@ export function RemixCatchBoundary({
  */
 export function RemixRootDefaultCatchBoundary() {
   let caught = useCatch();
+  return <RemixRootDefaultCatchBoundaryImpl caught={caught} />;
+}
+
+function RemixRootDefaultCatchBoundaryImpl({
+  caught,
+}: {
+  caught: ThrownResponse;
+}) {
   return (
     <html lang="en">
       <head>
diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx
index 5af30425e..5bad944d4 100644
--- a/packages/remix-react/index.tsx
+++ b/packages/remix-react/index.tsx
@@ -13,6 +13,7 @@ export type {
 export {
   Form,
   Outlet,
+  isRouteErrorResponse,
   useBeforeUnload,
   useFormAction,
   useHref,
@@ -25,6 +26,7 @@ export {
   useParams,
   useResolvedPath,
   useRevalidator,
+  useRouteError,
   useRouteLoaderData,
   useSearchParams,
   useSubmit,
diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json
index a0846da78..6c545c7b5 100644
--- a/packages/remix-react/package.json
+++ b/packages/remix-react/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/react",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "React DOM bindings for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -21,7 +21,7 @@
     "use-sync-external-store": "1.2.0"
   },
   "devDependencies": {
-    "@remix-run/server-runtime": "1.10.0",
+    "@remix-run/server-runtime": "1.10.1",
     "@testing-library/jest-dom": "^5.16.2",
     "@testing-library/react": "^13.3.0",
     "abort-controller": "^3.0.0",
diff --git a/packages/remix-react/routeModules.ts b/packages/remix-react/routeModules.ts
index db5496ce2..785add903 100644
--- a/packages/remix-react/routeModules.ts
+++ b/packages/remix-react/routeModules.ts
@@ -17,7 +17,7 @@ export interface RouteModules {
 
 export interface RouteModule {
   CatchBoundary?: CatchBoundaryComponent;
-  ErrorBoundary?: ErrorBoundaryComponent;
+  ErrorBoundary?: ErrorBoundaryComponent | V2_ErrorBoundaryComponent;
   default: RouteComponent;
   handle?: RouteHandle;
   links?: LinksFunction;
@@ -32,22 +32,29 @@ export interface RouteModule {
 /**
  * A React component that is rendered when the server throws a Response.
  *
- * @see https://remix.run/api/conventions#catchboundary
+ * @see https://remix.run/route/catch-boundary
  */
 export type CatchBoundaryComponent = ComponentType<{}>;
 
 /**
  * A React component that is rendered when there is an error on a route.
  *
- * @see https://remix.run/api/conventions#errorboundary
+ * @see https://remix.run/route/error-boundary
  */
 export type ErrorBoundaryComponent = ComponentType<{ error: Error }>;
 
+/**
+ * V2 version of the ErrorBoundary that eliminates the distinction between
+ * Error and Catch Boundaries and behaves like RR 6.4 errorElement and captures
+ * errors with useRouteError()
+ */
+export type V2_ErrorBoundaryComponent = ComponentType;
+
 /**
  * A function that defines `<link>` tags to be inserted into the `<head>` of
  * the document on route transitions.
  *
- * @see https://remix.run/api/remix#meta-links-scripts
+ * @see https://remix.run/route/meta
  */
 export interface LinksFunction {
   (): LinkDescriptor[];
@@ -58,7 +65,7 @@ export interface LinksFunction {
  * `<meta>` tags for a route. These tags will be merged with (and take
  * precedence over) tags from parent routes.
  *
- * @see https://remix.run/api/remix#meta-links-scripts
+ * @see https://remix.run/route/meta
  */
 export interface V1_MetaFunction {
   (args: {
@@ -123,7 +130,7 @@ export type RouteComponent = ComponentType<{}>;
 /**
  * An arbitrary object that is associated with a route.
  *
- * @see https://remix.run/api/conventions#handle
+ * @see https://remix.run/route/handle
  */
 export type RouteHandle = any;
 
diff --git a/packages/remix-react/routes.tsx b/packages/remix-react/routes.tsx
index 15c237a7d..0b342f808 100644
--- a/packages/remix-react/routes.tsx
+++ b/packages/remix-react/routes.tsx
@@ -10,6 +10,7 @@ import { redirect } from "react-router-dom";
 import type { RouteModules } from "./routeModules";
 import { loadRouteModule } from "./routeModules";
 import { fetchData, isCatchResponse, isRedirectResponse } from "./data";
+import type { FutureConfig } from "./entry";
 import { prefetchStyleLinks } from "./links";
 import invariant from "./invariant";
 import { RemixRoute, RemixRouteError } from "./components";
@@ -41,13 +42,18 @@ export interface EntryRoute extends Route {
 export function createServerRoutes(
   manifest: RouteManifest<EntryRoute>,
   routeModules: RouteModules,
+  future: FutureConfig,
   parentId?: string
 ): DataRouteObject[] {
   return Object.values(manifest)
     .filter((route) => route.parentId === parentId)
     .map((route) => {
       let hasErrorBoundary =
-        route.id === "root" || route.hasErrorBoundary || route.hasCatchBoundary;
+        future.v2_errorBoundary === true
+          ? route.id === "root" || route.hasErrorBoundary
+          : route.id === "root" ||
+            route.hasCatchBoundary ||
+            route.hasErrorBoundary;
       let dataRoute: DataRouteObject = {
         caseSensitive: route.caseSensitive,
         element: <RemixRoute id={route.id} />,
@@ -62,7 +68,12 @@ export function createServerRoutes(
         // since they're for a static render
       };
 
-      let children = createServerRoutes(manifest, routeModules, route.id);
+      let children = createServerRoutes(
+        manifest,
+        routeModules,
+        future,
+        route.id
+      );
       if (children.length > 0) dataRoute.children = children;
       return dataRoute;
     });
@@ -71,13 +82,18 @@ export function createServerRoutes(
 export function createClientRoutes(
   manifest: RouteManifest<EntryRoute>,
   routeModulesCache: RouteModules,
+  future: FutureConfig,
   parentId?: string
 ): DataRouteObject[] {
   return Object.values(manifest)
     .filter((entryRoute) => entryRoute.parentId === parentId)
     .map((route) => {
       let hasErrorBoundary =
-        route.id === "root" || route.hasErrorBoundary || route.hasCatchBoundary;
+        future.v2_errorBoundary === true
+          ? route.id === "root" || route.hasErrorBoundary
+          : route.id === "root" ||
+            route.hasCatchBoundary ||
+            route.hasErrorBoundary;
 
       let dataRoute: DataRouteObject = {
         caseSensitive: route.caseSensitive,
@@ -95,7 +111,12 @@ export function createClientRoutes(
         action: createDataFunction(route, routeModulesCache, true),
         shouldRevalidate: createShouldRevalidate(route, routeModulesCache),
       };
-      let children = createClientRoutes(manifest, routeModulesCache, route.id);
+      let children = createClientRoutes(
+        manifest,
+        routeModulesCache,
+        future,
+        route.id
+      );
       if (children.length > 0) dataRoute.children = children;
       return dataRoute;
     });
diff --git a/packages/remix-react/scroll-restoration.tsx b/packages/remix-react/scroll-restoration.tsx
index 70ab75f17..31694ee11 100644
--- a/packages/remix-react/scroll-restoration.tsx
+++ b/packages/remix-react/scroll-restoration.tsx
@@ -14,7 +14,7 @@ let STORAGE_KEY = "positions";
  * This component will emulate the browser's scroll restoration on location
  * changes.
  *
- * @see https://remix.run/api/remix#scrollrestoration
+ * @see https://remix.run/components/scroll-restoration
  */
 export function ScrollRestoration({
   getKey,
diff --git a/packages/remix-react/server.tsx b/packages/remix-react/server.tsx
index 61bc6816c..1f3786924 100644
--- a/packages/remix-react/server.tsx
+++ b/packages/remix-react/server.tsx
@@ -29,7 +29,11 @@ export function RemixServer({ context, url }: RemixServerProps): ReactElement {
   }
 
   let { manifest, routeModules, serverHandoffString } = context;
-  let routes = createServerRoutes(manifest.routes, routeModules);
+  let routes = createServerRoutes(
+    manifest.routes,
+    routeModules,
+    context.future
+  );
   let router = createStaticRouter(routes, context.staticHandlerContext);
 
   return (
diff --git a/packages/remix-react/transition.ts b/packages/remix-react/transition.ts
index fb052fc85..8b0ec322d 100644
--- a/packages/remix-react/transition.ts
+++ b/packages/remix-react/transition.ts
@@ -120,7 +120,7 @@ export type FetcherStates<TData = any> = {
     formData: FormData;
     formEncType: string;
     submission: ActionSubmission;
-    data: undefined;
+    data: TData | undefined;
   };
   SubmittingLoader: {
     state: "submitting";
diff --git a/packages/remix-serve/CHANGELOG.md b/packages/remix-serve/CHANGELOG.md
index a347b39dd..1b05f457e 100644
--- a/packages/remix-serve/CHANGELOG.md
+++ b/packages/remix-serve/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/serve`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/express@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json
index c664514c1..a97949336 100644
--- a/packages/remix-serve/package.json
+++ b/packages/remix-serve/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/serve",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Production application server for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -17,7 +17,7 @@
     "remix-serve": "dist/cli.js"
   },
   "dependencies": {
-    "@remix-run/express": "1.10.0",
+    "@remix-run/express": "1.10.1",
     "compression": "^1.7.4",
     "express": "^4.17.1",
     "morgan": "^1.10.0"
diff --git a/packages/remix-server-runtime/CHANGELOG.md b/packages/remix-server-runtime/CHANGELOG.md
index 2803e1228..f2c6ba38d 100644
--- a/packages/remix-server-runtime/CHANGELOG.md
+++ b/packages/remix-server-runtime/CHANGELOG.md
@@ -1,5 +1,9 @@
 # `@remix-run/server-runtime`
 
+## 1.10.1
+
+No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.10.1) for an overview of all changes in v1.10.1.
+
 ## 1.10.0
 
 ### Minor Changes
diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts
index aa909fc76..ebc87d98a 100644
--- a/packages/remix-server-runtime/cookies.ts
+++ b/packages/remix-server-runtime/cookies.ts
@@ -30,7 +30,7 @@ export type CookieOptions = CookieParseOptions &
  * `serialize()` methods that allow a single instance to be reused for
  * parsing/encoding multiple different values.
  *
- * @see https://remix.run/api/remix#cookie-api
+ * @see https://remix.run/utils/cookies#cookie-api
  */
 export interface Cookie {
   /**
@@ -75,7 +75,7 @@ export type CreateCookieFunction = (
 /**
  * Creates a logical container for managing a browser cookie from the server.
  *
- * @see https://remix.run/api/remix#createcookie
+ * @see https://remix.run/utils/cookies#createcookie
  */
 export const createCookieFactory =
   ({
@@ -135,7 +135,7 @@ export type IsCookieFunction = (object: any) => object is Cookie;
 /**
  * Returns true if an object is a Remix cookie container.
  *
- * @see https://remix.run/api/remix#iscookie
+ * @see https://remix.run/utils/cookies#iscookie
  */
 export const isCookie: IsCookieFunction = (object): object is Cookie => {
   return (
diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts
index 152bf2ad1..7dddbb380 100644
--- a/packages/remix-server-runtime/entry.ts
+++ b/packages/remix-server-runtime/entry.ts
@@ -15,6 +15,7 @@ export interface FutureConfig {
   unstable_cssModules: true;
   unstable_cssSideEffectImports: boolean;
   unstable_vanillaExtract: boolean;
+  v2_errorBoundary: boolean;
   v2_meta: boolean;
 }
 
diff --git a/packages/remix-server-runtime/formData.ts b/packages/remix-server-runtime/formData.ts
index 8d6d40955..5e1baef2d 100644
--- a/packages/remix-server-runtime/formData.ts
+++ b/packages/remix-server-runtime/formData.ts
@@ -30,7 +30,7 @@ export function composeUploadHandlers(
  * Allows you to handle multipart forms (file uploads) for your app.
  *
  * TODO: Update this comment
- * @see https://remix.run/api/remix#parsemultipartformdata-node
+ * @see https://remix.run/utils/parse-multipart-form-data
  */
 export async function parseMultipartFormData(
   request: Request,
diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json
index 3cfb68eac..a951fd9d1 100644
--- a/packages/remix-server-runtime/package.json
+++ b/packages/remix-server-runtime/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/server-runtime",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Server runtime for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts
index c27b4ddb0..e442dc68c 100644
--- a/packages/remix-server-runtime/responses.ts
+++ b/packages/remix-server-runtime/responses.ts
@@ -16,7 +16,7 @@ export type TypedResponse<T extends unknown = unknown> = Omit<
  * This is a shortcut for creating `application/json` responses. Converts `data`
  * to JSON and sets the `Content-Type` header.
  *
- * @see https://remix.run/api/remix#json
+ * @see https://remix.run/utils/json
  */
 export const json: JsonFunction = (data, init = {}) => {
   let responseInit = typeof init === "number" ? { status: init } : init;
@@ -41,7 +41,7 @@ export type RedirectFunction = (
  * A redirect response. Sets the status code and the `Location` header.
  * Defaults to "302 Found".
  *
- * @see https://remix.run/api/remix#redirect
+ * @see https://remix.run/utils/redirect
  */
 export const redirect: RedirectFunction = (url, init = 302) => {
   let responseInit = init;
diff --git a/packages/remix-server-runtime/routes.ts b/packages/remix-server-runtime/routes.ts
index 19a48e589..6c63ae870 100644
--- a/packages/remix-server-runtime/routes.ts
+++ b/packages/remix-server-runtime/routes.ts
@@ -5,6 +5,7 @@ import type {
 } from "@remix-run/router";
 
 import { callRouteActionRR, callRouteLoaderRR } from "./data";
+import type { FutureConfig } from "./entry";
 import type { ServerRouteModule } from "./routeModules";
 
 export interface RouteManifest<Route> {
@@ -54,17 +55,21 @@ export function createRoutes(
 // createStaticHandler
 export function createStaticHandlerDataRoutes(
   manifest: ServerRouteManifest,
+  future: FutureConfig,
   parentId?: string
 ): AgnosticDataRouteObject[] {
   return Object.values(manifest)
     .filter((route) => route.parentId === parentId)
     .map((route) => {
+      let hasErrorBoundary =
+        future.v2_errorBoundary === true
+          ? route.id === "root" || route.module.ErrorBoundary != null
+          : route.id === "root" ||
+            route.module.CatchBoundary != null ||
+            route.module.ErrorBoundary != null;
       let commonRoute = {
         // Always include root due to default boundaries
-        hasErrorBoundary:
-          route.id === "root" ||
-          route.module.CatchBoundary != null ||
-          route.module.ErrorBoundary != null,
+        hasErrorBoundary,
         id: route.id,
         path: route.path,
         loader: route.module.loader
@@ -97,7 +102,7 @@ export function createStaticHandlerDataRoutes(
           }
         : {
             caseSensitive: route.caseSensitive,
-            children: createStaticHandlerDataRoutes(manifest, route.id),
+            children: createStaticHandlerDataRoutes(manifest, future, route.id),
             ...commonRoute,
           };
     });
diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts
index 7c0142760..d62f8a19e 100644
--- a/packages/remix-server-runtime/server.ts
+++ b/packages/remix-server-runtime/server.ts
@@ -34,7 +34,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
   mode
 ) => {
   let routes = createRoutes(build.routes);
-  let dataRoutes = createStaticHandlerDataRoutes(build.routes);
+  let dataRoutes = createStaticHandlerDataRoutes(build.routes, build.future);
   let serverMode = isServerMode(mode) ? mode : ServerMode.Production;
   let staticHandler = createStaticHandler(dataRoutes);
 
@@ -230,7 +230,9 @@ async function handleDocumentRequestRR(
   }
 
   // Restructure context.errors to the right Catch/Error Boundary
-  differentiateCatchVersusErrorBoundaries(build, context);
+  if (build.future.v2_errorBoundary !== true) {
+    differentiateCatchVersusErrorBoundaries(build, context);
+  }
 
   let headers = getDocumentHeadersRR(build, context);
 
@@ -266,7 +268,9 @@ async function handleDocumentRequestRR(
     );
 
     // Restructure context.errors to the right Catch/Error Boundary
-    differentiateCatchVersusErrorBoundaries(build, context);
+    if (build.future.v2_errorBoundary !== true) {
+      differentiateCatchVersusErrorBoundaries(build, context);
+    }
 
     // Update entryContext for the second render pass
     entryContext = {
diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts
index 4367d110c..6d3a05bec 100644
--- a/packages/remix-server-runtime/sessions.ts
+++ b/packages/remix-server-runtime/sessions.ts
@@ -14,7 +14,7 @@ export interface SessionData {
 /**
  * Session persists data across HTTP requests.
  *
- * @see https://remix.run/api/remix#session-api
+ * @see https://remix.run/utils/sessions#session-api
  */
 export interface Session {
   /**
@@ -76,7 +76,7 @@ export type CreateSessionFunction = (
  * Note: This function is typically not invoked directly by application code.
  * Instead, use a `SessionStorage` object's `getSession` method.
  *
- * @see https://remix.run/api/remix#createsession
+ * @see https://remix.run/utils/sessions#createsession
  */
 export const createSession: CreateSessionFunction = (
   initialData = {},
@@ -123,7 +123,7 @@ export type IsSessionFunction = (object: any) => object is Session;
 /**
  * Returns true if an object is a Remix session.
  *
- * @see https://remix.run/api/remix#issession
+ * @see https://remix.run/utils/sessions#issession
  */
 export const isSession: IsSessionFunction = (object): object is Session => {
   return (
@@ -222,7 +222,7 @@ export type CreateSessionStorageFunction = (
  * Note: This is a low-level API that should only be used if none of the
  * existing session storage options meet your requirements.
  *
- * @see https://remix.run/api/remix#createsessionstorage
+ * @see https://remix.run/utils/sessions#createsessionstorage
  */
 export const createSessionStorageFactory =
   (createCookie: CreateCookieFunction): CreateSessionStorageFunction =>
@@ -265,7 +265,7 @@ export function warnOnceAboutSigningSessionCookie(cookie: Cookie) {
     cookie.isSigned,
     `The "${cookie.name}" cookie is not signed, but session cookies should be ` +
       `signed to prevent tampering on the client before they are sent back to the ` +
-      `server. See https://remix.run/api/remix#signing-cookies ` +
+      `server. See https://remix.run/utils/cookies#signing-cookies ` +
       `for more information.`
   );
 }
diff --git a/packages/remix-server-runtime/sessions/cookieStorage.ts b/packages/remix-server-runtime/sessions/cookieStorage.ts
index 90d0a34da..2c3223f82 100644
--- a/packages/remix-server-runtime/sessions/cookieStorage.ts
+++ b/packages/remix-server-runtime/sessions/cookieStorage.ts
@@ -24,7 +24,7 @@ export type CreateCookieSessionStorageFunction = (
  * also has the limitation that serialized session data may not exceed the
  * browser's maximum cookie size. Trade-offs!
  *
- * @see https://remix.run/api/remix#createcookiesessionstorage
+ * @see https://remix.run/utils/sessions#createcookiesessionstorage
  */
 export const createCookieSessionStorageFactory =
   (createCookie: CreateCookieFunction): CreateCookieSessionStorageFunction =>
diff --git a/packages/remix-server-runtime/sessions/memoryStorage.ts b/packages/remix-server-runtime/sessions/memoryStorage.ts
index 0398eead8..f7ebcedf7 100644
--- a/packages/remix-server-runtime/sessions/memoryStorage.ts
+++ b/packages/remix-server-runtime/sessions/memoryStorage.ts
@@ -24,7 +24,7 @@ export type CreateMemorySessionStorageFunction = (
  * Note: This storage does not scale beyond a single process, so it is not
  * suitable for most production scenarios.
  *
- * @see https://remix.run/api/remix#creatememorysessionstorage
+ * @see https://remix.run/utils/sessions#creatememorysessionstorage
  */
 export const createMemorySessionStorageFactory =
   (
diff --git a/packages/remix-vercel/CHANGELOG.md b/packages/remix-vercel/CHANGELOG.md
index dc11e4c19..70de80176 100644
--- a/packages/remix-vercel/CHANGELOG.md
+++ b/packages/remix-vercel/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/vercel`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/node@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-vercel/package.json b/packages/remix-vercel/package.json
index 76d2c42fc..b822f5181 100644
--- a/packages/remix-vercel/package.json
+++ b/packages/remix-vercel/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/vercel",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Vercel server request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -14,7 +14,7 @@
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "dependencies": {
-    "@remix-run/node": "1.10.0"
+    "@remix-run/node": "1.10.1"
   },
   "devDependencies": {
     "@types/supertest": "^2.0.10",
diff --git a/packages/remix/package.json b/packages/remix/package.json
index cdaafd3a1..281c7fbc8 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -1,6 +1,6 @@
 {
   "name": "remix",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "A framework for building better websites",
   "homepage": "https://remix.run",
   "bugs": {
diff --git a/scripts/patchup-version.mjs b/scripts/patchup-version.mjs
index 16ad422dc..6add0cbc3 100644
--- a/scripts/patchup-version.mjs
+++ b/scripts/patchup-version.mjs
@@ -49,52 +49,51 @@ async function bumpPeerDepRanges() {
   }
 
   let allPackages = getPackagesSync(rootDir).packages;
-  let pkgChanges = new Map(
-    gitStatusResult.stdout
-      .toString()
-      .trim()
-      .split("\n")
-      .filter((line) => /^\s*M\s+.*\/package.json/.test(line))
-      .map((line) => {
-        /**
-         * @type {string}
-         * This will always be defined but TS doesn't know that
-         * @ts-expect-error */
-        let gitPath = line.match(/[^\s]+package.json/)[0];
-        let fsPath = path.join(rootDir, gitPath);
-        let packageJson = JSON.parse(fs.readFileSync(fsPath, "utf-8"));
-        let previousPackageJsonResult = spawnSync("git", [
-          "show",
-          `HEAD:${gitPath}`,
-        ]);
+  //   let pkgChanges = new Map(
+  //     gitStatusResult.stdout
+  //       .toString()
+  //       .trim()
+  //       .split("\n")
+  //       .filter((line) => /^\s*M\s+.*\/package.json/.test(line))
+  //       .map((line) => {
+  //         /**
+  //          * @type {string}
+  //          * This will always be defined but TS doesn't know that
+  //          * @ts-expect-error */
+  //         let gitPath = line.match(/[^\s]+package.json/)[0];
+  //         let fsPath = path.join(rootDir, gitPath);
+  //         let packageJson = JSON.parse(fs.readFileSync(fsPath, "utf-8"));
+  //         let previousPackageJsonResult = spawnSync("git", [
+  //           "show",
+  //           `HEAD:${gitPath}`,
+  //         ]);
 
-        if (previousPackageJsonResult.status !== 0) {
-          process.exit(gitStatusResult.status || undefined);
-        }
+  //         if (previousPackageJsonResult.status !== 0) {
+  //           process.exit(gitStatusResult.status || undefined);
+  //         }
 
-        return [
-          packageJson.name,
-          {
-            path: fsPath,
-            packageJson: packageJson,
-            versionChanged:
-              packageJson.version !==
-              JSON.parse(previousPackageJsonResult.stdout.toString().trim())
-                .version,
-          },
-        ];
-      })
-  );
+  //         return [
+  //           packageJson.name,
+  //           {
+  //             path: fsPath,
+  //             packageJson: packageJson,
+  //             versionChanged:
+  //               packageJson.version !==
+  //               JSON.parse(previousPackageJsonResult.stdout.toString().trim())
+  //                 .version,
+  //           },
+  //         ];
+  //       })
+  //   );
 
   for (let pkg of allPackages) {
     let peerPkg = pkg.packageJson.name;
-    let peerPkgChange = pkgChanges.get(peerPkg);
-    if (!peerPkgChange || !peerPkgChange.versionChanged) {
-      continue;
-    }
+    let peerPkgVersion = pkg.packageJson.version;
+    // let peerPkgChange = pkgChanges.get(peerPkg);
+    // if (!peerPkgChange || !peerPkgChange.versionChanged) {
+    //   continue;
+    // }
 
-    /** @type {Promise<any>[]} */
-    let writeFilePromises = [];
     for (let dependentPkg of allPackages) {
       let peerDeps = dependentPkg.packageJson.peerDependencies;
       if (!peerDeps || !peerDeps[peerPkg]) {
@@ -103,20 +102,14 @@ async function bumpPeerDepRanges() {
       let pkgJsonCopy = { ...dependentPkg.packageJson };
       // TS not smart enough to realize we checked this before copying the object
       // @ts-expect-error
-      pkgJsonCopy.peerDependencies[
-        peerPkg
-      ] = `^${peerPkgChange.packageJson.version}`;
+      pkgJsonCopy.peerDependencies[peerPkg] = `^${peerPkgVersion}`;
 
-      writeFilePromises.push(
-        fs.promises.writeFile(
-          path.join(dependentPkg.dir, "package.json"),
-          JSON.stringify(pkgJsonCopy, null, 2) + "\n",
-          "utf-8"
-        )
+      await fs.promises.writeFile(
+        path.join(dependentPkg.dir, "package.json"),
+        JSON.stringify(pkgJsonCopy, null, 2) + "\n",
+        "utf-8"
       );
     }
-
-    return Promise.all(writeFilePromises);
   }
 }
 
diff --git a/scripts/playground/template/app/entry.client.tsx b/scripts/playground/template/app/entry.client.tsx
index 1d4ba68d5..93347c1e9 100644
--- a/scripts/playground/template/app/entry.client.tsx
+++ b/scripts/playground/template/app/entry.client.tsx
@@ -13,10 +13,10 @@ const hydrate = () => {
   });
 };
 
-if (window.requestIdleCallback) {
-  window.requestIdleCallback(hydrate);
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
 } else {
   // Safari doesn't support requestIdleCallback
   // https://caniuse.com/requestidlecallback
-  window.setTimeout(hydrate, 1);
+  setTimeout(hydrate, 1);
 }
diff --git a/scripts/release/find-release-from-changeset.js b/scripts/release/find-release-from-changeset.js
new file mode 100644
index 000000000..ea0cb529e
--- /dev/null
+++ b/scripts/release/find-release-from-changeset.js
@@ -0,0 +1,38 @@
+/**
+ *
+ * @param {string | undefined} publishedPackages
+ * @param {string | undefined} packageVersionToFollow
+ * @returns {string | undefined}
+ */
+function findReleaseFromChangeset(publishedPackages, packageVersionToFollow) {
+  if (!publishedPackages) {
+    throw new Error("No published packages found");
+  }
+
+  let packages = JSON.parse(publishedPackages);
+
+  if (!Array.isArray(packages)) {
+    throw new Error("Published packages is not an array");
+  }
+
+  /** @see https://github.com/changesets/action#outputs */
+  /** @type { { name: string; version: string }[] }  */
+  let typed = packages.filter((pkg) => "name" in pkg && "version" in pkg);
+
+  let found = typed.find((pkg) => pkg.name === packageVersionToFollow);
+
+  if (!found) {
+    throw new Error(
+      `${packageVersionToFollow} was not found in the published packages`
+    );
+  }
+
+  let result = `${found.name}@${found.version}`;
+  console.log(result);
+  return result;
+}
+
+findReleaseFromChangeset(
+  process.env.publishedPackages,
+  process.env.packageVersionToFollow
+);
diff --git a/scripts/remove-prerelease-changelogs.mjs b/scripts/remove-prerelease-changelogs.mjs
index 25ceb8541..61e658ed0 100644
--- a/scripts/remove-prerelease-changelogs.mjs
+++ b/scripts/remove-prerelease-changelogs.mjs
@@ -11,9 +11,16 @@ import { remove } from "unist-util-remove";
 const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
 const rootDir = path.join(__dirname, "..");
 
-removePreReleaseChangelogs().then(() =>
-  console.log("✅ Removed pre-release changelogs")
-);
+main();
+
+async function main() {
+  if (isPrereleaseMode()) {
+    console.log("🚫 Skipping changelog removal in prerelease mode");
+    return;
+  }
+  await removePreReleaseChangelogs();
+  console.log("✅ Removed pre-release changelogs");
+}
 
 async function removePreReleaseChangelogs() {
   let allPackages = getPackagesSync(rootDir).packages;
@@ -105,3 +112,12 @@ function removePreReleaseSectionFromMarkdown() {
 function isPrereleaseVersion(str) {
   return /^(v?\d+\.){2}\d+-[a-z]+\.\d+$/i.test(str.trim());
 }
+
+function isPrereleaseMode() {
+  try {
+    let prereleaseFilePath = path.join(rootDir, ".changeset", "pre.json");
+    return fs.existsSync(prereleaseFilePath);
+  } catch (err) {
+    return false;
+  }
+}
diff --git a/templates/arc/app/entry.client.tsx b/templates/arc/app/entry.client.tsx
index f1fed31af..8338545d1 100644
--- a/templates/arc/app/entry.client.tsx
+++ b/templates/arc/app/entry.client.tsx
@@ -13,10 +13,10 @@ function hydrate() {
   });
 }
 
-if (window.requestIdleCallback) {
-  window.requestIdleCallback(hydrate);
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
 } else {
   // Safari doesn't support requestIdleCallback
   // https://caniuse.com/requestidlecallback
-  window.setTimeout(hydrate, 1);
+  setTimeout(hydrate, 1);
 }
diff --git a/templates/cloudflare-pages/app/entry.client.tsx b/templates/cloudflare-pages/app/entry.client.tsx
index 3eec1fd0a..8338545d1 100644
--- a/templates/cloudflare-pages/app/entry.client.tsx
+++ b/templates/cloudflare-pages/app/entry.client.tsx
@@ -1,4 +1,22 @@
 import { RemixBrowser } from "@remix-run/react";
-import { hydrate } from "react-dom";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
 
-hydrate(<RemixBrowser />, document);
+function hydrate() {
+  startTransition(() => {
+    hydrateRoot(
+      document,
+      <StrictMode>
+        <RemixBrowser />
+      </StrictMode>
+    );
+  });
+}
+
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
+} else {
+  // Safari doesn't support requestIdleCallback
+  // https://caniuse.com/requestidlecallback
+  setTimeout(hydrate, 1);
+}
diff --git a/templates/cloudflare-pages/package.json b/templates/cloudflare-pages/package.json
index 5a312d6dc..c0a83e519 100644
--- a/templates/cloudflare-pages/package.json
+++ b/templates/cloudflare-pages/package.json
@@ -4,25 +4,26 @@
   "scripts": {
     "build": "remix build",
     "dev:remix": "remix watch",
-    "dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public",
+    "dev:wrangler": "cross-env NODE_ENV=development npm run wrangler",
     "dev": "remix build && run-p \"dev:*\"",
-    "start": "cross-env NODE_ENV=production npm run dev:wrangler",
-    "typecheck": "tsc -b"
+    "start": "cross-env NODE_ENV=production npm run wrangler",
+    "typecheck": "tsc -b",
+    "wrangler": "wrangler pages dev ./public"
   },
   "dependencies": {
     "@remix-run/cloudflare": "*",
     "@remix-run/cloudflare-pages": "*",
     "@remix-run/react": "*",
     "cross-env": "^7.0.3",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2"
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^3.18.0",
     "@remix-run/dev": "*",
     "@remix-run/eslint-config": "*",
-    "@types/react": "^17.0.52",
-    "@types/react-dom": "^17.0.18",
+    "@types/react": "^18.0.25",
+    "@types/react-dom": "^18.0.8",
     "eslint": "^8.27.0",
     "npm-run-all": "^4.1.5",
     "typescript": "^4.8.4",
diff --git a/templates/cloudflare-workers/app/entry.client.tsx b/templates/cloudflare-workers/app/entry.client.tsx
index 3eec1fd0a..8338545d1 100644
--- a/templates/cloudflare-workers/app/entry.client.tsx
+++ b/templates/cloudflare-workers/app/entry.client.tsx
@@ -1,4 +1,22 @@
 import { RemixBrowser } from "@remix-run/react";
-import { hydrate } from "react-dom";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
 
-hydrate(<RemixBrowser />, document);
+function hydrate() {
+  startTransition(() => {
+    hydrateRoot(
+      document,
+      <StrictMode>
+        <RemixBrowser />
+      </StrictMode>
+    );
+  });
+}
+
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
+} else {
+  // Safari doesn't support requestIdleCallback
+  // https://caniuse.com/requestidlecallback
+  setTimeout(hydrate, 1);
+}
diff --git a/templates/cloudflare-workers/package.json b/templates/cloudflare-workers/package.json
index eb28f0235..f9264be25 100644
--- a/templates/cloudflare-workers/package.json
+++ b/templates/cloudflare-workers/package.json
@@ -15,15 +15,15 @@
     "@remix-run/cloudflare-workers": "*",
     "@remix-run/react": "*",
     "cross-env": "^7.0.3",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2"
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^3.18.0",
     "@remix-run/dev": "*",
     "@remix-run/eslint-config": "*",
-    "@types/react": "^17.0.52",
-    "@types/react-dom": "^17.0.18",
+    "@types/react": "^18.0.25",
+    "@types/react-dom": "^18.0.8",
     "eslint": "^8.27.0",
     "miniflare": "^2.11.0",
     "npm-run-all": "^4.1.5",
diff --git a/templates/deno/app/entry.client.tsx b/templates/deno/app/entry.client.tsx
index fa091046f..3ffb6c2eb 100644
--- a/templates/deno/app/entry.client.tsx
+++ b/templates/deno/app/entry.client.tsx
@@ -1,5 +1,22 @@
 import { RemixBrowser } from "@remix-run/react";
-import * as React from "react";
-import { hydrate } from "react-dom";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
 
-hydrate(<RemixBrowser />, document);
+function hydrate() {
+  startTransition(() => {
+    hydrateRoot(
+      document,
+      <StrictMode>
+        <RemixBrowser />
+      </StrictMode>,
+    );
+  });
+}
+
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
+} else {
+  // Safari doesn't support requestIdleCallback
+  // https://caniuse.com/requestidlecallback
+  setTimeout(hydrate, 1);
+}
diff --git a/templates/deno/package.json b/templates/deno/package.json
index ddbb4c54f..6fc3ec215 100644
--- a/templates/deno/package.json
+++ b/templates/deno/package.json
@@ -15,8 +15,8 @@
   "dependencies": {
     "@remix-run/deno": "*",
     "@remix-run/react": "*",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2"
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
   },
   "devDependencies": {
     "@remix-run/dev": "*",
diff --git a/templates/express/app/entry.client.tsx b/templates/express/app/entry.client.tsx
index f1fed31af..8338545d1 100644
--- a/templates/express/app/entry.client.tsx
+++ b/templates/express/app/entry.client.tsx
@@ -13,10 +13,10 @@ function hydrate() {
   });
 }
 
-if (window.requestIdleCallback) {
-  window.requestIdleCallback(hydrate);
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
 } else {
   // Safari doesn't support requestIdleCallback
   // https://caniuse.com/requestidlecallback
-  window.setTimeout(hydrate, 1);
+  setTimeout(hydrate, 1);
 }
diff --git a/templates/fly/app/entry.client.tsx b/templates/fly/app/entry.client.tsx
index f1fed31af..8338545d1 100644
--- a/templates/fly/app/entry.client.tsx
+++ b/templates/fly/app/entry.client.tsx
@@ -13,10 +13,10 @@ function hydrate() {
   });
 }
 
-if (window.requestIdleCallback) {
-  window.requestIdleCallback(hydrate);
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
 } else {
   // Safari doesn't support requestIdleCallback
   // https://caniuse.com/requestidlecallback
-  window.setTimeout(hydrate, 1);
+  setTimeout(hydrate, 1);
 }
diff --git a/templates/netlify/app/entry.client.tsx b/templates/netlify/app/entry.client.tsx
index f1fed31af..8338545d1 100644
--- a/templates/netlify/app/entry.client.tsx
+++ b/templates/netlify/app/entry.client.tsx
@@ -13,10 +13,10 @@ function hydrate() {
   });
 }
 
-if (window.requestIdleCallback) {
-  window.requestIdleCallback(hydrate);
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
 } else {
   // Safari doesn't support requestIdleCallback
   // https://caniuse.com/requestidlecallback
-  window.setTimeout(hydrate, 1);
+  setTimeout(hydrate, 1);
 }
diff --git a/templates/remix/app/entry.client.tsx b/templates/remix/app/entry.client.tsx
index f1fed31af..8338545d1 100644
--- a/templates/remix/app/entry.client.tsx
+++ b/templates/remix/app/entry.client.tsx
@@ -13,10 +13,10 @@ function hydrate() {
   });
 }
 
-if (window.requestIdleCallback) {
-  window.requestIdleCallback(hydrate);
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
 } else {
   // Safari doesn't support requestIdleCallback
   // https://caniuse.com/requestidlecallback
-  window.setTimeout(hydrate, 1);
+  setTimeout(hydrate, 1);
 }
diff --git a/templates/vercel/app/entry.client.tsx b/templates/vercel/app/entry.client.tsx
index f1fed31af..8338545d1 100644
--- a/templates/vercel/app/entry.client.tsx
+++ b/templates/vercel/app/entry.client.tsx
@@ -13,10 +13,10 @@ function hydrate() {
   });
 }
 
-if (window.requestIdleCallback) {
-  window.requestIdleCallback(hydrate);
+if (typeof requestIdleCallback === "function") {
+  requestIdleCallback(hydrate);
 } else {
   // Safari doesn't support requestIdleCallback
   // https://caniuse.com/requestidlecallback
-  window.setTimeout(hydrate, 1);
+  setTimeout(hydrate, 1);
 }


Test patch:
diff --git a/integration/error-boundary-v2-test.ts b/integration/error-boundary-v2-test.ts
new file mode 100644
index 000000000..bbacdfa33
--- /dev/null
+++ b/integration/error-boundary-v2-test.ts
@@ -0,0 +1,237 @@
+import type { Page } from "@playwright/test";
+import { test, expect } from "@playwright/test";
+import { ServerMode } from "@remix-run/server-runtime/mode";
+
+import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
+import type { Fixture, AppFixture } from "./helpers/create-fixture";
+import { PlaywrightFixture } from "./helpers/playwright-fixture";
+
+test.describe("V2 Singular ErrorBoundary (future.v2_errorBoundary)", () => {
+  let fixture: Fixture;
+  let appFixture: AppFixture;
+  let oldConsoleError: () => void;
+
+  test.beforeAll(async () => {
+    fixture = await createFixture({
+      future: {
+        v2_errorBoundary: true,
+      },
+      files: {
+        "app/root.jsx": js`
+          import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
+
+          export default function Root() {
+            return (
+              <html lang="en">
+                <head>
+                  <Meta />
+                  <Links />
+                </head>
+                <body>
+                  <main>
+                    <Outlet />
+                  </main>
+                  <Scripts />
+                </body>
+              </html>
+            );
+          }
+        `,
+
+        "app/routes/parent.jsx": js`
+          import {
+            Link,
+            Outlet,
+            isRouteErrorResponse,
+            useLoaderData,
+            useRouteError,
+          } from "@remix-run/react";
+
+          export function loader() {
+            return "PARENT LOADER";
+          }
+
+          export default function Component() {
+            return (
+              <div>
+                <nav>
+                  <ul>
+                    <li><Link to="/parent/child-with-boundary">Link</Link></li>
+                    <li><Link to="/parent/child-with-boundary?type=error">Link</Link></li>
+                    <li><Link to="/parent/child-with-boundary?type=response">Link</Link></li>
+                    <li><Link to="/parent/child-with-boundary?type=render">Link</Link></li>
+                    <li><Link to="/parent/child-without-boundary?type=error">Link</Link></li>
+                    <li><Link to="/parent/child-without-boundary?type=response">Link</Link></li>
+                    <li><Link to="/parent/child-without-boundary?type=render">Link</Link></li>
+                  </ul>
+                </nav>
+                <p id="parent-data">{useLoaderData()}</p>
+                <Outlet />
+              </div>
+            )
+          }
+
+          export function ErrorBoundary() {
+            let error = useRouteError();
+            return isRouteErrorResponse(error) ?
+              <p id="parent-error-response">{error.status + ' ' + error.data}</p> :
+              <p id="parent-error">{error.message}</p>;
+          }
+        `,
+
+        "app/routes/parent/child-with-boundary.jsx": js`
+          import {
+            isRouteErrorResponse,
+            useLoaderData,
+            useLocation,
+            useRouteError,
+          } from "@remix-run/react";
+
+          export function loader({ request }) {
+            let errorType = new URL(request.url).searchParams.get('type');
+            if (errorType === 'response') {
+              throw new Response('Loader Response', { status: 418 });
+            } else if (errorType === 'error') {
+              throw new Error('Loader Error');
+            }
+            return "CHILD LOADER";
+          }
+
+          export default function Component() {;
+            let data = useLoaderData();
+            if (new URLSearchParams(useLocation().search).get('type') === "render") {
+              throw new Error("Render Error");
+            }
+            return <p id="child-data">{data}</p>;
+          }
+
+          export function ErrorBoundary() {
+            let error = useRouteError();
+            return isRouteErrorResponse(error) ?
+              <p id="child-error-response">{error.status + ' ' + error.data}</p> :
+              <p id="child-error">{error.message}</p>;
+          }
+        `,
+
+        "app/routes/parent/child-without-boundary.jsx": js`
+          import { useLoaderData, useLocation } from "@remix-run/react";
+
+          export function loader({ request }) {
+            let errorType = new URL(request.url).searchParams.get('type');
+            if (errorType === 'response') {
+              throw new Response('Loader Response', { status: 418 });
+            } else if (errorType === 'error') {
+              throw new Error('Loader Error');
+            }
+            return "CHILD LOADER";
+          }
+
+          export default function Component() {;
+            let data = useLoaderData();
+            if (new URLSearchParams(useLocation().search).get('type') === "render") {
+              throw new Error("Render Error");
+            }
+            return <p id="child-data">{data}</p>;
+          }
+        `,
+      },
+    });
+
+    appFixture = await createAppFixture(fixture, ServerMode.Development);
+  });
+
+  test.afterAll(() => {
+    appFixture.close();
+  });
+
+  test.beforeEach(({ page }) => {
+    oldConsoleError = console.error;
+    console.error = () => {};
+  });
+
+  test.afterEach(() => {
+    console.error = oldConsoleError;
+  });
+
+  test.describe("without JavaScript", () => {
+    test.use({ javaScriptEnabled: false });
+    runBoundaryTests();
+  });
+
+  test.describe("with JavaScript", () => {
+    test.use({ javaScriptEnabled: true });
+    runBoundaryTests();
+  });
+
+  function runBoundaryTests() {
+    // Shorthand util to wait for an element to appear before asserting it
+    async function waitForAndAssert(
+      page: Page,
+      app: PlaywrightFixture,
+      selector: string,
+      match: string
+    ) {
+      await page.waitForSelector(selector);
+      expect(await app.getHtml(selector)).toMatch(match);
+    }
+
+    test("No errors", async ({ page }) => {
+      let app = new PlaywrightFixture(appFixture, page);
+      await app.goto("/parent");
+      await app.clickLink("/parent/child-with-boundary");
+      await waitForAndAssert(page, app, "#child-data", "CHILD LOADER");
+    });
+
+    test("Throwing a Response to own boundary", async ({ page }) => {
+      let app = new PlaywrightFixture(appFixture, page);
+      await app.goto("/parent");
+      await app.clickLink("/parent/child-with-boundary?type=response");
+      await waitForAndAssert(
+        page,
+        app,
+        "#child-error-response",
+        "418 Loader Response"
+      );
+    });
+
+    test("Throwing an Error to own boundary", async ({ page }) => {
+      let app = new PlaywrightFixture(appFixture, page);
+      await app.goto("/parent");
+      await app.clickLink("/parent/child-with-boundary?type=error");
+      await waitForAndAssert(page, app, "#child-error", "Loader Error");
+    });
+
+    test("Throwing a render error to own boundary", async ({ page }) => {
+      let app = new PlaywrightFixture(appFixture, page);
+      await app.goto("/parent");
+      await app.clickLink("/parent/child-with-boundary?type=render");
+      await waitForAndAssert(page, app, "#child-error", "Render Error");
+    });
+
+    test("Throwing a Response to parent boundary", async ({ page }) => {
+      let app = new PlaywrightFixture(appFixture, page);
+      await app.goto("/parent");
+      await app.clickLink("/parent/child-without-boundary?type=response");
+      await waitForAndAssert(
+        page,
+        app,
+        "#parent-error-response",
+        "418 Loader Response"
+      );
+    });
+
+    test("Throwing an Error to parent boundary", async ({ page }) => {
+      let app = new PlaywrightFixture(appFixture, page);
+      await app.goto("/parent");
+      await app.clickLink("/parent/child-without-boundary?type=error");
+      await waitForAndAssert(page, app, "#parent-error", "Loader Error");
+    });
+
+    test("Throwing a render error to parent boundary", async ({ page }) => {
+      let app = new PlaywrightFixture(appFixture, page);
+      await app.goto("/parent");
+      await app.clickLink("/parent/child-without-boundary?type=render");
+      await waitForAndAssert(page, app, "#parent-error", "Render Error");
+    });
+  }
+});
diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts
index a8e9a9520..ed4382983 100644
--- a/integration/fetcher-test.ts
+++ b/integration/fetcher-test.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test";
+import { expect, test } from "@playwright/test";
 
 import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
 import type { Fixture, AppFixture } from "./helpers/create-fixture";
@@ -143,6 +143,57 @@ test.describe("useFetcher", () => {
             );
           }
         `,
+
+        "app/routes/fetcher-echo.jsx": js`
+        import { json } from "@remix-run/node";
+        import { useFetcher } from "@remix-run/react";
+
+          export async function action({ request }) {
+            await new Promise(r => setTimeout(r, 1000));
+            let value = (await request.formData()).get('value');
+            return json({ data: "ACTION " + value })
+          }
+
+          export async function loader({ request }) {
+            await new Promise(r => setTimeout(r, 1000));
+            let value = new URL(request.url).searchParams.get('value');
+            return json({ data: "LOADER " + value })
+          }
+
+          export default function Index() {
+            let fetcherValues = [];
+            if (typeof window !== 'undefined') {
+              if (!window.fetcherValues) {
+                window.fetcherValues = [];
+              }
+              fetcherValues = window.fetcherValues
+            }
+
+            let fetcher = useFetcher();
+
+            let currentValue = fetcher.state + '/' + fetcher.data?.data;
+            if (fetcherValues[fetcherValues.length - 1] !== currentValue) {
+              fetcherValues.push(currentValue)
+            }
+
+            return (
+              <>
+                <input id="fetcher-input" name="value" />
+                <button id="fetcher-load" onClick={() => {
+                  let value = document.getElementById('fetcher-input').value;
+                  fetcher.load('/fetcher-echo?value=' + value)
+                }}>Load</button>
+                <button id="fetcher-submit" onClick={() => {
+                  let value = document.getElementById('fetcher-input').value;
+                  fetcher.submit({ value }, { method: 'post', action: '/fetcher-echo' })
+                }}>Submit</button>
+
+                {fetcher.state === 'idle' ? <p id="fetcher-idle">IDLE</p> : null}
+                <pre>{JSON.stringify(fetcherValues)}</pre>
+              </>
+            );
+          }
+        `,
       },
     });
 
@@ -232,4 +283,71 @@ test.describe("useFetcher", () => {
     await app.clickElement("#submit-index-post");
     await page.waitForSelector(`pre:has-text("${PARENT_INDEX_ACTION}")`);
   });
+
+  test("fetcher.load persists data through reloads", async ({ page }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+
+    await app.goto("/fetcher-echo", true);
+    expect(await app.getHtml("pre")).toMatch(
+      JSON.stringify(["idle/undefined"])
+    );
+
+    await page.fill("#fetcher-input", "1");
+    await app.clickElement("#fetcher-load");
+    await page.waitForSelector("#fetcher-idle");
+    expect(await app.getHtml("pre")).toMatch(
+      JSON.stringify(["idle/undefined", "loading/undefined", "idle/LOADER 1"])
+    );
+
+    await page.fill("#fetcher-input", "2");
+    await app.clickElement("#fetcher-load");
+    await page.waitForSelector("#fetcher-idle");
+    expect(await app.getHtml("pre")).toMatch(
+      JSON.stringify([
+        "idle/undefined",
+        "loading/undefined",
+        "idle/LOADER 1",
+        "loading/LOADER 1", // Preserves old data during reload
+        "idle/LOADER 2",
+      ])
+    );
+  });
+
+  test("fetcher.submit persists data through resubmissions", async ({
+    page,
+  }) => {
+    let app = new PlaywrightFixture(appFixture, page);
+
+    await app.goto("/fetcher-echo", true);
+    expect(await app.getHtml("pre")).toMatch(
+      JSON.stringify(["idle/undefined"])
+    );
+
+    await page.fill("#fetcher-input", "1");
+    await app.clickElement("#fetcher-submit");
+    await page.waitForSelector("#fetcher-idle");
+    expect(await app.getHtml("pre")).toMatch(
+      JSON.stringify([
+        "idle/undefined",
+        "submitting/undefined",
+        "loading/ACTION 1",
+        "idle/ACTION 1",
+      ])
+    );
+
+    await page.fill("#fetcher-input", "2");
+    await app.clickElement("#fetcher-submit");
+    await page.waitForSelector("#fetcher-idle");
+    expect(await app.getHtml("pre")).toMatch(
+      JSON.stringify([
+        "idle/undefined",
+        "submitting/undefined",
+        "loading/ACTION 1",
+        "idle/ACTION 1",
+        "submitting/ACTION 1", // Preserves old data during resubmissions
+        "loading/ACTION 2",
+        "idle/ACTION 2",
+      ])
+    );
+  });
 });
diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts
index a88bb37f5..38cbb33b3 100644
--- a/packages/remix-dev/__tests__/readConfig-test.ts
+++ b/packages/remix-dev/__tests__/readConfig-test.ts
@@ -26,6 +26,7 @@ describe("readConfig", () => {
           unstable_cssModules: expect.any(Boolean),
           unstable_cssSideEffectImports: expect.any(Boolean),
           unstable_vanillaExtract: expect.any(Boolean),
+          v2_errorBoundary: expect.any(Boolean),
           v2_meta: expect.any(Boolean),
           v2_routeConvention: expect.any(Boolean),
         },
@@ -43,6 +44,7 @@ describe("readConfig", () => {
           "unstable_cssModules": Any<Boolean>,
           "unstable_cssSideEffectImports": Any<Boolean>,
           "unstable_vanillaExtract": Any<Boolean>,
+          "v2_errorBoundary": Any<Boolean>,
           "v2_meta": Any<Boolean>,
           "v2_routeConvention": Any<Boolean>,
         },
diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts
index fca71a1f9..5b64f3c71 100644
--- a/packages/remix-server-runtime/__tests__/data-test.ts
+++ b/packages/remix-server-runtime/__tests__/data-test.ts
@@ -21,9 +21,10 @@ describe("loaders", () => {
         },
       },
       entry: { module: {} },
+      future: {},
     } as unknown as ServerBuild;
 
-    let handler = createRequestHandler(build, {});
+    let handler = createRequestHandler(build);
 
     let request = new Request(
       "http://example.com/random?_data=routes/random&foo=bar",
@@ -59,9 +60,10 @@ describe("loaders", () => {
         },
       },
       entry: { module: {} },
+      future: {},
     } as unknown as ServerBuild;
 
-    let handler = createRequestHandler(build, {});
+    let handler = createRequestHandler(build);
 
     let request = new Request(
       "http://example.com/random?_data=routes/random&foo=bar",
@@ -93,9 +95,10 @@ describe("loaders", () => {
         },
       },
       entry: { module: {} },
+      future: {},
     } as unknown as ServerBuild;
 
-    let handler = createRequestHandler(build, {});
+    let handler = createRequestHandler(build);
 
     let request = new Request(
       "http://example.com/random?_data=routes/random&index&foo=bar",
@@ -127,9 +130,10 @@ describe("loaders", () => {
         },
       },
       entry: { module: {} },
+      future: {},
     } as unknown as ServerBuild;
 
-    let handler = createRequestHandler(build, {});
+    let handler = createRequestHandler(build);
 
     let request = new Request(
       "http://example.com/random?_data=routes/random&index&foo=bar&index=test",
diff --git a/packages/remix-server-runtime/__tests__/handler-test.ts b/packages/remix-server-runtime/__tests__/handler-test.ts
index bb676a8e9..8b0e05688 100644
--- a/packages/remix-server-runtime/__tests__/handler-test.ts
+++ b/packages/remix-server-runtime/__tests__/handler-test.ts
@@ -16,6 +16,10 @@ describe("createRequestHandler", () => {
       },
       assets: {} as any,
       entry: { module: {} as any },
+      future: {
+        v2_errorBoundary: false,
+        v2_meta: false,
+      },
     });
 
     let response = await handler(
diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts
index 6834bd4b4..e4d195172 100644
--- a/packages/remix-server-runtime/__tests__/server-test.ts
+++ b/packages/remix-server-runtime/__tests__/server-test.ts
@@ -55,6 +55,7 @@ describe("server", () => {
         },
       },
     },
+    future: {},
   } as unknown as ServerBuild;
 
   describe("createRequestHandler", () => {
diff --git a/packages/remix-server-runtime/__tests__/sessions-test.ts b/packages/remix-server-runtime/__tests__/sessions-test.ts
index 98f62e7de..5aca3ea48 100644
--- a/packages/remix-server-runtime/__tests__/sessions-test.ts
+++ b/packages/remix-server-runtime/__tests__/sessions-test.ts
@@ -161,7 +161,7 @@ describe("Cookie session storage", () => {
 
       expect(spy.console).toHaveBeenCalledTimes(1);
       expect(spy.console).toHaveBeenCalledWith(
-        'The "__session" cookie is not signed, but session cookies should be signed to prevent tampering on the client before they are sent back to the server. See https://remix.run/api/remix#signing-cookies for more information.'
+        'The "__session" cookie is not signed, but session cookies should be signed to prevent tampering on the client before they are sent back to the server. See https://remix.run/utils/cookies#signing-cookies for more information.'
       );
     });
   });
diff --git a/packages/remix-server-runtime/__tests__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts
index 9a75e5ce5..21223d7d5 100644
--- a/packages/remix-server-runtime/__tests__/utils.ts
+++ b/packages/remix-server-runtime/__tests__/utils.ts
@@ -80,6 +80,7 @@ export function mockServerBuild(
       },
       {}
     ),
+    future: {},
   };
 }
 
diff --git a/packages/remix-testing/CHANGELOG.md b/packages/remix-testing/CHANGELOG.md
index 85a893e63..e87d13e65 100644
--- a/packages/remix-testing/CHANGELOG.md
+++ b/packages/remix-testing/CHANGELOG.md
@@ -1,5 +1,13 @@
 # `@remix-run/testing`
 
+## 1.10.1
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/react@1.10.1`
+  - `@remix-run/node@1.10.1`
+
 ## 1.10.0
 
 ### Patch Changes
diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx
index fef6ea302..e84ec5bf2 100644
--- a/packages/remix-testing/create-remix-stub.tsx
+++ b/packages/remix-testing/create-remix-stub.tsx
@@ -68,10 +68,11 @@ export function createRemixStub(routes: AgnosticDataRouteObject[]) {
     if (remixContextRef.current == null) {
       remixContextRef.current = {
         future: {
-          v2_meta: false,
           unstable_cssModules: false,
           unstable_cssSideEffectImports: false,
           unstable_vanillaExtract: false,
+          v2_errorBoundary: false,
+          v2_meta: false,
           ...remixConfigFuture,
         },
         manifest: createManifest(routes),
diff --git a/packages/remix-testing/package.json b/packages/remix-testing/package.json
index a2ee9f43c..d0a0bd8be 100644
--- a/packages/remix-testing/package.json
+++ b/packages/remix-testing/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/testing",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "Testing utilities for Remix apps",
   "homepage": "https://remix.run",
   "bugs": {
@@ -16,8 +16,8 @@
   "typings": "./dist/index.d.ts",
   "module": "./dist/esm/index.js",
   "dependencies": {
-    "@remix-run/node": "1.10.0",
-    "@remix-run/react": "1.10.0",
+    "@remix-run/node": "1.10.1",
+    "@remix-run/react": "1.10.1",
     "@remix-run/router": "1.2.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",