diff --git a/.changeset/eight-squids-greet.md b/.changeset/eight-squids-greet.md new file mode 100644 index 00000000000..6431db0debb --- /dev/null +++ b/.changeset/eight-squids-greet.md @@ -0,0 +1,6 @@ +--- +"@remix-run/react": major +"@remix-run/server-runtime": major +--- + +Remove `v2_errorBoundary` flag and `CatchBoundary` logic diff --git a/docs/api/conventions.md b/docs/api/conventions.md index 0d1f2931fea..3a7c29877be 100644 --- a/docs/api/conventions.md +++ b/docs/api/conventions.md @@ -178,10 +178,6 @@ title: Conventions [Moved →][moved-41] -### CatchBoundary - -[Moved →][moved-42] - ### ErrorBoundary [Moved →][moved-43] @@ -239,7 +235,6 @@ title: Conventions [moved-39]: ../route/links [moved-40]: ../route/links#htmllinkdescriptor [moved-41]: ../route/links#pagelinkdescriptor -[moved-42]: ../route/catch-boundary [moved-43]: ../route/error-boundary [moved-44]: ../route/handle [moved-45]: ../route/should-revalidate diff --git a/docs/guides/routing.md b/docs/guides/routing.md index 33fd58fb4ff..ce7cf607139 100644 --- a/docs/guides/routing.md +++ b/docs/guides/routing.md @@ -369,7 +369,7 @@ export async function loader({ params }: LoaderArgs) { You can add splats at any level of your route hierarchy. Any sibling routes will match first (like `/files/mine`). -It's common to add a `routes/$.jsx` file build custom 404 pages with data from a loader (without it, Remix renders your root `CatchBoundary` with no ability to load data for the page when the URL doesn't match any routes). +It's common to add a `routes/$.jsx` file build custom 404 pages with data from a loader (without it, Remix renders your root `ErrorBoundary` with no ability to load data for the page when the URL doesn't match any routes). ## Conclusion diff --git a/docs/pages/faq.md b/docs/pages/faq.md index 3bfefba36f3..174cbc10db2 100644 --- a/docs/pages/faq.md +++ b/docs/pages/faq.md @@ -220,11 +220,4 @@ Again, `formData.getAll()` is often all you need, we encourage you to give it a [form-data]: https://developer.mozilla.org/en-US/docs/Web/API/FormData [query-string]: https://www.npmjs.com/package/query-string [ramda]: https://www.npmjs.com/package/ramda - -## What's the difference between `CatchBoundary` & `ErrorBoundary`? - -Error boundaries render when your application throws an error and you had no clue it was going to happen. Most apps just go blank or have spinners spin forever. In remix the error boundary renders and you have granular control over it. - -Catch boundaries render when you decide in a loader that you can't proceed down the happy path to render the UI you want (auth required, record not found, etc.), so you throw a response and let some catch boundary up the tree handle it. - [watch-on-you-tube]: https://www.youtube.com/watch?v=w2i-9cYxSdc&ab_channel=Remix diff --git a/docs/route/catch-boundary.md b/docs/route/catch-boundary.md deleted file mode 100644 index 556fdc64d9b..00000000000 --- a/docs/route/catch-boundary.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -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. - -A Remix `CatchBoundary` component works just like a route component, but instead of `useLoaderData` you have access to `useCatch`. When a response is thrown in an action or loader, the `CatchBoundary` will be rendered in its place, nested inside parent routes. - -A `CatchBoundary` component has access to the status code and thrown response data through `useCatch`. - -```tsx -import { useCatch } from "@remix-run/react"; - -export function CatchBoundary() { - const caught = useCatch(); - - return ( -
-

Caught

-

Status: {caught.status}

