From f0b8d0a22bb778fc62148694eb00a6aa8280fa84 Mon Sep 17 00:00:00 2001 From: Mehdi Achour Date: Sun, 16 Jun 2024 17:55:41 +0100 Subject: [PATCH] refactor(catch-boundary): Switch to V2 + Vite (#511) --- catch-boundary/.eslintrc.js | 4 - catch-boundary/app/routes/users.$userId.tsx | 64 ------------- catch-boundary/package.json | 29 ------ catch-boundary/remix.config.js | 11 --- catch-boundary/remix.env.d.ts | 2 - catch-boundary/tsconfig.json | 22 ----- error-boundary/.eslintrc.cjs | 84 ++++++++++++++++++ {catch-boundary => error-boundary}/.gitignore | 1 - {catch-boundary => error-boundary}/README.md | 12 +-- .../app/data.server.ts | 0 .../app/root.tsx | 19 ++-- .../app/routes/_index.tsx | 0 error-boundary/app/routes/users.$userId.tsx | 61 +++++++++++++ .../app/routes/users.tsx | 2 +- error-boundary/package.json | 40 +++++++++ .../public/favicon.ico | Bin .../sandbox.config.json | 0 error-boundary/tsconfig.json | 32 +++++++ error-boundary/vite.config.ts | 16 ++++ 19 files changed, 248 insertions(+), 151 deletions(-) delete mode 100644 catch-boundary/.eslintrc.js delete mode 100644 catch-boundary/app/routes/users.$userId.tsx delete mode 100644 catch-boundary/package.json delete mode 100644 catch-boundary/remix.config.js delete mode 100644 catch-boundary/remix.env.d.ts delete mode 100644 catch-boundary/tsconfig.json create mode 100644 error-boundary/.eslintrc.cjs rename {catch-boundary => error-boundary}/.gitignore (70%) rename {catch-boundary => error-boundary}/README.md (52%) rename {catch-boundary => error-boundary}/app/data.server.ts (100%) rename {catch-boundary => error-boundary}/app/root.tsx (55%) rename {catch-boundary => error-boundary}/app/routes/_index.tsx (100%) create mode 100644 error-boundary/app/routes/users.$userId.tsx rename {catch-boundary => error-boundary}/app/routes/users.tsx (95%) create mode 100644 error-boundary/package.json rename {catch-boundary => error-boundary}/public/favicon.ico (100%) rename {catch-boundary => error-boundary}/sandbox.config.json (100%) create mode 100644 error-boundary/tsconfig.json create mode 100644 error-boundary/vite.config.ts diff --git a/catch-boundary/.eslintrc.js b/catch-boundary/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/catch-boundary/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/catch-boundary/app/routes/users.$userId.tsx b/catch-boundary/app/routes/users.$userId.tsx deleted file mode 100644 index b043de80..00000000 --- a/catch-boundary/app/routes/users.$userId.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { LoaderArgs, MetaFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; -import { useCatch, useLoaderData, useParams } from "@remix-run/react"; - -import { getUsers } from "~/data.server"; - -export const meta: MetaFunction = ({ data }) => { - // When the response is thrown for a missing user, the data will be the - // thrown response. - if (!data || !data.user) { - return { title: "User not found!" }; - } - return { title: data.user.name }; -}; - -export const loader = async ({ params }: LoaderArgs) => { - const userId = params.userId; - - const users = getUsers(); - const user = users.find(({ id }) => id === userId); - - if (!user) { - // When there's an expected error (like no found user) throw a response. - throw new Response("Not Found", { status: 404 }); - } - - return json({ user }); -}; - -export default function User() { - const { user } = useLoaderData(); - return
Hi there {user.name}!
; -} - -// Export a CatchBoundary and use the useCatch hook to handle thrown responses -// like the 404 we have in our loader. -// You can also catch thrown responses from actions as well. -export function CatchBoundary() { - const caught = useCatch(); - const params = useParams(); - - switch (caught.status) { - case 404: { - return

User with ID "{params.userId}" not found!

