diff --git a/.changeset/odd-numbers-film.md b/.changeset/odd-numbers-film.md
new file mode 100644
index 00000000000..1e129194f69
--- /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
{caught.status} {caught.data}
;
+}
+
+export function ErrorBoundary({ error }) {
+ return {error.message}
;
+}
+
+
+// Using future.v2_errorBoundary
+import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
+
+export function ErrorBoundary() {
+ let error = useRouteError();
+
+ return isRouteErrorResponse(error) ?
+ {error.status} {error.data}
:
+ {error.message}
;
+}
+```
\ No newline at end of file
diff --git a/integration/error-boundary-v2-test.ts b/integration/error-boundary-v2-test.ts
new file mode 100644
index 00000000000..bbacdfa3359
--- /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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "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 (
+
+
+
+ Link
+ Link
+ Link
+ Link
+ Link
+ Link
+ Link
+
+
+
{useLoaderData()}
+
+
+ )
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return isRouteErrorResponse(error) ?
+ {error.status + ' ' + error.data}
:
+ {error.message}
;
+ }
+ `,
+
+ "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 {data}
;
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return isRouteErrorResponse(error) ?
+ {error.status + ' ' + error.data}
:
+ {error.message}
;
+ }
+ `,
+
+ "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 {data}
;
+ }
+ `,
+ },
+ });
+
+ 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/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts
index 7cda8189132..4238d6a3b75 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;
}
export type Fixture = Awaited>;
@@ -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/remix.config.js b/integration/helpers/node-template/remix.config.js
index adf2a0b5d3e..b7f693265aa 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/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts
index a88bb37f599..38cbb33b315 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,
"unstable_cssSideEffectImports": Any,
"unstable_vanillaExtract": Any,
+ "v2_errorBoundary": Any,
"v2_meta": Any,
"v2_routeConvention": Any,
},
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index 1733a292604..143c9f6fee1 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-react/browser.tsx b/packages/remix-react/browser.tsx
index ce280c23ca2..38110fe4cb0 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 fadac712ac0..191f7579150 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
- // }
+ 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 ;
+ }
+ 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 (
-
- );
+ return ;
}
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 (
-
- );
+ return ;
}
throw error;
diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts
index 77f17b4988b..33bef61688f 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 f2f8916e860..7b1419dfd55 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 ;
+ } else if (error instanceof Error) {
+ return ;
+ } else {
+ let errorString =
+ error == null
+ ? "Unknown Error"
+ : typeof error === "object" && "toString" in error
+ ? error.toString()
+ : JSON.stringify(error);
+ return ;
+ }
+}
+
let RemixCatchContext = React.createContext(
undefined
);
@@ -150,6 +167,14 @@ export function RemixCatchBoundary({
*/
export function RemixRootDefaultCatchBoundary() {
let caught = useCatch();
+ return ;
+}
+
+function RemixRootDefaultCatchBoundaryImpl({
+ caught,
+}: {
+ caught: ThrownResponse;
+}) {
return (
diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx
index 5af30425e82..5bad944d4da 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/routeModules.ts b/packages/remix-react/routeModules.ts
index db5496ce288..454b33cc4c5 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;
@@ -43,6 +43,13 @@ export type CatchBoundaryComponent = ComponentType<{}>;
*/
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 ` ` tags to be inserted into the `` of
* the document on route transitions.
diff --git a/packages/remix-react/routes.tsx b/packages/remix-react/routes.tsx
index 15c237a7da3..0b342f808fa 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,
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: ,
@@ -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,
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/server.tsx b/packages/remix-react/server.tsx
index 61bc6816c47..1f378692480 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-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts
index fca71a1f9fe..5b64f3c7118 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 bb676a8e9d3..8b0e05688ef 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 6834bd4b4bd..e4d1951728d 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__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts
index 9a75e5ce5c2..21223d7d5d0 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-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts
index 152bf2ad13a..7dddbb38061 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/routes.ts b/packages/remix-server-runtime/routes.ts
index 19a48e589f4..6c63ae870b1 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 {
@@ -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 7c0142760cc..d62f8a19efd 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-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx
index fef6ea3021b..e84ec5bf288 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),