-
-        {JSON.stringify(caught.data, null, 2)}
-      
-
- ); -} -``` - -[error-boundary-v2]: ./error-boundary-v2 diff --git a/docs/route/error-boundary-v2.md b/docs/route/error-boundary-v2.md deleted file mode 100644 index c1a03390587..00000000000 --- a/docs/route/error-boundary-v2.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -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 5b8e9118a0d..b1d6f3296f0 100644 --- a/docs/route/error-boundary.md +++ b/docs/route/error-boundary.md @@ -4,28 +4,52 @@ 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. +The Remix `ErrorBoundary` is an implementation of the React Router [`errorElement`/`ErrorBoundary`][rr-error-boundary]. -An `ErrorBoundary` is a React component that renders whenever there is an error anywhere on the route, either during rendering or during data loading. +A Remix `ErrorBoundary` component works just like normal React [error boundaries][error-boundaries], but with a few extra capabilities. When there is an error in your route component, the `ErrorBoundary` will be rendered in its place, nested inside any parent routes. `ErrorBoundary` components also render when there is an error in the `loader` or `action` functions for a route, so all errors for that route may be handled in one spot. -**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. +The most common use-cases tend to be: -A Remix `ErrorBoundary` component works just like normal React [error boundaries][error-boundaries], but with a few extra capabilities. When there is an error in your route component, the `ErrorBoundary` will be rendered in its place, nested inside any parent routes. `ErrorBoundary` components also render when there is an error in the `loader` or `action` functions for a route, so all errors for that route may be handled in one spot. +- You may intentionally throw a 4xx `Response` to trigger an error UI + - Throwing a 400 on bad user input + - Throwing a 401 for unauthorized access + - Throwing a 404 when you can't find requested data +- React may unintentionally throw an `Error` if it encounters a runtime error during rendering -An `ErrorBoundary` component receives one prop: the `error` that occurred. +To obtain the thrown object, you can use the [`useRouteError`][use-route-error] hook. When a `Response` is thrown, it will be automatically unwrapped into an `ErrorResponse` instance with `state`/`statusText`/`data` fields so that you don't need to bother with `await response.json()` in your component. To differentiate thrown `Response`'s from thrown `Error`'s' you can use the [`isRouteErrorResponse`][is-route-error-response] utility. ```tsx -export function ErrorBoundary({ error }) { - return ( -
-

Error

-

{error.message}

-

The stack trace is:

-
{error.stack}
-
- ); +import { isRouteErrorResponse } from "@remix-run/node"; +import { useRouteError } from "@remix-run/react"; + +export function ErrorBoundary() { + let error = useRouteError(); + + if (isRouteErrorResponse(error)) { + return ( +
+

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

+

{error.data}

+
+ ); + } else if (error instanceof Error) { + return ( +
+

Error

+

{error.message}

+

The stack trace is:

+
{error.stack}
+
+ ); + } else { + return

Unknown Error

; + } } ``` [error-boundaries]: https://reactjs.org/docs/error-boundaries.html -[error-boundary-v2]: ./error-boundary-v2 +[rr-error-boundary]: https://reactrouter.com/en/main/route/error-element +[use-route-error]: ../hooks/use-route-error +[is-route-error-response]: ../utils/is-route-error-response.md diff --git a/docs/utils/is-route-error-response.md b/docs/utils/is-route-error-response.md new file mode 100644 index 00000000000..03a913e8f32 --- /dev/null +++ b/docs/utils/is-route-error-response.md @@ -0,0 +1,33 @@ +--- +title: isRouteErrorResponse +--- + +# `isRouteErrorResponse` + +This is just a re-export of the React Router [`isRouteErrorResponse`][rr-is-route-error-response] utility. + +When a response is thrown from an action or loader, it will be unwrapped into an `ErrorResponse` so that your component doesn't have to deal with the complexity of unwrapping it (which would require React state and effects to deal with the promise returned from `res.json()`) + +```jsx +import { json } from "@remix-run/node"; + +export function action() { + throw json( + { message: "email is required" }, + { status: 400, statusText: "Bad Request" } + ); +} + +export function ErrorBoundary() { + const error = useRouteError(); + if (isRouteErrorResponse(error)) { + error.status; // 400 + error.statusText; // Bad Request + error.data; // { "message: "email is required" } + } +} +``` + +If the user visits a route that does not match any routes in the app, Remix itself will throw a 404 response. + +[rr-is-route-error-response]: https://reactrouter.com/en/main/utils/is-route-error-response diff --git a/integration/action-test.ts b/integration/action-test.ts index dee315b2a55..1d30db48a19 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -19,7 +19,6 @@ test.describe("actions", () => { fixture = await createFixture({ future: { v2_routeConvention: true, - v2_errorBoundary: true, }, files: { "app/routes/urlencoded.jsx": js` diff --git a/integration/catch-boundary-data-test.ts b/integration/catch-boundary-data-test.ts index becde4b6a42..f675f24d80b 100644 --- a/integration/catch-boundary-data-test.ts +++ b/integration/catch-boundary-data-test.ts @@ -26,222 +26,11 @@ let LAYOUT_DATA = "root data"; test.beforeAll(async () => { fixture = await createFixture({ - future: { v2_routeConvention: true }, + future: { + v2_routeConvention: true, + }, files: { "app/root.jsx": js` - import { json } from "@remix-run/node"; - import { - Links, - Meta, - Outlet, - Scripts, - useLoaderData, - useMatches, - } from "@remix-run/react"; - - export const loader = () => json("${ROOT_DATA}"); - - export default function Root() { - const data = useLoaderData(); - - return ( - - - - - - -
{data}
- - - - - ); - } - - export function CatchBoundary() { - let matches = useMatches(); - let { data } = matches.find(match => match.id === "root"); - - return ( - - - -
${ROOT_BOUNDARY_TEXT}
-
{data}
- - - - ); - } - `, - - "app/routes/_index.jsx": js` - import { Link } from "@remix-run/react"; - export default function Index() { - return ( -
- ${NO_BOUNDARY_LOADER} - ${HAS_BOUNDARY_LAYOUT_NESTED_LOADER} - ${HAS_BOUNDARY_NESTED_LOADER} -
- ); - } - `, - - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` - export function loader() { - throw new Response("", { status: 401 }); - } - export default function Index() { - return
; - } - `, - - [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}.jsx`]: js` - import { useMatches } from "@remix-run/react"; - export function loader() { - return "${LAYOUT_DATA}"; - } - export default function Layout() { - return
; - } - export function CatchBoundary() { - let matches = useMatches(); - let { data } = matches.find(match => match.id === "routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}"); - - return ( -
-
${LAYOUT_BOUNDARY_TEXT}
-
{data}
-
- ); - } - `, - - [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}._index.jsx`]: js` - export function loader() { - throw new Response("", { status: 401 }); - } - export default function Index() { - return
; - } - `, - - [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}.jsx`]: js` - import { Outlet, useLoaderData } from "@remix-run/react"; - export function loader() { - return "${LAYOUT_DATA}"; - } - export default function Layout() { - let data = useLoaderData(); - return ( -
-
{data}
- -
- ); - } - `, - - [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}._index.jsx`]: js` - export function loader() { - throw new Response("", { status: 401 }); - } - export default function Index() { - return
; - } - export function CatchBoundary() { - return ( -
${OWN_BOUNDARY_TEXT}
- ); - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); -}); - -test.afterAll(() => { - appFixture.close(); -}); - -test("renders root boundary with data available", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_BOUNDARY_TEXT); - expect(html).toMatch(ROOT_DATA); -}); - -test("renders root boundary with data available on transition", async ({ - page, -}) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector("#root-boundary"); - await page.waitForSelector(`#root-boundary-data:has-text("${ROOT_DATA}")`); -}); - -test("renders layout boundary with data available", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LAYOUT_NESTED_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_DATA); - expect(html).toMatch(LAYOUT_BOUNDARY_TEXT); - expect(html).toMatch(LAYOUT_DATA); -}); - -test("renders layout boundary with data available on transition", async ({ - page, -}) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LAYOUT_NESTED_LOADER); - await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`); - await page.waitForSelector( - `#layout-boundary:has-text("${LAYOUT_BOUNDARY_TEXT}")` - ); - await page.waitForSelector( - `#layout-boundary-data:has-text("${LAYOUT_DATA}")` - ); -}); - -test("renders self boundary with layout data available", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_NESTED_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_DATA); - expect(html).toMatch(LAYOUT_DATA); - expect(html).toMatch(OWN_BOUNDARY_TEXT); -}); - -test("renders self boundary with layout data available on transition", async ({ - page, -}) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_NESTED_LOADER); - await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`); - await page.waitForSelector(`#layout-data:has-text("${LAYOUT_DATA}")`); - await page.waitForSelector(`#own-boundary:has-text("${OWN_BOUNDARY_TEXT}")`); -}); - -// Copy/Paste of the above tests altered to use v2_errorBoundary. In v2 we can: -// - delete the above tests -// - remove this describe block -// - remove the v2_errorBoundary flag -test.describe("v2_errorBoundary", () => { - test.beforeAll(async () => { - fixture = await createFixture({ - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, - files: { - "app/root.jsx": js` import { json } from "@remix-run/node"; import { Links, @@ -289,7 +78,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/_index.jsx": js` + "app/routes/_index.jsx": js` import { Link } from "@remix-run/react"; export default function Index() { return ( @@ -302,7 +91,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` export function loader() { throw new Response("", { status: 401 }); } @@ -311,7 +100,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}.jsx`]: js` import { useMatches } from "@remix-run/react"; export function loader() { return "${LAYOUT_DATA}"; @@ -332,7 +121,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}._index.jsx`]: js` + [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}._index.jsx`]: js` export function loader() { throw new Response("", { status: 401 }); } @@ -341,7 +130,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}.jsx`]: js` import { Outlet, useLoaderData } from "@remix-run/react"; export function loader() { return "${LAYOUT_DATA}"; @@ -357,7 +146,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}._index.jsx`]: js` + [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}._index.jsx`]: js` export function loader() { throw new Response("", { status: 401 }); } @@ -370,77 +159,74 @@ test.describe("v2_errorBoundary", () => { ); } `, - }, - }); - - appFixture = await createAppFixture(fixture); + }, }); - test.afterAll(() => { - appFixture.close(); - }); + appFixture = await createAppFixture(fixture); +}); - test("renders root boundary with data available", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_BOUNDARY_TEXT); - expect(html).toMatch(ROOT_DATA); - }); +test.afterAll(() => { + appFixture.close(); +}); - test("renders root boundary with data available on transition", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector("#root-boundary"); - await page.waitForSelector(`#root-boundary-data:has-text("${ROOT_DATA}")`); - }); +test("renders root boundary with data available", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); + expect(res.status).toBe(401); + let html = await res.text(); + expect(html).toMatch(ROOT_BOUNDARY_TEXT); + expect(html).toMatch(ROOT_DATA); +}); - test("renders layout boundary with data available", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LAYOUT_NESTED_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_DATA); - expect(html).toMatch(LAYOUT_BOUNDARY_TEXT); - expect(html).toMatch(LAYOUT_DATA); - }); +test("renders root boundary with data available on transition", async ({ + page, +}) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(NO_BOUNDARY_LOADER); + await page.waitForSelector("#root-boundary"); + await page.waitForSelector(`#root-boundary-data:has-text("${ROOT_DATA}")`); +}); - test("renders layout boundary with data available on transition", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LAYOUT_NESTED_LOADER); - await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`); - await page.waitForSelector( - `#layout-boundary:has-text("${LAYOUT_BOUNDARY_TEXT}")` - ); - await page.waitForSelector( - `#layout-boundary-data:has-text("${LAYOUT_DATA}")` - ); - }); +test("renders layout boundary with data available", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_LAYOUT_NESTED_LOADER); + expect(res.status).toBe(401); + let html = await res.text(); + expect(html).toMatch(ROOT_DATA); + expect(html).toMatch(LAYOUT_BOUNDARY_TEXT); + expect(html).toMatch(LAYOUT_DATA); +}); - test("renders self boundary with layout data available", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_NESTED_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_DATA); - expect(html).toMatch(LAYOUT_DATA); - expect(html).toMatch(OWN_BOUNDARY_TEXT); - }); +test("renders layout boundary with data available on transition", async ({ + page, +}) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_LAYOUT_NESTED_LOADER); + await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`); + await page.waitForSelector( + `#layout-boundary:has-text("${LAYOUT_BOUNDARY_TEXT}")` + ); + await page.waitForSelector( + `#layout-boundary-data:has-text("${LAYOUT_DATA}")` + ); +}); - test("renders self boundary with layout data available on transition", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_NESTED_LOADER); - await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`); - await page.waitForSelector(`#layout-data:has-text("${LAYOUT_DATA}")`); - await page.waitForSelector( - `#own-boundary:has-text("${OWN_BOUNDARY_TEXT}")` - ); - }); +test("renders self boundary with layout data available", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_NESTED_LOADER); + expect(res.status).toBe(401); + let html = await res.text(); + expect(html).toMatch(ROOT_DATA); + expect(html).toMatch(LAYOUT_DATA); + expect(html).toMatch(OWN_BOUNDARY_TEXT); +}); + +test("renders self boundary with layout data available on transition", async ({ + page, +}) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_NESTED_LOADER); + await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`); + await page.waitForSelector(`#layout-data:has-text("${LAYOUT_DATA}")`); + await page.waitForSelector(`#own-boundary:has-text("${OWN_BOUNDARY_TEXT}")`); }); diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index d6ce148f187..c789d88c930 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -24,370 +24,11 @@ test.describe("CatchBoundary", () => { test.beforeAll(async () => { fixture = await createFixture({ - future: { v2_routeConvention: true }, + future: { + v2_routeConvention: true, + }, files: { "app/root.jsx": js` - import { json } from "@remix-run/node"; - import { Links, Meta, Outlet, Scripts, useMatches } from "@remix-run/react"; - - export function loader() { - return json({ data: "ROOT LOADER" }); - } - - export default function Root() { - return ( - - - - - - - - - - - ); - } - - export function CatchBoundary() { - let matches = useMatches() - return ( - - - -
${ROOT_BOUNDARY_TEXT}
-
{JSON.stringify(matches)}
- - - - ) - } - `, - - "app/routes/_index.jsx": js` - import { Link, Form } from "@remix-run/react"; - export default function() { - return ( -
- ${NOT_FOUND_HREF} - -
-
- ) - } - `, - - [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "@remix-run/react"; - export async function action() { - throw new Response("", { status: 401 }) - } - export function CatchBoundary() { - return

${OWN_BOUNDARY_TEXT}

- } - export default function Index() { - return ( -
- -
- ); - } - `, - - [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "@remix-run/react"; - export function action() { - throw new Response("", { status: 401 }) - } - export default function Index() { - return ( -
- -
- ) - } - `, - - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` - import { useCatch } from '@remix-run/react'; - export function loader() { - throw new Response("", { status: 401 }) - } - export function CatchBoundary() { - let caught = useCatch(); - return ( - <> -
${OWN_BOUNDARY_TEXT}
-
{caught.status}
- - ); - } - export default function Index() { - return
- } - `, - - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.child.jsx`]: js` - export function loader() { - throw new Response("", { status: 404 }) - } - export default function Index() { - return
- } - `, - - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` - export function loader() { - throw new Response("", { status: 401 }) - } - export default function Index() { - return
- } - `, - - "app/routes/action.jsx": js` - import { Outlet, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "PARENT"; - } - - export default function () { - return ( -
-

{useLoaderData()}

- -
- ) - } - `, - - "app/routes/action.child-catch.jsx": js` - import { Form, useCatch, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "CHILD"; - } - - export function action() { - throw new Response("Caught!", { status: 400 }); - } - - export default function () { - return ( - <> -

{useLoaderData()}

-
- -
- - ) - } - - export function CatchBoundary() { - let caught = useCatch() - return

{caught.status} {caught.data}

; - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); - }); - - test.afterAll(() => { - appFixture.close(); - }); - - test("non-matching urls on document requests", async () => { - let res = await fixture.requestDocument(NOT_FOUND_HREF); - expect(res.status).toBe(404); - let html = await res.text(); - expect(html).toMatch(ROOT_BOUNDARY_TEXT); - - // There should be no loader data on the root route - let expected = JSON.stringify([ - { id: "root", pathname: "", params: {} }, - ]).replace(/"/g, """); - expect(html).toContain(`
${expected}
`); - }); - - test("non-matching urls on client transitions", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NOT_FOUND_HREF, { wait: false }); - await page.waitForSelector("#root-boundary"); - - // Root loader data sticks around from previous load - let expected = JSON.stringify([ - { id: "root", pathname: "", params: {}, data: { data: "ROOT LOADER" } }, - ]); - expect(await app.getHtml("#matches")).toContain(expected); - }); - - test("own boundary, action, document request", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, action, client transition from other route", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector("#action-boundary"); - }); - - test("own boundary, action, client transition from itself", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(HAS_BOUNDARY_ACTION); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector("#action-boundary"); - }); - - test("bubbles to parent in action document requests", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector("#root-boundary"); - }); - - test("bubbles to parent in action script transitions from self", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_BOUNDARY_ACTION); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector("#root-boundary"); - }); - - test("own boundary, loader, document request", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, loader, client transition", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LOADER); - await page.waitForSelector("#boundary-loader"); - }); - - test("bubbles to parent in loader document requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in loader transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector("#root-boundary"); - }); - - test("uses correct catch boundary on server action errors", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/action/child-catch`); - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-data")).toMatch("CHILD"); - await page.click("button[type=submit]"); - await page.waitForSelector("#child-catch"); - // Preserves parent loader data - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-catch")).toMatch("400"); - expect(await app.getHtml("#child-catch")).toMatch("Caught!"); - }); - - test("prefers parent catch when child loader also bubbles, document request", async () => { - let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`); - expect(res.status).toBe(401); - let text = await res.text(); - expect(text).toMatch(OWN_BOUNDARY_TEXT); - expect(text).toMatch('
401
'); - }); - - test("prefers parent catch when child loader also bubbles, client transition", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`); - await page.waitForSelector("#boundary-loader"); - expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT); - expect(await app.getHtml("#status")).toMatch("401"); - }); -}); - -// Copy/Paste of the above tests altered to use v2_errorBoundary. In v2 we can: -// - delete the above tests -// - remove this describe block -// - remove the v2_errorBoundary flag -test.describe("v2_errorBoundary", () => { - test.describe("CatchBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - - let ROOT_BOUNDARY_TEXT = "ROOT_TEXT" as const; - let OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT" as const; - - let HAS_BOUNDARY_LOADER = "/yes/loader" as const; - let HAS_BOUNDARY_LOADER_FILE = "/yes.loader" as const; - let HAS_BOUNDARY_ACTION = "/yes/action" as const; - let HAS_BOUNDARY_ACTION_FILE = "/yes.action" as const; - let NO_BOUNDARY_ACTION = "/no/action" as const; - let NO_BOUNDARY_ACTION_FILE = "/no.action" as const; - let NO_BOUNDARY_LOADER = "/no/loader" as const; - let NO_BOUNDARY_LOADER_FILE = "/no.loader" as const; - - let NOT_FOUND_HREF = "/not/found"; - - test.beforeAll(async () => { - fixture = await createFixture({ - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, - files: { - "app/root.jsx": js` import { json } from "@remix-run/node"; import { Links, Meta, Outlet, Scripts, useMatches } from "@remix-run/react"; @@ -425,7 +66,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/_index.jsx": js` + "app/routes/_index.jsx": js` import { Link, Form } from "@remix-run/react"; export default function() { return ( @@ -451,7 +92,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` import { Form } from "@remix-run/react"; export async function action() { throw new Response("", { status: 401 }) @@ -470,7 +111,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` import { Form } from "@remix-run/react"; export function action() { throw new Response("", { status: 401 }) @@ -486,7 +127,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` import { useRouteError } from '@remix-run/react'; export function loader() { throw new Response("", { status: 401 }) @@ -505,7 +146,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.child.jsx`]: js` + [`app/routes${HAS_BOUNDARY_LOADER_FILE}.child.jsx`]: js` export function loader() { throw new Response("", { status: 404 }) } @@ -514,7 +155,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` export function loader() { throw new Response("", { status: 401 }) } @@ -523,7 +164,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/action.jsx": js` + "app/routes/action.jsx": js` import { Outlet, useLoaderData } from "@remix-run/react"; export function loader() { @@ -540,7 +181,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/action.child-catch.jsx": js` + "app/routes/action.child-catch.jsx": js` import { Form, useLoaderData, useRouteError } from "@remix-run/react"; export function loader() { @@ -569,152 +210,151 @@ test.describe("v2_errorBoundary", () => { return

{error.status} {error.data}

; } `, - }, - }); - - appFixture = await createAppFixture(fixture); + }, }); - test.afterAll(() => { - appFixture.close(); - }); + appFixture = await createAppFixture(fixture); + }); - test("non-matching urls on document requests", async () => { - let res = await fixture.requestDocument(NOT_FOUND_HREF); - expect(res.status).toBe(404); - let html = await res.text(); - expect(html).toMatch(ROOT_BOUNDARY_TEXT); - - // There should be no loader data on the root route - let expected = JSON.stringify([ - { id: "root", pathname: "", params: {} }, - ]).replace(/"/g, """); - expect(html).toContain(`
${expected}
`); - }); + test.afterAll(() => { + appFixture.close(); + }); - test("non-matching urls on client transitions", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NOT_FOUND_HREF, { wait: false }); - await page.waitForSelector("#root-boundary"); - - // Root loader data sticks around from previous load - let expected = JSON.stringify([ - { id: "root", pathname: "", params: {}, data: { data: "ROOT LOADER" } }, - ]); - expect(await app.getHtml("#matches")).toContain(expected); - }); + test("non-matching urls on document requests", async () => { + let res = await fixture.requestDocument(NOT_FOUND_HREF); + expect(res.status).toBe(404); + let html = await res.text(); + expect(html).toMatch(ROOT_BOUNDARY_TEXT); - test("own boundary, action, document request", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); + // There should be no loader data on the root route + let expected = JSON.stringify([ + { id: "root", pathname: "", params: {} }, + ]).replace(/"/g, """); + expect(html).toContain(`
${expected}
`); + }); - test("own boundary, action, client transition from other route", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector("#action-boundary"); - }); + test("non-matching urls on client transitions", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(NOT_FOUND_HREF, { wait: false }); + await page.waitForSelector("#root-boundary"); - test("own boundary, action, client transition from itself", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(HAS_BOUNDARY_ACTION); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector("#action-boundary"); - }); + // Root loader data sticks around from previous load + let expected = JSON.stringify([ + { id: "root", pathname: "", params: {}, data: { data: "ROOT LOADER" } }, + ]); + expect(await app.getHtml("#matches")).toContain(expected); + }); - test("bubbles to parent in action document requests", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("own boundary, action, document request", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); - test("bubbles to parent in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector("#root-boundary"); - }); + test("own boundary, action, client transition from other route", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + await page.waitForSelector("#action-boundary"); + }); - test("bubbles to parent in action script transitions from self", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_BOUNDARY_ACTION); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector("#root-boundary"); - }); + test("own boundary, action, client transition from itself", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(HAS_BOUNDARY_ACTION); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + await page.waitForSelector("#action-boundary"); + }); - test("own boundary, loader, document request", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); + test("bubbles to parent in action document requests", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("own boundary, loader, client transition", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LOADER); - await page.waitForSelector("#boundary-loader"); - }); + test("bubbles to parent in action script transitions from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + await page.waitForSelector("#root-boundary"); + }); - test("bubbles to parent in loader document requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("bubbles to parent in action script transitions from self", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(NO_BOUNDARY_ACTION); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + await page.waitForSelector("#root-boundary"); + }); - test("bubbles to parent in loader transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector("#root-boundary"); - }); + test("own boundary, loader, document request", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); - test("uses correct catch boundary on server action errors", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/action/child-catch`); - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-data")).toMatch("CHILD"); - await page.click("button[type=submit]"); - await page.waitForSelector("#child-catch"); - // Preserves parent loader data - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-catch")).toMatch("400"); - expect(await app.getHtml("#child-catch")).toMatch("Caught!"); - }); + test("own boundary, loader, client transition", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_LOADER); + await page.waitForSelector("#boundary-loader"); + }); - test("prefers parent catch when child loader also bubbles, document request", async () => { - let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`); - expect(res.status).toBe(401); - let text = await res.text(); - expect(text).toMatch(OWN_BOUNDARY_TEXT); - expect(text).toMatch('
401
'); - }); + test("bubbles to parent in loader document requests", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("prefers parent catch when child loader also bubbles, client transition", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`); - await page.waitForSelector("#boundary-loader"); - expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT); - expect(await app.getHtml("#status")).toMatch("401"); - }); + test("bubbles to parent in loader transitions from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(NO_BOUNDARY_LOADER); + await page.waitForSelector("#root-boundary"); + }); + + test("uses correct catch boundary on server action errors", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(`/action/child-catch`); + expect(await app.getHtml("#parent-data")).toMatch("PARENT"); + expect(await app.getHtml("#child-data")).toMatch("CHILD"); + await page.click("button[type=submit]"); + await page.waitForSelector("#child-catch"); + // Preserves parent loader data + expect(await app.getHtml("#parent-data")).toMatch("PARENT"); + expect(await app.getHtml("#child-catch")).toMatch("400"); + expect(await app.getHtml("#child-catch")).toMatch("Caught!"); + }); + + test("prefers parent catch when child loader also bubbles, document request", async () => { + let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`); + expect(res.status).toBe(401); + let text = await res.text(); + expect(text).toMatch(OWN_BOUNDARY_TEXT); + expect(text).toMatch('
401
'); + }); + + test("prefers parent catch when child loader also bubbles, client transition", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`); + await page.waitForSelector("#boundary-loader"); + expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT); + expect(await app.getHtml("#status")).toMatch("401"); }); }); diff --git a/integration/defer-test.ts b/integration/defer-test.ts index 7c3a92fb940..1c81b0e7e7d 100644 --- a/integration/defer-test.ts +++ b/integration/defer-test.ts @@ -35,7 +35,6 @@ test.describe("non-aborted", () => { fixture = await createFixture({ future: { v2_routeConvention: true, - v2_errorBoundary: true, }, files: { "app/components/counter.tsx": js` diff --git a/integration/error-boundary-test.ts b/integration/error-boundary-test.ts index 5f19ed4309a..03103acbcae 100644 --- a/integration/error-boundary-test.ts +++ b/integration/error-boundary-test.ts @@ -41,7 +41,9 @@ test.describe("ErrorBoundary", () => { _consoleError = console.error; console.error = () => {}; fixture = await createFixture({ - future: { v2_routeConvention: true }, + future: { + v2_routeConvention: true, + }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; @@ -259,7 +261,7 @@ test.describe("ErrorBoundary", () => { `, "app/routes/action.child-error.jsx": js` - import { Form, useLoaderData } from "@remix-run/react"; + import { Form, useLoaderData, useRouteError } from "@remix-run/react"; export function loader() { return "CHILD"; @@ -282,7 +284,8 @@ test.describe("ErrorBoundary", () => { ) } - export function ErrorBoundary({ error }) { + export function ErrorBoundary() { + let error = useRouteError(); return