; - } - default: { - // if we don't handle this then all bets are off. Just throw an error - // and let the nearest ErrorBoundary handle this - throw new Error(`${caught.status} not handled`); - } - } -} - -// this will handle unexpected errors (like the default case above where the -// CatchBoundary gets a response it's not prepared to handle). -export function ErrorBoundary({ error }: { error: Error }) { - console.error(error); - - return ( -
-
{JSON.stringify(error, null, 2)}
-
- ); -} diff --git a/catch-boundary/package.json b/catch-boundary/package.json deleted file mode 100644 index a437590d..00000000 --- a/catch-boundary/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "private": true, - "sideEffects": false, - "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", - "typecheck": "tsc" - }, - "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "isbot": "^3.6.5", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" - }, - "engines": { - "node": ">=14.0.0" - } -} diff --git a/catch-boundary/remix.config.js b/catch-boundary/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/catch-boundary/remix.config.js +++ /dev/null @@ -1,11 +0,0 @@ -/** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { - future: { - v2_routeConvention: true, - }, - ignoredRouteFiles: ["**/.*"], - // appDirectory: "app", - // assetsBuildDirectory: "public/build", - // publicPath: "/build/", - // serverBuildPath: "build/index.js", -}; diff --git a/catch-boundary/remix.env.d.ts b/catch-boundary/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/catch-boundary/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/catch-boundary/tsconfig.json b/catch-boundary/tsconfig.json deleted file mode 100644 index 20f8a386..00000000 --- a/catch-boundary/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2019"], - "isolatedModules": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "resolveJsonModule": true, - "target": "ES2019", - "strict": true, - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - - // Remix takes care of building everything in `remix build`. - "noEmit": true - } -} diff --git a/error-boundary/.eslintrc.cjs b/error-boundary/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/error-boundary/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/catch-boundary/.gitignore b/error-boundary/.gitignore similarity index 70% rename from catch-boundary/.gitignore rename to error-boundary/.gitignore index 3f7bf98d..80ec311f 100644 --- a/catch-boundary/.gitignore +++ b/error-boundary/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/catch-boundary/README.md b/error-boundary/README.md similarity index 52% rename from catch-boundary/README.md rename to error-boundary/README.md index 790ca13b..1dbb6bbd 100644 --- a/catch-boundary/README.md +++ b/error-boundary/README.md @@ -1,19 +1,19 @@ -# CatchBoundary Example +# ErrorBoundary Example -If you want to handle _expected_ errors, you use a `CatchBoundary` to catch those types of errors. Think about HTTP-400-level errors like unauthorized etc. +If you want to handle errors, export an `ErrorBoundary` from your route. ## Preview Open this example on [CodeSandbox](https://codesandbox.com): -[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/catch-boundary) +[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/error-boundary) ## Example -In this example, we have a list of users and one user that does not exist. When you navigate to the user that does not exist, our CatchBoundary renders in place of the component for that route. +In this example, we have a list of users and one user that does not exist. When you navigate to the user that does not exist, our ErrorBoundary renders in place of the component for that route. -Check [app/routes/users/$userId.tsx](app/routes/users/$userId.tsx) to see the CatchBoundary in action. +Check [app/routes/users/$userId.tsx](app/routes/users/$userId.tsx) to see the ErrorBoundary in action. ## Related Links -- [CatchBoundary in the Remix Docs](https://remix.run/route/catch-boundary) +- [ErrorBoundary in the Remix Docs](https://remix.run/route/error-boundary) diff --git a/catch-boundary/app/data.server.ts b/error-boundary/app/data.server.ts similarity index 100% rename from catch-boundary/app/data.server.ts rename to error-boundary/app/data.server.ts diff --git a/catch-boundary/app/root.tsx b/error-boundary/app/root.tsx similarity index 55% rename from catch-boundary/app/root.tsx rename to error-boundary/app/root.tsx index 927a0f74..e82f26fd 100644 --- a/catch-boundary/app/root.tsx +++ b/error-boundary/app/root.tsx @@ -1,32 +1,29 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/catch-boundary/app/routes/_index.tsx b/error-boundary/app/routes/_index.tsx similarity index 100% rename from catch-boundary/app/routes/_index.tsx rename to error-boundary/app/routes/_index.tsx diff --git a/error-boundary/app/routes/users.$userId.tsx b/error-boundary/app/routes/users.$userId.tsx new file mode 100644 index 00000000..a18508e1 --- /dev/null +++ b/error-boundary/app/routes/users.$userId.tsx @@ -0,0 +1,61 @@ +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { + isRouteErrorResponse, + useLoaderData, + useParams, + useRouteError, +} from "@remix-run/react"; + +import { getUsers } from "~/data.server"; + +export const meta: MetaFunction = ({ data }) => { + // Handle our 404 gracefully by setting a generic error as page title + if (!data || !data.user) { + return [{ title: "User not found!" }]; + } + return [{ title: data.user.name }]; +}; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const userId = params.userId; + + const users = getUsers(); + const user = users.find(({ id }) => id === userId); + + if (!user) { + // When there's an expected error (like no found user) throw a response. + throw new Response("Not Found", { status: 404 }); + } + + return json({ user }); +}; + +export default function User() { + const { user } = useLoaderData(); + return
Hi there {user.name}!
; +} + +// Export an ErrorBoundary and use the useRouteError/isRouteErrorResponse +// combo to handle thrown responses like the 404 we have in our loader. +// You can also catch thrown responses from actions as well. +export function ErrorBoundary() { + const error = useRouteError(); + const params = useParams(); + + if (isRouteErrorResponse(error) && error.status === 404) { + return ( + + User with ID "{params.userId}" not found! + + ); + } + + console.error(error); + + return ( +
+
{JSON.stringify(error, null, 2)}
+
+ ); +} diff --git a/catch-boundary/app/routes/users.tsx b/error-boundary/app/routes/users.tsx similarity index 95% rename from catch-boundary/app/routes/users.tsx rename to error-boundary/app/routes/users.tsx index b6f0cf22..21ec37d7 100644 --- a/catch-boundary/app/routes/users.tsx +++ b/error-boundary/app/routes/users.tsx @@ -5,7 +5,7 @@ import { Link, Outlet, useLoaderData } from "@remix-run/react"; import { getUsers } from "~/data.server"; export const meta: MetaFunction = () => { - return { title: "Users" }; + return [{ title: "Users" }]; }; export const loader = async () => { diff --git a/error-boundary/package.json b/error-boundary/package.json new file mode 100644 index 00000000..23b2fdab --- /dev/null +++ b/error-boundary/package.json @@ -0,0 +1,40 @@ +{ + "name": "error-boundary", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", + "typecheck": "tsc" + }, + "dependencies": { + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/catch-boundary/public/favicon.ico b/error-boundary/public/favicon.ico similarity index 100% rename from catch-boundary/public/favicon.ico rename to error-boundary/public/favicon.ico diff --git a/catch-boundary/sandbox.config.json b/error-boundary/sandbox.config.json similarity index 100% rename from catch-boundary/sandbox.config.json rename to error-boundary/sandbox.config.json diff --git a/error-boundary/tsconfig.json b/error-boundary/tsconfig.json new file mode 100644 index 00000000..9d87dd37 --- /dev/null +++ b/error-boundary/tsconfig.json @@ -0,0 +1,32 @@ +{ + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Vite takes care of building everything, not tsc. + "noEmit": true + } +} diff --git a/error-boundary/vite.config.ts b/error-boundary/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/error-boundary/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +});