diff --git a/.changeset/three-cheetahs-lick.md b/.changeset/three-cheetahs-lick.md new file mode 100644 index 00000000000..3a6d06828e3 --- /dev/null +++ b/.changeset/three-cheetahs-lick.md @@ -0,0 +1,7 @@ +--- +"@remix-run/dev": patch +"@remix-run/react": patch +"@remix-run/server-runtime": patch +--- + +Deprecate `CatchBoundary` in favor of `future.v2_errorBoundary` diff --git a/docs/route/catch-boundary.md b/docs/route/catch-boundary.md index bd5985a8d58..556fdc64d9b 100644 --- a/docs/route/catch-boundary.md +++ b/docs/route/catch-boundary.md @@ -4,6 +4,8 @@ title: CatchBoundary # `CatchBoundary` +The separation of `CatchBoundary` and `ErrorBoundary` has been deprecated and Remix v2 will use a singular `ErrorBoundary` for all thrown Responses and Errors. It is recommended that you opt-into the new behavior in Remix v1 via the `future.v2_errorBoundary` flag in your `remix.config.js` file. Please refer to the [ErrorBoundary (v2)][error-boundary-v2] docs for more information. + A `CatchBoundary` is a React component that renders whenever an action or loader throws a `Response`. **Note:** We use the word "catch" to represent the codepath taken when a `Response` type is thrown; you thought about bailing from the "happy path". This is different from an uncaught error you did not expect to occur. @@ -29,3 +31,5 @@ export function CatchBoundary() { ); } ``` + +[error-boundary-v2]: ./error-boundary-v2 diff --git a/docs/route/error-boundary-v2.md b/docs/route/error-boundary-v2.md new file mode 100644 index 00000000000..c1a03390587 --- /dev/null +++ b/docs/route/error-boundary-v2.md @@ -0,0 +1,11 @@ +--- +title: ErrorBoundary (v2) +--- + +# `ErrorBoundary (v2)` + +You can opt into the Remix v2 `ErrorBoundary` behavior via the `future.v2_errorBoundary` flag in your `remix.config.js` + +If you export an `ErrorBoundary` component from your route module, it will be used as the React Router [`errorElement`][rr-error-element] and will render if you throw from a loader/action or if React throws during rendering your Route component. + +[rr-error-element]: https://reactrouter.com/route/error-element diff --git a/docs/route/error-boundary.md b/docs/route/error-boundary.md index 8266833e628..5b8e9118a0d 100644 --- a/docs/route/error-boundary.md +++ b/docs/route/error-boundary.md @@ -4,6 +4,8 @@ title: ErrorBoundary # `ErrorBoundary` +The separation of `CatchBoundary` and `ErrorBoundary` has been deprecated and Remix v2 will use a singular `ErrorBoundary` for all thrown Responses and Errors. It is recommended that you opt-into the new behavior in Remix v1 via the `future.v2_errorBoundary` flag in your `remix.config.js` file. Please refer to the [ErrorBoundary (v2)][error-boundary-v2] docs for more information. + An `ErrorBoundary` is a React component that renders whenever there is an error anywhere on the route, either during rendering or during data loading. **Note:** We use the word "error" to mean an uncaught exception; something you didn't anticipate happening. This is different from other types of "errors" that you are able to recover from easily, for example a 404 error where you can still show something in the user interface to indicate you weren't able to find some data. @@ -26,3 +28,4 @@ export function ErrorBoundary({ error }) { ``` [error-boundaries]: https://reactjs.org/docs/error-boundaries.html +[error-boundary-v2]: ./error-boundary-v2 diff --git a/integration/action-test.ts b/integration/action-test.ts index f8b25e21768..dee315b2a55 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -17,7 +17,10 @@ test.describe("actions", () => { test.beforeAll(async () => { fixture = await createFixture({ - future: { v2_routeConvention: true }, + future: { + v2_routeConvention: true, + v2_errorBoundary: true, + }, files: { "app/routes/urlencoded.jsx": js` import { Form, useActionData } from "@remix-run/react"; diff --git a/integration/defer-test.ts b/integration/defer-test.ts index df06225a6ca..edac58789e1 100644 --- a/integration/defer-test.ts +++ b/integration/defer-test.ts @@ -33,11 +33,10 @@ test.describe("non-aborted", () => { test.beforeAll(async () => { fixture = await createFixture({ - future: { v2_routeConvention: true }, - //////////////////////////////////////////////////////////////////////////// - // 💿 Next, add files to this object, just like files in a real app, - // `createFixture` will make an app and run your tests against it. - //////////////////////////////////////////////////////////////////////////// + future: { + v2_routeConvention: true, + v2_errorBoundary: true, + }, files: { "app/components/counter.tsx": js` import { useState } from "react"; diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts index ba8a5ed1d1e..13af69b0261 100644 --- a/integration/hmr-test.ts +++ b/integration/hmr-test.ts @@ -15,6 +15,7 @@ let fixture = (options: { port: number; appServerPort: number }) => ({ }, unstable_tailwind: true, v2_routeConvention: true, + v2_errorBoundary: true, }, files: { "package.json": json({ diff --git a/packages/remix-dev/__tests__/create-test.ts b/packages/remix-dev/__tests__/create-test.ts index 4508ac91907..2f9eb196b83 100644 --- a/packages/remix-dev/__tests__/create-test.ts +++ b/packages/remix-dev/__tests__/create-test.ts @@ -8,7 +8,7 @@ import stripAnsi from "strip-ansi"; import { run } from "../cli/run"; import { server } from "./msw"; -import { flatRoutesWarning } from "../config"; +import { errorBoundaryWarning, flatRoutesWarning } from "../config"; beforeAll(() => server.listen({ onUnhandledRequest: "error" })); afterAll(() => server.close()); @@ -348,7 +348,9 @@ describe("the create command", () => { "--no-typescript", ]); expect(output.trim()).toBe( - flatRoutesWarning + + errorBoundaryWarning + + "\n" + + flatRoutesWarning + "\n\n" + getOptOutOfInstallMessage() + "\n\n" + diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 8686777438a..6770b8e66fd 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -397,6 +397,10 @@ export async function readConfig( } } + if (!appConfig.future?.v2_errorBoundary) { + warnOnce(errorBoundaryWarning, "v2_errorBoundary"); + } + if (appConfig.serverBuildTarget) { warnOnce(serverBuildTargetWarning, "v2_serverBuildTarget"); } @@ -734,3 +738,11 @@ let listFormat = new Intl.ListFormat("en", { export let serverBuildTargetWarning = `⚠️ DEPRECATED: The "serverBuildTarget" config option is deprecated. Use a combination of "publicPath", "serverBuildPath", "serverConditions", "serverDependenciesToBundle", "serverMainFields", "serverMinify", "serverModuleFormat" and/or "serverPlatform" instead.`; export let flatRoutesWarning = `⚠️ DEPRECATED: The old nested folders route convention has been deprecated in favor of "flat routes". Please enable the new routing convention via the \`future.v2_routeConvention\` flag in your \`remix.config.js\` file. For more information, please see https://remix.run/docs/en/main/file-conventions/route-files-v2.`; + +export const errorBoundaryWarning = + "⚠️ DEPRECATED: The separation of `CatchBoundary` and `ErrorBoundary` has " + + "been deprecated and Remix v2 will use a singular `ErrorBoundary` for " + + "all thrown values (`Response` and `Error`). Please migrate to the new " + + "behavior in Remix v1 via the `future.v2_errorBoundary` flag in your " + + "`remix.config.js` file. For more information, see " + + "https://remix.run/docs/route/error-boundary-v2"; diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index fb3d1f153f9..70eeb7095d5 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -14,6 +14,7 @@ import { import { deserializeErrors } from "./errors"; import type { RouteModules } from "./routeModules"; import { createClientRoutes } from "./routes"; +import { warnOnce } from "./warnings"; /* eslint-disable prefer-let/prefer-let */ declare global { @@ -138,6 +139,18 @@ if (import.meta && import.meta.hot) { */ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { if (!router) { + if (!window.__remixContext.future.v2_errorBoundary) { + warnOnce( + false, + "⚠️ DEPRECATED: The separation of `CatchBoundary` and `ErrorBoundary` has " + + "been deprecated and Remix v2 will use a singular `ErrorBoundary` for " + + "all thrown values (`Response` and `Error`). Please migrate to the new " + + "behavior in Remix v1 via the `future.v2_errorBoundary` flag in your " + + "`remix.config.js` file. For more information, see " + + "https://remix.run/docs/route/error-boundary-v2" + ); + } + let routes = createClientRoutes( window.__remixManifest.routes, window.__remixRouteModules, diff --git a/packages/remix-react/errorBoundaries.tsx b/packages/remix-react/errorBoundaries.tsx index faa50dd4193..5e083af4d36 100644 --- a/packages/remix-react/errorBoundaries.tsx +++ b/packages/remix-react/errorBoundaries.tsx @@ -133,6 +133,8 @@ let RemixCatchContext = React.createContext( /** * Returns the status code and thrown response data. * + * @deprecated Please enable the v2_errorBoundary flag + * * @see https://remix.run/route/catch-boundary */ export function useCatch< diff --git a/packages/remix-react/routeModules.ts b/packages/remix-react/routeModules.ts index 785add903db..147d799bdff 100644 --- a/packages/remix-react/routeModules.ts +++ b/packages/remix-react/routeModules.ts @@ -32,6 +32,8 @@ export interface RouteModule { /** * A React component that is rendered when the server throws a Response. * + * @deprecated Please enable the v2_errorBoundary flag + * * @see https://remix.run/route/catch-boundary */ export type CatchBoundaryComponent = ComponentType<{}>; @@ -39,6 +41,8 @@ export type CatchBoundaryComponent = ComponentType<{}>; /** * A React component that is rendered when there is an error on a route. * + * @deprecated Please enable the v2_errorBoundary flag + * * @see https://remix.run/route/error-boundary */ export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index 380b2549d1f..25a7bb19820 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -36,14 +36,25 @@ export interface ActionFunction { /** * A React component that is rendered when the server throws a Response. + * + * @deprecated Please enable the v2_errorBoundary flag */ export type CatchBoundaryComponent = ComponentType; /** * A React component that is rendered when there is an error on a route. + * + * @deprecated Please enable the v2_errorBoundary flag */ 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 returns HTTP headers to be used for a route. These headers * will be merged with (and take precedence over) headers from parent routes. @@ -224,7 +235,7 @@ export type RouteHandle = any; export interface EntryRouteModule { CatchBoundary?: CatchBoundaryComponent; - ErrorBoundary?: ErrorBoundaryComponent; + ErrorBoundary?: ErrorBoundaryComponent | V2_ErrorBoundaryComponent; default: RouteComponent; handle?: RouteHandle; links?: LinksFunction;