{error.message}

; } `, @@ -659,7 +662,9 @@ test.describe("loaderData in ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture({ - future: { v2_routeConvention: true }, + future: { + v2_routeConvention: true, + }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; @@ -683,7 +688,7 @@ test.describe("loaderData in ErrorBoundary", () => { `, "app/routes/parent.jsx": js` - import { Outlet, useLoaderData, useMatches } from "@remix-run/react"; + import { Outlet, useLoaderData, useMatches, useRouteError } from "@remix-run/react"; export function loader() { return "PARENT"; @@ -698,7 +703,8 @@ test.describe("loaderData in ErrorBoundary", () => { ) } - export function ErrorBoundary({ error }) { + export function ErrorBoundary() { + let error = useRouteError(); return ( <>

{useLoaderData()}

@@ -712,7 +718,7 @@ test.describe("loaderData in ErrorBoundary", () => { `, "app/routes/parent.child-with-boundary.jsx": js` - import { Form, useLoaderData } from "@remix-run/react"; + import { Form, useLoaderData, useRouteError } from "@remix-run/react"; export function loader() { return "CHILD"; @@ -735,7 +741,8 @@ test.describe("loaderData in ErrorBoundary", () => { ) } - export function ErrorBoundary({ error }) { + export function ErrorBoundary() { + let error = useRouteError(); return ( <>

{useLoaderData()}

@@ -903,24 +910,26 @@ test.describe("Default ErrorBoundary", () => { ? "" : rootErrorBoundaryThrows ? js` - export function ErrorBoundary({ error }) { - return ( - - - -
-
Root Error Boundary
-

{error.message}

-

{oh.no.what.have.i.done}

-
- - - - ) - } - ` + export function ErrorBoundary() { + let error = useRouteError(); + return ( + + + +
+
Root Error Boundary
+

{error.message}

+

{oh.no.what.have.i.done}

+
+ + + + ) + } + ` : js` - export function ErrorBoundary({ error }) { + export function ErrorBoundary() { + let error = useRouteError(); return ( @@ -938,7 +947,7 @@ test.describe("Default ErrorBoundary", () => { return { "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; + import { Links, Meta, Outlet, Scripts, useRouteError } from "@remix-run/react"; export default function Root() { return ( @@ -1180,6 +1189,7 @@ test.describe("Default ErrorBoundary", () => { page, }, workerInfo) => { let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); await app.clickLink("/loader-error"); await page.waitForSelector("pre"); @@ -1218,1252 +1228,3 @@ test.describe("Default ErrorBoundary", () => { }); }); }); - -// Copy/Paste of the above tests altered to use v2_errorBoundary. In v2 we can: -// - delete the above tests -// - remove this describe block -// - remove the v2_errorBoundary flag -test.describe("v2_errorBoundary", () => { - test.describe("ErrorBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - let _consoleError: any; - - let ROOT_BOUNDARY_TEXT = "ROOT_BOUNDARY_TEXT"; - let OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT"; - - let HAS_BOUNDARY_LOADER = "/yes/loader" as const; - let HAS_BOUNDARY_LOADER_FILE = "/yes.loader" as const; - let HAS_BOUNDARY_ACTION = "/yes/action" as const; - let HAS_BOUNDARY_ACTION_FILE = "/yes.action" as const; - let HAS_BOUNDARY_RENDER = "/yes/render" as const; - let HAS_BOUNDARY_RENDER_FILE = "/yes.render" as const; - let HAS_BOUNDARY_NO_LOADER_OR_ACTION = "/yes/no-loader-or-action" as const; - let HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE = - "/yes.no-loader-or-action" as const; - - let NO_BOUNDARY_ACTION = "/no/action" as const; - let NO_BOUNDARY_ACTION_FILE = "/no.action" as const; - let NO_BOUNDARY_LOADER = "/no/loader" as const; - let NO_BOUNDARY_LOADER_FILE = "/no.loader" as const; - let NO_BOUNDARY_RENDER = "/no/render" as const; - let NO_BOUNDARY_RENDER_FILE = "/no.render" as const; - let NO_BOUNDARY_NO_LOADER_OR_ACTION = "/no/no-loader-or-action" as const; - let NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE = - "/no.no-loader-or-action" as const; - - let NOT_FOUND_HREF = "/not/found"; - - // packages/remix-react/errorBoundaries.tsx - let INTERNAL_ERROR_BOUNDARY_HEADING = "Application Error"; - - test.beforeAll(async () => { - _consoleError = console.error; - console.error = () => {}; - fixture = await createFixture({ - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, - files: { - "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export default function Root() { - return ( - - - - - - -
- -
- - - - ); - } - - export function ErrorBoundary() { - return ( - - - -
-
${ROOT_BOUNDARY_TEXT}
-
- - - - ) - } - `, - - "app/routes/_index.jsx": js` - import { Link, Form } from "@remix-run/react"; - export default function () { - return ( -
- ${NOT_FOUND_HREF} - -
- - - - -
- - - ${HAS_BOUNDARY_LOADER} - - - ${NO_BOUNDARY_LOADER} - - - ${HAS_BOUNDARY_RENDER} - - - ${NO_BOUNDARY_RENDER} - -
- ) - } - `, - - [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "@remix-run/react"; - export async function action() { - throw new Error("Kaboom!") - } - export function ErrorBoundary() { - return

${OWN_BOUNDARY_TEXT}

- } - export default function () { - return ( -
- -
- ); - } - `, - - [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "@remix-run/react"; - export function action() { - throw new Error("Kaboom!") - } - export default function () { - return ( -
- -
- ) - } - `, - - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` - export function loader() { - throw new Error("Kaboom!") - } - export function ErrorBoundary() { - return
${OWN_BOUNDARY_TEXT}
- } - export default function () { - return
- } - `, - - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` - export function loader() { - throw new Error("Kaboom!") - } - export default function () { - return
- } - `, - - [`app/routes${NO_BOUNDARY_RENDER_FILE}.jsx`]: js` - export default function () { - throw new Error("Kaboom!") - return
- } - `, - - [`app/routes${HAS_BOUNDARY_RENDER_FILE}.jsx`]: js` - export default function () { - throw new Error("Kaboom!") - return
- } - - export function ErrorBoundary() { - return
${OWN_BOUNDARY_TEXT}
- } - `, - - [`app/routes${HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js` - export function ErrorBoundary() { - return
${OWN_BOUNDARY_TEXT}
- } - export default function Index() { - return
- } - `, - - [`app/routes${NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js` - export default function Index() { - return
- } - `, - - "app/routes/fetcher-boundary.jsx": js` - import { useFetcher } from "@remix-run/react"; - export function ErrorBoundary() { - return

${OWN_BOUNDARY_TEXT}

- } - export default function() { - let fetcher = useFetcher(); - - return ( -
- -
- ) - } - `, - - "app/routes/fetcher-no-boundary.jsx": js` - import { useFetcher } from "@remix-run/react"; - export default function() { - let fetcher = useFetcher(); - - return ( -
- - - -
- ) - } - `, - - "app/routes/action.jsx": js` - import { Outlet, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "PARENT"; - } - - export default function () { - return ( -
-

{useLoaderData()}

- -
- ) - } - `, - - "app/routes/action.child-error.jsx": js` - import { Form, useLoaderData, useRouteError } from "@remix-run/react"; - - export function loader() { - return "CHILD"; - } - - export function action() { - throw new Error("Broken!"); - } - - export default function () { - return ( - <> -

{useLoaderData()}

-
- -
- - ) - } - - export function ErrorBoundary() { - let error = useRouteError(); - return

{error.message}

; - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); - }); - - test.afterAll(() => { - console.error = _consoleError; - appFixture.close(); - }); - - test("invalid request methods", async () => { - let res = await fixture.requestDocument("/", { method: "OPTIONS" }); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("own boundary, action, document request", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, action, client transition from other route", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, action, client transition from itself", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(HAS_BOUNDARY_ACTION); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("bubbles to parent in action document requests", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in action script transitions from self", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_BOUNDARY_ACTION); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("own boundary, loader, document request", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, loader, client transition", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LOADER); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("bubbles to parent in loader document requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in loader script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("ssr rendering errors with no boundary", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_RENDER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("script transition rendering errors with no boundary", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_RENDER); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("ssr rendering errors with boundary", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_RENDER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("script transition rendering errors with boundary", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_RENDER); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("uses correct error boundary on server action errors in nested routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/action/child-error`); - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-data")).toMatch("CHILD"); - await page.click("button[type=submit]"); - await page.waitForSelector("#child-error"); - // Preserves parent loader data - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-error")).toMatch("Broken!"); - }); - - test("renders own boundary in fetcher action submission without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/fetcher-boundary"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector("#fetcher-boundary"); - }); - - test("renders root boundary in fetcher action submission without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/fetcher-no-boundary"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - }); - - test("renders root boundary in document POST without action requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_NO_LOADER_OR_ACTION, { - method: "post", - }); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("renders root boundary in action script transitions without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - }); - - test("renders own boundary in document POST without action requests", async () => { - let res = await fixture.requestDocument( - HAS_BOUNDARY_NO_LOADER_OR_ACTION, - { - method: "post", - } - ); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("renders own boundary in action script transitions without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector("#boundary-no-loader-or-action"); - }); - - test.describe("if no error boundary exists in the app", () => { - let NO_ROOT_BOUNDARY_LOADER = "/loader-bad" as const; - let NO_ROOT_BOUNDARY_ACTION = "/action-bad" as const; - let NO_ROOT_BOUNDARY_LOADER_RETURN = "/loader-no-return" as const; - let NO_ROOT_BOUNDARY_ACTION_RETURN = "/action-no-return" as const; - - test.beforeAll(async () => { - fixture = await createFixture({ - future: { - v2_routeConvention: true, - }, - files: { - "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export default function Root() { - return ( - - - - - - - - - - - ); - } - `, - - "app/routes/_index.jsx": js` - import { Link, Form } from "@remix-run/react"; - - export default function () { - return ( -
-

Home

- Loader no return -
- - -
-
- ) - } - `, - - [`app/routes${NO_ROOT_BOUNDARY_LOADER}.jsx`]: js` - export async function loader() { - throw Error("BLARGH"); - } - - export default function () { - return ( -
-

Hello

-
- ) - } - `, - - [`app/routes${NO_ROOT_BOUNDARY_ACTION}.jsx`]: js` - export async function action() { - throw Error("YOOOOOOOO WHAT ARE YOU DOING"); - } - - export default function () { - return ( -
-

Goodbye

-
- ) - } - `, - - [`app/routes${NO_ROOT_BOUNDARY_LOADER_RETURN}.jsx`]: js` - import { useLoaderData } from "@remix-run/react"; - - export async function loader() {} - - export default function () { - let data = useLoaderData(); - return ( -
-

{data}

-
- ) - } - `, - - [`app/routes${NO_ROOT_BOUNDARY_ACTION_RETURN}.jsx`]: js` - import { useActionData } from "@remix-run/react"; - - export async function action() {} - - export default function () { - let data = useActionData(); - return ( -
-

{data}

-
- ) - } - `, - }, - }); - appFixture = await createAppFixture(fixture); - }); - - test("bubbles to internal boundary in loader document requests", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_ROOT_BOUNDARY_LOADER); - expect(await app.getHtml("h1")).toMatch( - INTERNAL_ERROR_BOUNDARY_HEADING - ); - }); - - test("bubbles to internal boundary in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch( - INTERNAL_ERROR_BOUNDARY_HEADING - ); - }); - - test("bubbles to internal boundary if loader doesn't return (document requests)", async () => { - let res = await fixture.requestDocument(NO_ROOT_BOUNDARY_LOADER_RETURN); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); - - test("bubbles to internal boundary if loader doesn't return", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_ROOT_BOUNDARY_LOADER_RETURN); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch( - INTERNAL_ERROR_BOUNDARY_HEADING - ); - }); - - test("bubbles to internal boundary if action doesn't return (document requests)", async () => { - let res = await fixture.requestDocument( - NO_ROOT_BOUNDARY_ACTION_RETURN, - { - method: "post", - } - ); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); - - test("bubbles to internal boundary if action doesn't return", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION_RETURN); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch( - INTERNAL_ERROR_BOUNDARY_HEADING - ); - }); - }); - }); - - test.describe("loaderData in ErrorBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - let consoleErrors: string[]; - let oldConsoleError: () => void; - - test.beforeAll(async () => { - fixture = await createFixture({ - future: { - v2_routeConvention: true, - 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 { Outlet, useLoaderData, useMatches, useRouteError } from "@remix-run/react"; - - export function loader() { - return "PARENT"; - } - - export default function () { - return ( -
-

{useLoaderData()}

- -
- ) - } - - export function ErrorBoundary() { - let error = useRouteError(); - return ( - <> -

{useLoaderData()}

-

- {useMatches().find(m => m.id === 'routes/parent').data} -

-

{error.message}

- - ); - } - `, - - "app/routes/parent.child-with-boundary.jsx": js` - import { Form, useLoaderData, useRouteError } from "@remix-run/react"; - - export function loader() { - return "CHILD"; - } - - export function action() { - throw new Error("Broken!"); - } - - export default function () { - return ( - <> -

{useLoaderData()}

-
- -
- - ) - } - - export function ErrorBoundary() { - let error = useRouteError(); - return ( - <> -

{useLoaderData()}

-

{error.message}

- - ); - } - `, - - "app/routes/parent.child-without-boundary.jsx": js` - import { Form, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "CHILD"; - } - - export function action() { - throw new Error("Broken!"); - } - - export default function () { - return ( - <> -

{useLoaderData()}

-
- -
- - ) - } - `, - }, - }); - - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => { - appFixture.close(); - }); - - test.beforeEach(({ page }) => { - oldConsoleError = console.error; - console.error = () => {}; - consoleErrors = []; - // Listen for all console events and handle errors - page.on("console", (msg) => { - if (msg.type() === "error") { - consoleErrors.push(msg.text()); - } - }); - }); - - 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() { - test("Prevents useLoaderData in self ErrorBoundary", async ({ - page, - javaScriptEnabled, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child-with-boundary"); - - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

CHILD

' - ); - expect(consoleErrors).toEqual([]); - - await app.clickSubmitButton("/parent/child-with-boundary"); - await page.waitForSelector("#child-error"); - - expect(await app.getHtml("#child-error")).toEqual( - '

Broken!

' - ); - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

' - ); - - // Only look for this message. Chromium browsers will also log the - // network error but firefox does not - // "Failed to load resource: the server responded with a status of 500 (Internal Server Error)", - let msg = - "You cannot `useLoaderData` in an errorElement (routeId: routes/parent.child-with-boundary)"; - if (javaScriptEnabled) { - expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]); - } else { - // We don't get the useLoaderData message in the client when JS is disabled - expect(consoleErrors.filter((m) => m === msg)).toEqual([]); - } - }); - - test("Prevents useLoaderData in bubbled ErrorBoundary", async ({ - page, - javaScriptEnabled, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child-without-boundary"); - - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

CHILD

' - ); - expect(consoleErrors).toEqual([]); - - await app.clickSubmitButton("/parent/child-without-boundary"); - await page.waitForSelector("#parent-error"); - - expect(await app.getHtml("#parent-error")).toEqual( - '

Broken!

' - ); - expect(await app.getHtml("#parent-matches-data")).toEqual( - '

' - ); - expect(await app.getHtml("#parent-data")).toEqual( - '

' - ); - - // Only look for this message. Chromium browsers will also log the - // network error but firefox does not - // "Failed to load resource: the server responded with a status of 500 (Internal Server Error)", - let msg = - "You cannot `useLoaderData` in an errorElement (routeId: routes/parent)"; - if (javaScriptEnabled) { - expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]); - } else { - // We don't get the useLoaderData message in the client when JS is disabled - expect(consoleErrors.filter((m) => m === msg)).toEqual([]); - } - }); - } - }); - - test.describe("Default ErrorBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - let _consoleError: any; - - function getFiles({ - includeRootErrorBoundary = false, - rootErrorBoundaryThrows = false, - } = {}) { - let errorBoundaryCode = !includeRootErrorBoundary - ? "" - : rootErrorBoundaryThrows - ? js` - export function ErrorBoundary() { - let error = useRouteError(); - return ( - - - -
-
Root Error Boundary
-

{error.message}

-

{oh.no.what.have.i.done}

-
- - - - ) - } - ` - : js` - export function ErrorBoundary() { - let error = useRouteError(); - return ( - - - -
-
Root Error Boundary
-

{error.message}

-
- - - - ) - } - `; - - return { - "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts, useRouteError } from "@remix-run/react"; - - export default function Root() { - return ( - - - - - - -
- -
- - - - ); - } - - ${errorBoundaryCode} - `, - - "app/routes/_index.jsx": js` - import { Link } from "@remix-run/react"; - export default function () { - return ( -
-

Index

- Loader Error - Render Error -
- ); - } - `, - - "app/routes/loader-error.jsx": js` - export function loader() { - throw new Error('Loader Error'); - } - export default function () { - return

Loader Error

- } - `, - - "app/routes/render-error.jsx": js` - export default function () { - throw new Error("Render Error") - } - `, - }; - } - - test.beforeAll(async () => { - _consoleError = console.error; - console.error = () => {}; - }); - - test.afterAll(async () => { - console.error = _consoleError; - appFixture.close(); - }); - - test.describe("When the root route does not have a boundary", () => { - test.beforeAll(async () => { - fixture = await createFixture({ - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, - files: getFiles({ includeRootErrorBoundary: false }), - }); - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("renders default boundary on loader errors", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Application Error"); - expect(text).toMatch("Loader Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - - test("renders default boundary on render errors", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Application Error"); - expect(text).toMatch("Render Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - }); - - test.describe("SPA navigations", () => { - test("renders default boundary on loader errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - expect(html).toMatch("Loader Error"); - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("renders default boundary on render errors", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - // Chromium seems to be the only one that includes the message in the stack - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("Render Error"); - } - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - }); - }); - - test.describe("When the root route has a boundary", () => { - test.beforeAll(async () => { - fixture = await createFixture({ - future: { - v2_routeConvention: true, - }, - files: getFiles({ includeRootErrorBoundary: true }), - }); - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("renders root boundary on loader errors", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Root Error Boundary"); - expect(text).toMatch("Loader Error"); - expect(text).not.toMatch("Application Error"); - }); - - test("renders root boundary on render errors", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Root Error Boundary"); - expect(text).toMatch("Render Error"); - expect(text).not.toMatch("Application Error"); - }); - }); - - test.describe("SPA navigations", () => { - test("renders root boundary on loader errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("#root-error-boundary"); - let html = await app.getHtml(); - expect(html).toMatch("Root Error Boundary"); - expect(html).toMatch("Loader Error"); - expect(html).not.toMatch("Application Error"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("renders root boundary on render errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("#root-error-boundary"); - let html = await app.getHtml(); - expect(html).toMatch("Root Error Boundary"); - expect(html).toMatch("Render Error"); - expect(html).not.toMatch("Application Error"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - }); - }); - - test.describe("When the root route has a boundary but it also throws 😦", () => { - test.beforeAll(async () => { - fixture = await createFixture({ - future: { - v2_routeConvention: true, - }, - files: getFiles({ - includeRootErrorBoundary: true, - rootErrorBoundaryThrows: true, - }), - }); - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("tries to render root boundary on loader errors but bubbles to default boundary", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Unexpected Server Error"); - expect(text).not.toMatch("Application Error"); - expect(text).not.toMatch("Loader Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - - test("tries to render root boundary on render errors but bubbles to default boundary", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Unexpected Server Error"); - expect(text).not.toMatch("Application Error"); - expect(text).not.toMatch("Render Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - }); - - test.describe("SPA navigations", () => { - test("tries to render root boundary on loader errors but bubbles to default boundary", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("ReferenceError: oh is not defined"); - } - expect(html).not.toMatch("Loader Error"); - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("tries to render root boundary on render errors but bubbles to default boundary", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("ReferenceError: oh is not defined"); - } - expect(html).not.toMatch("Render Error"); - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - }); - }); - }); -}); diff --git a/integration/error-boundary-v2-test.ts b/integration/error-boundary-v2-test.ts index 8f0a627c576..e8b1d381baf 100644 --- a/integration/error-boundary-v2-test.ts +++ b/integration/error-boundary-v2-test.ts @@ -14,7 +14,6 @@ test.describe("V2 Singular ErrorBoundary (future.v2_errorBoundary)", () => { test.beforeAll(async () => { fixture = await createFixture({ future: { - v2_errorBoundary: true, v2_routeConvention: true, }, files: { diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts index 13af69b0261..ba8a5ed1d1e 100644 --- a/integration/hmr-test.ts +++ b/integration/hmr-test.ts @@ -15,7 +15,6 @@ let fixture = (options: { port: number; appServerPort: number }) => ({ }, unstable_tailwind: true, v2_routeConvention: true, - v2_errorBoundary: true, }, files: { "package.json": json({ diff --git a/integration/link-test.ts b/integration/link-test.ts index ae0e076ae3b..1911913ab98 100644 --- a/integration/link-test.ts +++ b/integration/link-test.ts @@ -34,7 +34,6 @@ test.describe("route module link export", () => { fixture = await createFixture({ future: { v2_routeConvention: true, - v2_errorBoundary: true, }, files: { "app/favicon.ico": js``, diff --git a/packages/remix-dev/__tests__/create-test.ts b/packages/remix-dev/__tests__/create-test.ts index 2f9eb196b83..4508ac91907 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 { errorBoundaryWarning, flatRoutesWarning } from "../config"; +import { flatRoutesWarning } from "../config"; beforeAll(() => server.listen({ onUnhandledRequest: "error" })); afterAll(() => server.close()); @@ -348,9 +348,7 @@ describe("the create command", () => { "--no-typescript", ]); expect(output.trim()).toBe( - errorBoundaryWarning + - "\n" + - flatRoutesWarning + + flatRoutesWarning + "\n\n" + getOptOutOfInstallMessage() + "\n\n" + diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index c023f0485b6..3acdab20b96 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -29,7 +29,6 @@ describe("readConfig", () => { unstable_postcss: expect.any(Boolean), unstable_tailwind: expect.any(Boolean), unstable_vanillaExtract: expect.any(Boolean), - v2_errorBoundary: expect.any(Boolean), v2_routeConvention: expect.any(Boolean), }, }, @@ -50,7 +49,6 @@ describe("readConfig", () => { "unstable_postcss": Any, "unstable_tailwind": Any, "unstable_vanillaExtract": Any, - "v2_errorBoundary": Any, "v2_routeConvention": Any, }, "mdx": undefined, diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index f8c427704cc..c8bf11df2d7 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -27,7 +27,6 @@ export interface AssetsManifest { imports?: string[]; hasAction: boolean; hasLoader: boolean; - hasCatchBoundary: boolean; hasErrorBoundary: boolean; }; }; @@ -112,7 +111,6 @@ export async function createAssetsManifest({ imports: resolveImports(output.imports), hasAction: sourceExports.includes("action"), hasLoader: sourceExports.includes("loader"), - hasCatchBoundary: sourceExports.includes("CatchBoundary"), hasErrorBoundary: sourceExports.includes("ErrorBoundary"), }; } diff --git a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts index b8fdd1c88b8..51c5a45b0b0 100644 --- a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts @@ -7,7 +7,6 @@ import invariant from "../../invariant"; type Route = RemixConfig["routes"][string]; const browserSafeRouteExports: { [name: string]: boolean } = { - CatchBoundary: true, ErrorBoundary: true, default: true, handle: true, diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index ad1b7c37b65..22f8630406a 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -44,7 +44,6 @@ interface FutureConfig { unstable_postcss: boolean; unstable_tailwind: boolean; unstable_vanillaExtract: boolean | VanillaExtractOptions; - v2_errorBoundary: boolean; v2_routeConvention: boolean; } @@ -361,10 +360,6 @@ export async function readConfig( } } - if (!appConfig.future?.v2_errorBoundary) { - warnOnce(errorBoundaryWarning, "v2_errorBoundary"); - } - let serverBuildPath = path.resolve( rootDirectory, appConfig.serverBuildPath ?? "build/index.js" @@ -564,7 +559,6 @@ export async function readConfig( unstable_postcss: appConfig.future?.unstable_postcss === true, unstable_tailwind: appConfig.future?.unstable_tailwind === true, unstable_vanillaExtract: appConfig.future?.unstable_vanillaExtract ?? false, - v2_errorBoundary: appConfig.future?.v2_errorBoundary === true, v2_routeConvention: appConfig.future?.v2_routeConvention === true, }; @@ -649,11 +643,3 @@ export let flatRoutesWarning = "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/__tests__/components-test.tsx b/packages/remix-react/__tests__/components-test.tsx index ea112487dbb..cc4f53d7c66 100644 --- a/packages/remix-react/__tests__/components-test.tsx +++ b/packages/remix-react/__tests__/components-test.tsx @@ -92,7 +92,6 @@ function itPrefetchesPageLinks< idk: { hasLoader: true, hasAction: false, - hasCatchBoundary: false, hasErrorBoundary: false, id: "idk", module: "idk.js", diff --git a/packages/remix-react/__tests__/scroll-restoration-test.tsx b/packages/remix-react/__tests__/scroll-restoration-test.tsx index f286182f176..98f9637342b 100644 --- a/packages/remix-react/__tests__/scroll-restoration-test.tsx +++ b/packages/remix-react/__tests__/scroll-restoration-test.tsx @@ -24,7 +24,6 @@ describe("", () => { root: { hasLoader: false, hasAction: false, - hasCatchBoundary: false, hasErrorBoundary: false, id: "root", module: "root.js", diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index 70eeb7095d5..c0eb632229e 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -7,14 +7,10 @@ import { useSyncExternalStore } from "use-sync-external-store/shim"; import { RemixContext } from "./components"; import type { EntryContext, FutureConfig } from "./entry"; -import { - RemixErrorBoundary, - RemixRootDefaultErrorBoundary, -} from "./errorBoundaries"; +import { RemixErrorBoundary } from "./errorBoundaries"; 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 { @@ -84,10 +80,6 @@ if (import.meta && import.meta.hot) { ? window.__remixRouteModules[id]?.default ?? imported.default : imported.default, - CatchBoundary: imported.CatchBoundary - ? window.__remixRouteModules[id]?.CatchBoundary ?? - imported.CatchBoundary - : imported.CatchBoundary, ErrorBoundary: imported.ErrorBoundary ? window.__remixRouteModules[id]?.ErrorBoundary ?? imported.ErrorBoundary @@ -139,18 +131,6 @@ 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, @@ -186,10 +166,7 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { future: window.__remixContext.future, }} > - + diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index fb02e1ad0b9..91567759cba 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -7,7 +7,6 @@ import * as React from "react"; import type { AgnosticDataRouteMatch, UNSAFE_DeferredData as DeferredData, - ErrorResponse, TrackedPromise, } from "@remix-run/router"; import type { @@ -26,7 +25,6 @@ import { NavLink as RouterNavLink, UNSAFE_DataRouterContext as DataRouterContext, UNSAFE_DataRouterStateContext as DataRouterStateContext, - isRouteErrorResponse, matchRoutes, useAsyncError, useFetcher as useFetcherRR, @@ -43,12 +41,7 @@ import type { SerializeFrom } from "@remix-run/server-runtime"; import type { AppData, FormMethod } from "./data"; import type { EntryContext, RemixContextObject } from "./entry"; -import { - RemixRootDefaultErrorBoundary, - RemixRootDefaultCatchBoundary, - RemixCatchBoundary, - V2_RemixRootDefaultErrorBoundary, -} from "./errorBoundaries"; +import { RemixRootDefaultErrorBoundary } from "./errorBoundaries"; import invariant from "./invariant"; import { getDataLinkHrefs, @@ -132,7 +125,7 @@ export function RemixRoute({ id }: { id: string }) { } export function RemixRouteError({ id }: { id: string }) { - let { future, routeModules } = useRemixContext(); + let { routeModules } = useRemixContext(); // This checks prevent cryptic error messages such as: 'Cannot read properties of undefined (reading 'root')' invariant( @@ -142,51 +135,15 @@ export function RemixRouteError({ id }: { id: string }) { ); let error = useRouteError(); - let { CatchBoundary, ErrorBoundary } = routeModules[id]; + let { ErrorBoundary } = routeModules[id]; - 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; + if (ErrorBoundary) { + return ; } - // Provide defaults for the root route if they are not present if (id === "root") { - CatchBoundary ||= RemixRootDefaultCatchBoundary; - ErrorBoundary ||= RemixRootDefaultErrorBoundary; - } - - if (isRouteErrorResponse(error)) { - let tError = error as any; - if ( - tError?.error instanceof Error && - tError.status !== 404 && - ErrorBoundary - ) { - // Internal framework-thrown ErrorResponses - return ; - } - if (CatchBoundary) { - // User-thrown ErrorResponses - return ( - - ); - } - } - - if (error instanceof Error && ErrorBoundary) { - // User- or framework-thrown Errors - return ; + // Provide defaults for the root route if they are not present + return ; } throw error; diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index 434de6c09f9..d2e10601c9f 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -36,7 +36,6 @@ export interface FutureConfig { unstable_postcss: boolean; unstable_tailwind: boolean; unstable_vanillaExtract: boolean | VanillaExtractOptions; - v2_errorBoundary: boolean; v2_routeConvention: boolean; } diff --git a/packages/remix-react/errorBoundaries.tsx b/packages/remix-react/errorBoundaries.tsx index 5e083af4d36..237774d046c 100644 --- a/packages/remix-react/errorBoundaries.tsx +++ b/packages/remix-react/errorBoundaries.tsx @@ -1,16 +1,9 @@ -import React, { useContext } from "react"; -import type { ErrorResponse, Location } from "@remix-run/router"; -import { isRouteErrorResponse, useRouteError } from "react-router-dom"; - -import type { - CatchBoundaryComponent, - ErrorBoundaryComponent, -} from "./routeModules"; -import type { ThrownResponse } from "./errors"; +import React from "react"; +import type { Location } from "@remix-run/router"; +import { isRouteErrorResponse } from "react-router-dom"; type RemixErrorBoundaryProps = React.PropsWithChildren<{ location: Location; - component: ErrorBoundaryComponent; error?: Error; }>; @@ -59,62 +52,29 @@ export class RemixErrorBoundary extends React.Component< render() { if (this.state.error) { - return ; + return ; } else { return this.props.children; } } } -/** - * When app's don't provide a root level ErrorBoundary, we default to this. - */ -export function RemixRootDefaultErrorBoundary({ error }: { error: Error }) { +export function RemixRootDefaultErrorBoundary({ error }: { error: unknown }) { console.error(error); - return ( - - - - - Application Error! - - -
-

Application Error

-
-            {error.stack}
-          
-
-