From ea19c12b37597aec51b13c95445ee3cef29f666a Mon Sep 17 00:00:00 2001 From: Mehdi Achour Date: Thu, 10 Nov 2022 21:14:45 +0100 Subject: [PATCH 01/77] docs(pages/contributing): Fix typos (#4565) --- docs/pages/contributing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/pages/contributing.md b/docs/pages/contributing.md index 433f0ba7d7e..1ad6684ffff 100644 --- a/docs/pages/contributing.md +++ b/docs/pages/contributing.md @@ -163,7 +163,7 @@ The following steps will get you setup to contribute changes to this repo: **Important:** When creating the PR in GitHub, make sure that you set the base to the correct branch. - **`dev`** is for changes to code -- **`main`**: is for changes to documentation, examples, and some templates +- **`main`**: is for changes to documentation and some templates You can set the base in GitHub when authoring the PR with the dropdown below the "Compare changes" heading: @@ -239,11 +239,11 @@ There may be other branches for various features and experimentation, but all of ## How the heck do nightly releases work? -Nightly releases will run the action files from the `main` branch as scheduled workflows will always use the latest commit to the default branch, signified by [this comment on the nightly action file][nightly-action-comment] and the explicit branch appended to the reuasable workflows in the [postrelease action][postrelease-action], however they checkout the `dev` branch during their set up as that's where we want our nightly releases to be cut from. From there, we check if the git sha is the same and only cut a new nightly if something has changed. +Nightly releases will run the action files from the `main` branch as scheduled workflows will always use the latest commit to the default branch, signified by [this comment on the nightly action file][nightly-action-comment] and the explicit branch appended to the reusable workflows in the [postrelease action][postrelease-action], however they checkout the `dev` branch during their set up as that's where we want our nightly releases to be cut from. From there, we check if the git sha is the same and only cut a new nightly if something has changed. ## End to end testing -For every release of Remix (stable, experimental, nightly, and pre-releases), we will do a complete end-to-end test of Remix apps on each of our official adapters from `create-remix`, all the way to deploying them to production. We do this by by utilizing the default [templates][templates] and the CLIs for Fly, Vercel, Netlify, and Arc. We'll then run some simple Cypress assertions to make sure everything is running properly for both development and the deployed app. +For every release of Remix (stable, experimental, nightly, and pre-releases), we will do a complete end-to-end test of Remix apps on each of our official adapters from `create-remix`, all the way to deploying them to production. We do this by utilizing the default [templates][templates] and the CLIs for Fly, Vercel, Netlify, and Arc. We'll then run some simple Cypress assertions to make sure everything is running properly for both development and the deployed app. ## Conclusion @@ -253,7 +253,7 @@ Thanks [roadmap]: https://github.com/orgs/remix-run/projects/5 [youtube]: https://www.youtube.com/c/Remix-Run/streams [discord]: https://rmx.as/discord -[contributorsyaml]: https://github.com/buzinas/remix/blob/add-support-for-module-workers/contributors.yml +[contributorsyaml]: https://github.com/remix-run/remix/blob/main/contributors.yml [cla]: https://github.com/remix-run/remix/blob/main/CLA.md [examples-repository]: https://github.com/remix-run/examples [this-page]: https://github.com/remix-run/remix From be159a6aace4b5c7e23698236ef1b3f6dc201982 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Sat, 12 Nov 2022 11:29:54 -0500 Subject: [PATCH 02/77] ci(deployment-test/cf-workers): update worker url (#4583) --- scripts/deployment-test/cf-workers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deployment-test/cf-workers.mjs b/scripts/deployment-test/cf-workers.mjs index 9a1fd119c3b..04da6bb8b9a 100644 --- a/scripts/deployment-test/cf-workers.mjs +++ b/scripts/deployment-test/cf-workers.mjs @@ -79,7 +79,7 @@ async function createAndDeployApp() { let wranglerToml = toml.parse(wranglerTomlContent); wranglerToml.name = APP_NAME; await fse.writeFile(wranglerTomlPath, toml.stringify(wranglerToml)); - let url = `https://${APP_NAME}.remix--run.workers.dev`; + let url = `https://${APP_NAME}.remixrun.workers.dev`; console.log(`worker url: ${url}`); spawnSync("npx", ["wrangler", "--version"], spawnOpts); From ad921e9c92e93e19fb1c5ce413dc7a6027fb7a3f Mon Sep 17 00:00:00 2001 From: Roj Date: Sun, 13 Nov 2022 23:42:41 +0300 Subject: [PATCH 03/77] docs(decisions): fix typos (#4591) --- contributors.yml | 1 + ...01-use-npm-to-manage-npm-dependencies-for-deno-projects.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contributors.yml b/contributors.yml index c1eb99ae3d5..e655927d0d2 100644 --- a/contributors.yml +++ b/contributors.yml @@ -363,6 +363,7 @@ - RomanSavarin - ronnylt - rossipedia +- roj1512 - RossJHagan - RossMcMillan92 - rowinbot diff --git a/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md b/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md index 46853296f58..35a6ec0a363 100644 --- a/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md +++ b/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md @@ -12,7 +12,7 @@ Deno has three ways to manage dependencies: 2. [deps.ts](https://deno.land/manual/examples/manage_dependencies) 3. [Import maps](https://deno.land/manual/linking_to_external_code/import_maps) -Additionally, NPM packages can be accessed as Deno modules via [Deno-friendly CDNs](https://deno.land/manual/node/cdns#deno-friendly-cdns) like https://esm.sh . +Additionally, NPM packages can be accessed as Deno modules via [Deno-friendly CDNs](https://deno.land/manual/node/cdns#deno-friendly-cdns) like https://esm.sh. Remix has some requirements around dependencies: @@ -78,7 +78,7 @@ Remix will not yet support import maps. ## Consequences -- URL imports will not be treeshaken +- URL imports will not be treeshaken. - Users can specify environment via the `NODE_ENV` environment variable at runtime. - Users won't have to do error-prone, manual dependency resolution. From 884d43315199caeb2887d0fa5545281b6f4ce820 Mon Sep 17 00:00:00 2001 From: "remix-cla-bot[bot]" <92060565+remix-cla-bot[bot]@users.noreply.github.com> Date: Sun, 13 Nov 2022 20:42:45 +0000 Subject: [PATCH 04/77] chore: sort contributors list --- contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index e655927d0d2..cb375ef0936 100644 --- a/contributors.yml +++ b/contributors.yml @@ -360,10 +360,10 @@ - roachjc - robindrost - roddds +- roj1512 - RomanSavarin - ronnylt - rossipedia -- roj1512 - RossJHagan - RossMcMillan92 - rowinbot From b3cd44d461df74d2df67057521ec9cc93d3bcadc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Mon, 14 Nov 2022 04:39:55 +0100 Subject: [PATCH 05/77] chore(templates): update dependencies to latest minor (#4593) --- templates/arc/package.json | 10 +++++----- templates/cloudflare-pages/package.json | 12 ++++++------ templates/cloudflare-workers/package.json | 14 +++++++------- templates/express/package.json | 16 ++++++++-------- templates/fly/package.json | 10 +++++----- templates/netlify/package.json | 10 +++++----- templates/remix/package.json | 10 +++++----- templates/vercel/package.json | 10 +++++----- 8 files changed, 46 insertions(+), 46 deletions(-) diff --git a/templates/arc/package.json b/templates/arc/package.json index 3d3673f7761..1c5af833dea 100644 --- a/templates/arc/package.json +++ b/templates/arc/package.json @@ -17,14 +17,14 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@architect/architect": "^10.3.4", + "@architect/architect": "^10.7.2", "@remix-run/dev": "*", "@remix-run/eslint-config": "*", - "@types/react": "^18.0.15", - "@types/react-dom": "^18.0.6", - "eslint": "^8.23.1", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "eslint": "^8.27.0", "npm-run-all": "^4.1.5", - "typescript": "^4.7.4" + "typescript": "^4.8.4" }, "engines": { "node": ">=14" diff --git a/templates/cloudflare-pages/package.json b/templates/cloudflare-pages/package.json index 37dce34a231..ece3441767e 100644 --- a/templates/cloudflare-pages/package.json +++ b/templates/cloudflare-pages/package.json @@ -17,15 +17,15 @@ "react-dom": "^17.0.2" }, "devDependencies": { - "@cloudflare/workers-types": "^3.14.1", + "@cloudflare/workers-types": "^3.18.0", "@remix-run/dev": "*", "@remix-run/eslint-config": "*", - "@types/react": "^17.0.47", - "@types/react-dom": "^17.0.17", - "eslint": "^8.23.1", + "@types/react": "^17.0.52", + "@types/react-dom": "^17.0.18", + "eslint": "^8.27.0", "npm-run-all": "^4.1.5", - "typescript": "^4.7.4", - "wrangler": "^2.0.27" + "typescript": "^4.8.4", + "wrangler": "^2.2.1" }, "engines": { "node": ">=16.13" diff --git a/templates/cloudflare-workers/package.json b/templates/cloudflare-workers/package.json index 89af6ff8aaa..5ec505b3237 100644 --- a/templates/cloudflare-workers/package.json +++ b/templates/cloudflare-workers/package.json @@ -18,16 +18,16 @@ "react-dom": "^17.0.2" }, "devDependencies": { - "@cloudflare/workers-types": "^3.14.1", + "@cloudflare/workers-types": "^3.18.0", "@remix-run/dev": "*", "@remix-run/eslint-config": "*", - "@types/react": "^17.0.47", - "@types/react-dom": "^17.0.17", - "eslint": "^8.23.1", - "miniflare": "^2.6.0", + "@types/react": "^17.0.52", + "@types/react-dom": "^17.0.18", + "eslint": "^8.27.0", + "miniflare": "^2.11.0", "npm-run-all": "^4.1.5", - "typescript": "^4.7.4", - "wrangler": "^2.0.22" + "typescript": "^4.8.4", + "wrangler": "^2.2.1" }, "engines": { "node": ">=16.13" diff --git a/templates/express/package.json b/templates/express/package.json index 4f76f5cc958..49be2f1ac73 100644 --- a/templates/express/package.json +++ b/templates/express/package.json @@ -14,8 +14,8 @@ "@remix-run/react": "*", "compression": "^1.7.4", "cross-env": "^7.0.3", - "express": "^4.18.1", - "isbot": "^3.5.4", + "express": "^4.18.2", + "isbot": "^3.6.5", "morgan": "^1.10.0", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -23,13 +23,13 @@ "devDependencies": { "@remix-run/dev": "*", "@remix-run/eslint-config": "*", - "@types/react": "^18.0.15", - "@types/react-dom": "^18.0.6", - "dotenv": "^16.0.2", - "eslint": "^8.23.1", - "nodemon": "^2.0.19", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "dotenv": "^16.0.3", + "eslint": "^8.27.0", + "nodemon": "^2.0.20", "npm-run-all": "^4.1.5", - "typescript": "^4.7.4" + "typescript": "^4.8.4" }, "engines": { "node": ">=14" diff --git a/templates/fly/package.json b/templates/fly/package.json index 5a85885dc49..6c831114ec3 100644 --- a/templates/fly/package.json +++ b/templates/fly/package.json @@ -11,17 +11,17 @@ "@remix-run/node": "*", "@remix-run/react": "*", "@remix-run/serve": "*", - "isbot": "^3.5.4", + "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@remix-run/dev": "*", "@remix-run/eslint-config": "*", - "@types/react": "^18.0.15", - "@types/react-dom": "^18.0.6", - "eslint": "^8.23.1", - "typescript": "^4.7.4" + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "eslint": "^8.27.0", + "typescript": "^4.8.4" }, "engines": { "node": ">=14" diff --git a/templates/netlify/package.json b/templates/netlify/package.json index 1a3a5021476..23aafd48e71 100644 --- a/templates/netlify/package.json +++ b/templates/netlify/package.json @@ -7,7 +7,7 @@ "start": "cross-env NODE_ENV=production netlify dev" }, "dependencies": { - "@netlify/functions": "^1.0.0", + "@netlify/functions": "^1.3.0", "@remix-run/netlify": "*", "@remix-run/node": "*", "@remix-run/react": "*", @@ -19,10 +19,10 @@ "@remix-run/dev": "*", "@remix-run/eslint-config": "*", "@remix-run/serve": "*", - "@types/react": "^18.0.15", - "@types/react-dom": "^18.0.6", - "eslint": "^8.23.1", - "typescript": "^4.7.4" + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "eslint": "^8.27.0", + "typescript": "^4.8.4" }, "engines": { "node": ">=14" diff --git a/templates/remix/package.json b/templates/remix/package.json index a58c9898a2f..97a88fa7e10 100644 --- a/templates/remix/package.json +++ b/templates/remix/package.json @@ -10,17 +10,17 @@ "@remix-run/node": "*", "@remix-run/react": "*", "@remix-run/serve": "*", - "isbot": "^3.5.4", + "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@remix-run/dev": "*", "@remix-run/eslint-config": "*", - "@types/react": "^18.0.15", - "@types/react-dom": "^18.0.6", - "eslint": "^8.23.1", - "typescript": "^4.7.4" + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "eslint": "^8.27.0", + "typescript": "^4.8.4" }, "engines": { "node": ">=14" diff --git a/templates/vercel/package.json b/templates/vercel/package.json index 581e21e9267..dd49a972653 100644 --- a/templates/vercel/package.json +++ b/templates/vercel/package.json @@ -9,7 +9,7 @@ "@remix-run/node": "*", "@remix-run/react": "*", "@remix-run/vercel": "*", - "@vercel/node": "^2.4.4", + "@vercel/node": "^2.6.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -17,10 +17,10 @@ "@remix-run/dev": "*", "@remix-run/eslint-config": "*", "@remix-run/serve": "*", - "@types/react": "^18.0.15", - "@types/react-dom": "^18.0.6", - "eslint": "^8.23.1", - "typescript": "^4.7.4" + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "eslint": "^8.27.0", + "typescript": "^4.8.4" }, "engines": { "node": ">=14" From 8f0f0f6e1fbad4edaeee5a69bc2177b2e260a32c Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Tue, 15 Nov 2022 04:01:07 +0900 Subject: [PATCH 06/77] chore(remix): fix typo (#4477) --- contributors.yml | 1 + packages/remix/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index cb375ef0936..5f1d4a66f39 100644 --- a/contributors.yml +++ b/contributors.yml @@ -116,6 +116,7 @@ - edmundhung - efkann - eldarshamukhamedov +- eltociear - emzoumpo - eps1lon - esamattis diff --git a/packages/remix/index.ts b/packages/remix/index.ts index 50130216a30..7c7563f2cd9 100644 --- a/packages/remix/index.ts +++ b/packages/remix/index.ts @@ -1,4 +1,4 @@ -// This class exists to prevent https://github.com/remix-run/remix/issues/2031 from occuring +// This class exists to prevent https://github.com/remix-run/remix/issues/2031 from occurring export class RemixNotSetupError extends Error { constructor() { super("Did you forget to run `remix setup` for your platform?"); From c10d6362404cc70b71fabf77a586add77b39f939 Mon Sep 17 00:00:00 2001 From: Austin Gil Date: Tue, 15 Nov 2022 17:47:01 -0800 Subject: [PATCH 07/77] docs(pages/community): add tutorial (#4283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding tutorial * Adding my name * Update docs/pages/community.md Co-authored-by: MichaΓ«l De Boey Co-authored-by: MichaΓ«l De Boey --- contributors.yml | 1 + docs/pages/community.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/contributors.yml b/contributors.yml index 5f1d4a66f39..1fb86ef67e3 100644 --- a/contributors.yml +++ b/contributors.yml @@ -43,6 +43,7 @@ - ashleyryan - ashocean - athongsavath +- AustinGil - awthwathje - axel-habermaier - BasixKOR diff --git a/docs/pages/community.md b/docs/pages/community.md index eaa06d13ce8..5c3ebf82ceb 100644 --- a/docs/pages/community.md +++ b/docs/pages/community.md @@ -49,6 +49,8 @@ In addition to the [official][official] [tutorials][tutorials], here are Remix t - [How to Debug Remix loaders and actions in VS Code][how-to-debug-remix-loaders-and-actions-in-vs-code] by [Michael Carter][michael-carter]. - [How to build a Remix website with Sanity.io and live preview][how-to-build-a-remix-website-with-sanity-io-and-live-preview] by [Simeon Griggs][simeon-griggs] +- [Build A Pet Management System With Remix, Prisma, and Postgres][build-a-pet-management-system-with-remix-prisma-and-postgres] + by [Austin Gil][austin-gil] ## Events @@ -88,6 +90,8 @@ There are Remix Meetups all over the world with thousands of members. Some onlin [michael-carter]: https://twitter.com/kiliman [how-to-build-a-remix-website-with-sanity-io-and-live-preview]: https://www.sanity.io/guides/remix-run-live-preview [simeon-griggs]: https://twitter.com/simeongriggs +[build-a-pet-management-system-with-remix-prisma-and-postgres]: https://www.youtube.com/watch?v=wqyHGQlZcws&list=PLTnRtjQN5ieYu9SdwLvzKYFVtfqySY7FT +[austin-gil]: https://twitter.com/heyAustinGil [remix-conf]: ../../../../conf [the-remix-meetup-page]: https://rmx.as/meetup [remix-guide]: https://remix.guide From 93d33107c7c0409658cfacc7931f4d6e3a5518e9 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 17 Nov 2022 13:18:26 -0800 Subject: [PATCH 08/77] chore: revert local test changes --- integration/helpers/create-fixture.ts | 19 ++++--------------- integration/playwright.config.ts | 2 +- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts index 38a3894bd52..faaea5fb672 100644 --- a/integration/helpers/create-fixture.ts +++ b/integration/helpers/create-fixture.ts @@ -144,8 +144,6 @@ export async function createAppFixture(fixture: Fixture, mode?: ServerMode) { //////////////////////////////////////////////////////////////////////////////// -const symlinks = new Map(); - export async function createFixtureProject( init: FixtureInit = {} ): Promise { @@ -156,19 +154,10 @@ export async function createFixtureProject( await fse.ensureDir(projectDir); await fse.copy(integrationTemplateDir, projectDir); - - await fse.ensureDir(path.join(projectDir, "node_modules")); - await fse.symlink( - path.join(__dirname, "../../build/node_modules/@remix-run"), - path.join(projectDir, "node_modules/@remix-run") - ); - await fse.symlink( - path.join(__dirname, "../../build/node_modules/create-remix"), - path.join(projectDir, "node_modules/create-remix") - ); - await fse.symlink( - path.join(__dirname, "../../build/node_modules/remix"), - path.join(projectDir, "node_modules/remix") + await fse.copy( + path.join(__dirname, "../../build/node_modules"), + path.join(projectDir, "node_modules"), + { overwrite: true } ); if (init.setup) { diff --git a/integration/playwright.config.ts b/integration/playwright.config.ts index e66a2c1c019..60d88cbff39 100644 --- a/integration/playwright.config.ts +++ b/integration/playwright.config.ts @@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = { timeout: 5_000, }, forbidOnly: !!process.env.CI, - retries: 0, + retries: 3, reporter: process.env.CI ? "github" : [["html", { open: "never" }]], use: { actionTimeout: 0 }, From 5d1bbdf039ea49a44d06edea8dac67e5c90fc9b0 Mon Sep 17 00:00:00 2001 From: Manish Jangir Date: Fri, 18 Nov 2022 13:44:11 +0530 Subject: [PATCH 09/77] docs(guides/data-writes): Fix a couple typos (#4625) * Fix typos in the docs * Added git user in contributors.yaml * Update docs/guides/data-writes.md Co-authored-by: Mehdi Achour --- contributors.yml | 1 + docs/guides/data-writes.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contributors.yml b/contributors.yml index 1fb86ef67e3..9228e0c2436 100644 --- a/contributors.yml +++ b/contributors.yml @@ -452,3 +452,4 @@ - zachdtaylor - zainfathoni - zhe +- mjangir diff --git a/docs/guides/data-writes.md b/docs/guides/data-writes.md index 547965e984d..eb03a5facf3 100644 --- a/docs/guides/data-writes.md +++ b/docs/guides/data-writes.md @@ -23,9 +23,9 @@ This guide only covers `
`. We suggest you read the docs for the other two ## Plain HTML Forms -After teaching workshops with our company React Training for years, we've learned that a lot of newer web developers (through no fault of their own) don't actually know how `` works! +After teaching workshops with our company React Training for years, we've learned that a lot of newer web developers (though no fault of their own) don't actually know how `` works! -Since Remix `` works identically to `` (with a couple extra goodies for optimistic UI etc.), we're going to brush up on plain ol' HTML forms, so you can learn both HTML and Remix at the same time. +Since Remix `` works identically to `` (with a couple of extra goodies for optimistic UI etc.), we're going to brush up on plain ol' HTML forms, so you can learn both HTML and Remix at the same time. ### HTML Form HTTP Verbs From 41f921b33feff976b54b5fe92d34618a45fe7a04 Mon Sep 17 00:00:00 2001 From: "remix-cla-bot[bot]" <92060565+remix-cla-bot[bot]@users.noreply.github.com> Date: Fri, 18 Nov 2022 08:14:14 +0000 Subject: [PATCH 10/77] chore: sort contributors list --- contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index 9228e0c2436..fc860ea2850 100644 --- a/contributors.yml +++ b/contributors.yml @@ -306,6 +306,7 @@ - mikeybinnswebdesign - mirzafaizan - mjackson +- mjangir - mkrtchian - mochi-sann - mohammadhosseinbagheri @@ -452,4 +453,3 @@ - zachdtaylor - zainfathoni - zhe -- mjangir From 2248669ed59fd716e267ea41df5d665d4781f4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Sun, 20 Nov 2022 21:20:28 +0100 Subject: [PATCH 11/77] chore: fix `postrelease` workflow (#4596) --- .github/workflows/postrelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/postrelease.yml b/.github/workflows/postrelease.yml index b7c09ade633..dcd1168a13e 100644 --- a/.github/workflows/postrelease.yml +++ b/.github/workflows/postrelease.yml @@ -4,7 +4,7 @@ on: push: tags: # only run on `remix` tags - - "remix@*" + - remix@** jobs: comment: From 080e5f508db98392895703e25b2df11133b630ad Mon Sep 17 00:00:00 2001 From: "Khoa Chau (Finn)" <70827148+chaukhoa97@users.noreply.github.com> Date: Sat, 26 Nov 2022 01:10:18 +0700 Subject: [PATCH 12/77] docs(tutorials/jokes): throw 403 instead of 401 if user is not the owner (#4688) --- contributors.yml | 1 + docs/tutorials/jokes.md | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contributors.yml b/contributors.yml index fc860ea2850..14be49ef5b6 100644 --- a/contributors.yml +++ b/contributors.yml @@ -453,3 +453,4 @@ - zachdtaylor - zainfathoni - zhe +- chaukhoa97 diff --git a/docs/tutorials/jokes.md b/docs/tutorials/jokes.md index dba05ae5d7b..b7f773e9444 100644 --- a/docs/tutorials/jokes.md +++ b/docs/tutorials/jokes.md @@ -4631,7 +4631,7 @@ Awesome! We're ready to handle errors and it didn't complicate our happy path on Oh, and don't you love how just like with the `ErrorBoundary`, it's all contextual? So the rest of the app continues to function just as well. Another point for user experience πŸ’ͺ -You know what, while we're adding catch boundaries. Why don't we improve the `app/routes/jokes/$jokeId.tsx` route a bit by allowing users to delete the joke if they own it. If they don't, we can give them a 401 error in the catch boundary. +You know what, while we're adding catch boundaries. Why don't we improve the `app/routes/jokes/$jokeId.tsx` route a bit by allowing users to delete the joke if they own it. If they don't, we can give them a 403 error in the catch boundary. One thing to keep in mind with `delete` is that HTML forms only support `method="get"` and `method="post"`. They don't support `method="delete"`. So to make sure our form will work with and without JavaScript, it's a good idea to do something like this: @@ -4708,7 +4708,7 @@ export const action: ActionFunction = async ({ throw new Response( "Pssh, nice try. That's not your joke", { - status: 401, + status: 403, } ); } @@ -4756,7 +4756,7 @@ export function CatchBoundary() { ); } - case 401: { + case 403: { return (
Sorry, but {params.jokeId} is not your joke. @@ -4852,7 +4852,7 @@ export const action: ActionFunction = async ({ throw new Response( "Pssh, nice try. That's not your joke", { - status: 401, + status: 403, } ); } @@ -4902,7 +4902,7 @@ export function CatchBoundary() {
); } - case 401: { + case 403: { return (
Sorry, but {params.jokeId} is not your joke. From 8f7100b2d5c40d5b2f0856958898cab70ef9d407 Mon Sep 17 00:00:00 2001 From: "remix-cla-bot[bot]" <92060565+remix-cla-bot[bot]@users.noreply.github.com> Date: Fri, 25 Nov 2022 18:10:21 +0000 Subject: [PATCH 13/77] chore: sort contributors list --- contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index 14be49ef5b6..e60b26f7779 100644 --- a/contributors.yml +++ b/contributors.yml @@ -65,6 +65,7 @@ - CanRau - ccssmnn - chaance +- chaukhoa97 - chenc041 - chenxsan - chiangs @@ -453,4 +454,3 @@ - zachdtaylor - zainfathoni - zhe -- chaukhoa97 From 65cba48640ebc377535012057e1298565565d06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Wed, 30 Nov 2022 22:28:57 +0100 Subject: [PATCH 14/77] docs: fix `useActionData`/`useLoaderData` usage (#4684) --- docs/api/conventions.md | 129 ++--- docs/api/remix.md | 168 +++--- docs/guides/api-routes.md | 6 +- docs/guides/bff.md | 5 +- docs/guides/constraints.md | 22 +- docs/guides/data-loading.md | 80 ++- docs/guides/data-writes.md | 33 +- docs/guides/envvars.md | 6 +- docs/guides/mdx.md | 7 +- docs/guides/not-found.md | 7 +- docs/guides/optimistic-ui.md | 35 +- docs/guides/resource-routes.md | 32 +- docs/guides/routing.md | 6 +- docs/guides/styling.md | 7 +- docs/other-api/serve.md | 10 +- docs/pages/faq.md | 18 +- docs/pages/philosophy.md | 22 +- docs/pages/technical-explanation.md | 14 +- docs/tutorials/blog.md | 228 ++------ docs/tutorials/jokes.md | 814 ++++++++++++---------------- 20 files changed, 665 insertions(+), 984 deletions(-) diff --git a/docs/api/conventions.md b/docs/api/conventions.md index b8686a667e2..60d2eacc938 100644 --- a/docs/api/conventions.md +++ b/docs/api/conventions.md @@ -265,21 +265,17 @@ For example: `app/routes/blog/$postId.tsx` will match the following URLs: On each of these pages, the dynamic segment of the URL path is the value of the parameter. There can be multiple parameters active at any time (as in `/dashboard/:client/invoices/:invoiceId` [view example app][view-example-app]) and all parameters can be accessed within components via [`useParams`][use-params] and within loaders/actions via the argument's [`params`][params] property: ```tsx filename=app/routes/blog/$postId.tsx -import { useParams } from "@remix-run/react"; import type { - LoaderFunction, - ActionFunction, + ActionArgs, + LoaderArgs, } from "@remix-run/node"; // or cloudflare/deno +import { useParams } from "@remix-run/react"; -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { console.log(params.postId); }; -export const action: ActionFunction = async ({ - params, -}) => { +export const action = async ({ params }: ActionArgs) => { console.log(params.postId); }; @@ -428,21 +424,17 @@ Files that are named `$.tsx` are called "splat" (or "catch-all") routes. These r Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key. ```tsx filename=app/routes/$.tsx -import { useParams } from "@remix-run/react"; import type { - LoaderFunction, - ActionFunction, + ActionArgs, + LoaderArgs, } from "@remix-run/node"; // or cloudflare/deno +import { useParams } from "@remix-run/react"; -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { console.log(params["*"]); }; -export const action: ActionFunction = async ({ - params, -}) => { +export const action = async ({ params }: ActionArgs) => { console.log(params["*"]); }; @@ -554,7 +546,7 @@ export default function SomeRouteComponent() { Each route can define a "loader" function that will be called on the server before rendering to provide data to the route. You may think of this as a "GET" request handler in that you should not be reading the body of the request; that is the job of an [`action`][action]. -```js +```tsx import { json } from "@remix-run/node"; // or cloudflare/deno export const loader = async () => { @@ -564,16 +556,6 @@ export const loader = async () => { }; ``` -```ts -// Typescript -import { json } from "@remix-run/node"; // or cloudflare/deno -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno - -export const loader: LoaderFunction = async () => { - return json({ ok: true }); -}; -``` - This function is only ever run on the server. On the initial server render it will provide data to the HTML document. On navigations in the browser, Remix will call the function via [`fetch`][fetch]. This means you can talk directly to your database, use server only API secrets, etc. Any code that isn't used to render the UI will be removed from the browser bundle. Using the database ORM Prisma as an example: @@ -589,7 +571,7 @@ export const loader = async () => { }; export default function Users() { - const data = useLoaderData(); + const data = useLoaderData(); return (
    {data.map((user) => ( @@ -608,11 +590,9 @@ Remix polyfills the [Web Fetch API][fetch] on the server so you can use `fetch` Route params are passed to your loader. If you have a loader at `data/invoices/$invoiceId.tsx` then Remix will parse out the `invoiceId` and pass it to your loader. This is useful for fetching data from an API or database. -```ts +```tsx // if the user visits /invoices/123 -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { params.invoiceId; // "123" }; ``` @@ -624,9 +604,7 @@ This is a [Fetch Request][request] instance with information about the request. Most common cases are reading headers or the URL. You can also use this to read URL [URLSearchParams][urlsearchparams] from the request like so: ```tsx -export const loader: LoaderFunction = async ({ - request, -}) => { +export const loader = async ({ request }: LoaderArgs) => { // read a cookie const cookie = request.headers.get("Cookie"); @@ -662,10 +640,8 @@ app.all( And then your loader can access it. -```ts filename=routes/some-route.tsx -export const loader: LoaderFunction = async ({ - context, -}) => { +```tsx +export const loader = async ({ context }: LoaderArgs) => { const { expressUser } = context; // ... }; @@ -675,8 +651,8 @@ export const loader: LoaderFunction = async ({ You need to return a [Fetch Response][response] from your loader. -```ts -export const loader: LoaderFunction = async () => { +```tsx +export const loader = async () => { const users = await db.users.findMany(); const body = JSON.stringify(users); return new Response(body, { @@ -692,7 +668,7 @@ Using the `json` helper simplifies this so you don't have to construct them your ```tsx import { json } from "@remix-run/node"; // or cloudflare/deno -export const loader: LoaderFunction = async () => { +export const loader = async () => { const users = await fakeDb.users.findMany(); return json(users); }; @@ -701,11 +677,10 @@ export const loader: LoaderFunction = async () => { You can see how `json` just does a little of the work to make your loader a lot cleaner. You can also use the `json` helper to add headers or a status code to your response: ```tsx +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { const user = await fakeDb.project.findOne({ where: { id: params.id }, }); @@ -766,15 +741,14 @@ export async function requireUserSession(request) { ``` ```tsx filename=app/routes/invoice/$invoiceId.tsx -import { useCatch, useLoaderData } from "@remix-run/react"; +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno +import { json } from "@remix-run/node"; // or cloudflare/deno import type { ThrownResponse } from "@remix-run/react"; +import { useCatch, useLoaderData } from "@remix-run/react"; import { requireUserSession } from "~/http"; import { getInvoice } from "~/db"; -import type { - Invoice, - InvoiceNotFoundResponse, -} from "~/db"; +import type { InvoiceNotFoundResponse } from "~/db"; type InvoiceCatchData = { invoiceOwnerEmail: string; @@ -784,22 +758,25 @@ type ThrownResponses = | InvoiceNotFoundResponse | ThrownResponse<401, InvoiceCatchData>; -export const loader = async ({ request, params }) => { +export const loader = async ({ + params, + request, +}: LoaderArgs) => { const user = await requireUserSession(request); - const invoice: Invoice = getInvoice(params.invoiceId); + const invoice = getInvoice(params.invoiceId); if (!invoice.userIds.includes(user.id)) { - const data: InvoiceCatchData = { - invoiceOwnerEmail: invoice.owner.email, - }; - throw json(data, { status: 401 }); + throw json( + { invoiceOwnerEmail: invoice.owner.email }, + { status: 401 } + ); } return json(invoice); }; export default function InvoiceRoute() { - const invoice = useLoaderData(); + const invoice = useLoaderData(); return ; } @@ -844,6 +821,7 @@ Actions have the same API as loaders, the only difference is when they are calle This enables you to co-locate everything about a data set in a single route module: the data read, the component that renders the data, and the data writes: ```tsx +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno import { Form } from "@remix-run/react"; @@ -854,7 +832,7 @@ export async function loader() { return json(await fakeGetTodos()); } -export async function action({ request }) { +export async function action({ request }: ActionArgs) { const body = await request.formData(); const todo = await fakeCreateTodo({ title: body.get("title"), @@ -863,7 +841,7 @@ export async function action({ request }) { } export default function Todos() { - const data = useLoaderData(); + const data = useLoaderData(); return (
    @@ -1093,16 +1071,14 @@ export const meta: MetaFunction = ({ To infer types for `parentsData`, provide a mapping from the route's file path (relative to `app/`) to that route loader type: -```tsx -// app/routes/sales.tsx +```tsx filename=app/routes/sales.tsx const loader = () => { return json({ salesCount: 1074 }); }; -export type Loader = typeof loader; ``` ```tsx -import type { Loader as SalesLoader } from "../../sales"; +import type { loader as salesLoader } from "../../sales"; const loader = () => { return json({ name: "Customer name" }); @@ -1110,9 +1086,7 @@ const loader = () => { const meta: MetaFunction< typeof loader, - { - "routes/sales": SalesLoader; - } + { "routes/sales": typeof salesLoader } > = ({ data, parentsData }) => { const { name } = data; // ^? string @@ -1162,7 +1136,7 @@ Examples: ```tsx import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno -import stylesHref from "../styles/something.css"; +import stylesHref from "~/styles/something.css"; export const links: LinksFunction = () => { return [ @@ -1208,7 +1182,7 @@ export const links: LinksFunction = () => { These descriptors allow you to prefetch the resources for a page the user is likely to navigate to. While this API is useful, you might get more mileage out of `` instead. But if you'd like, you can get the same behavior with this API. -```js +```tsx export function links() { return [{ page: "/posts/public" }]; } @@ -1273,7 +1247,7 @@ export function ErrorBoundary({ error }) { Exporting a handle allows you to create application conventions with the `useMatches()` hook. You can put whatever values you want on it: -```js +```tsx export const handle = { its: "all yours", }; @@ -1289,7 +1263,7 @@ This is almost always used on conjunction with `useMatches`. To see what kinds o This function lets apps optimize which routes should be reloaded on some client-side transitions. -```ts +```tsx import type { ShouldReloadFunction } from "@remix-run/react"; export const unstable_shouldReload: ShouldReloadFunction = @@ -1327,7 +1301,7 @@ Here are a couple of common use-cases: It's common for root loaders to return data that never changes, like environment variables to be sent to the client app. In these cases you never need the root loader to be called again. For this case, you can simply `return false`. -```js [10] +```tsx lines=[10] export const loader = async () => { return json({ ENV: { @@ -1372,8 +1346,11 @@ And lets say the UI looks something like this: The `activity.tsx` loader can use the search params to filter the list, so visiting a URL like `/projects/design-revamp/activity?search=image` could filter the list of results. Maybe it looks something like this: -```js [2,8] -export async function loader({ request, params }) { +```tsx lines=[5,11] +export async function loader({ + params, + request, +}: LoaderArgs) { const url = new URL(request.url); return json( await exampleDb.activity.findAll({ @@ -1393,7 +1370,7 @@ This is great for the activity route, but Remix doesn't know if the parent loade In this UI, that's wasted bandwidth for the user, your server, and your database because `$projectId.tsx` doesn't use the search params. Consider that our loader for `$projectId.tsx` looks something like this: ```tsx -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { return json(await fakedb.findProject(params.projectId)); } ``` diff --git a/docs/api/remix.md b/docs/api/remix.md index a70385b6ae9..6d8f902efc8 100644 --- a/docs/api/remix.md +++ b/docs/api/remix.md @@ -340,7 +340,7 @@ export async function loader() { } export default function Invoices() { - const invoices = useLoaderData(); + const invoices = useLoaderData(); // ... } ``` @@ -349,18 +349,19 @@ export default function Invoices() { This hook returns the JSON parsed data from your route action. It returns `undefined` if there hasn't been a submission at the current location yet. -```tsx lines=[2,11,20] +```tsx lines=[3,12,21] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useActionData, Form } from "@remix-run/react"; -export async function action({ request }) { +export async function action({ request }: ActionArgs) { const body = await request.formData(); const name = body.get("visitorsName"); return json({ message: `Hello, ${name}` }); } export default function Invoices() { - const data = useActionData(); + const data = useActionData(); return (

    @@ -377,11 +378,12 @@ export default function Invoices() { The most common use-case for this hook is form validation errors. If the form isn't right, you can simply return the errors and let the user try again (instead of pushing all the errors into sessions and back out of the loader). -```tsx lines=[22, 31, 39-41, 45-47] +```tsx lines=[23,32,40-42,46-48] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect, json } from "@remix-run/node"; // or cloudflare/deno import { Form, useActionData } from "@remix-run/react"; -export async function action({ request }) { +export async function action({ request }: ActionArgs) { const form = await request.formData(); const email = form.get("email"); const password = form.get("password"); @@ -408,7 +410,7 @@ export async function action({ request }) { } export default function Signup() { - const errors = useActionData(); + const errors = useActionData(); return ( <> @@ -522,7 +524,8 @@ Returns the function that may be used to submit a `` (or some raw `FormDat This is useful whenever you need to programmatically submit a form. For example, you may wish to save a user preferences form whenever any field changes. -```tsx filename=app/routes/prefs.tsx lines=[2,14,18] +```tsx filename=app/routes/prefs.tsx lines=[3,15,19] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useSubmit, useTransition } from "@remix-run/react"; @@ -530,7 +533,7 @@ export async function loader() { return json(await getUserPreferences()); } -export async function action({ request }) { +export async function action({ request }: ActionArgs) { await updatePreferences(await request.formData()); return redirect("/prefs"); } @@ -909,7 +912,7 @@ See also: Perhaps you have a persistent newsletter signup at the bottom of every page on your site. This is not a navigation event, so useFetcher is perfect for the job. First, you create a Resource Route: ```tsx filename=routes/newsletter/subscribe.tsx -export async function action({ request }) { +export async function action({ request }: ActionArgs) { const email = (await request.formData()).get("email"); try { await subscribe(email); @@ -970,12 +973,12 @@ Because `useFetcher` doesn't cause a navigation, it won't automatically work if If you want to support a no JavaScript experience, just export a component from the route with the action. ```tsx filename=routes/newsletter/subscribe.tsx -export async function action({ request }) { +export async function action({ request }: ActionArgs) { // just like before } export default function NewsletterSignupRoute() { - const newsletter = useActionData(); + const newsletter = useActionData(); return (

    @@ -1033,7 +1036,7 @@ import { Form } from "@remix-run/react"; import { NewsletterForm } from "~/NewsletterSignup"; export default function NewsletterSignupRoute() { - const data = useActionData(); + const data = useActionData(); return ( { +export const loader = async () => { // So you can write this: return json({ any: "thing" }); @@ -1451,7 +1453,7 @@ export const loader: LoaderFunction = async () => { You can also pass a status code and headers: ```ts lines=[4-9] -export const loader: LoaderFunction = async () => { +export const loader = async () => { return json( { not: "coffee" }, { @@ -1468,11 +1470,10 @@ export const loader: LoaderFunction = async () => { This is shortcut for sending 30x responses. -```ts lines=[2,8] -import type { ActionFunction } from "@remix-run/node"; // or cloudflare/deno +```ts lines=[1,7] import { redirect } from "@remix-run/node"; // or cloudflare/deno -export const action: ActionFunction = async () => { +export const action = async () => { const userSession = await getUserSessionOrWhatever(); if (!userSession) { @@ -1538,9 +1539,7 @@ It's to be used in place of `request.formData()`. For example: ```tsx lines=[4-7,9,25] -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const formData = await unstable_parseMultipartFormData( request, uploadHandler // <-- we'll look at this deeper next @@ -1587,9 +1586,7 @@ An upload handler that will write parts with a filename to disk to keep them out **Example:** ```tsx -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const uploadHandler = unstable_composeUploadHandlers( unstable_createFileUploadHandler({ maxPartSize: 5_000_000, @@ -1629,9 +1626,7 @@ The `filter` function accepts an `object` and returns a `boolean` (or a promise **Example:** ```tsx -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const uploadHandler = unstable_createMemoryUploadHandler({ maxPartSize: 500_000, }); @@ -1656,11 +1651,14 @@ Most of the time, you'll probably want to proxy the file to a file host. **Example:** ```tsx -import type { UploadHandler } from "@remix-run/{runtime}"; +import type { + ActionArgs, + UploadHandler, +} from "@remix-run/node"; // or cloudflare/deno import { unstable_composeUploadHandlers, unstable_createMemoryUploadHandler, -} from "@remix-run/{runtime}"; +} from "@remix-run/node"; // or cloudflare/deno // writeAsyncIterableToWritable is a Node-only utility import { writeAsyncIterableToWritable } from "@remix-run/node"; import type { @@ -1698,9 +1696,7 @@ async function uploadImageToCloudinary( return uploadPromise; } -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const userId = getUserId(request); const uploadHandler = unstable_composeUploadHandlers( @@ -1798,20 +1794,24 @@ Then, you can `import` the cookie and use it in your `loader` and/or `action`. T **Note:** We recommend (for now) that you create all the cookies your app needs in `app/cookies.js` and `import` them into your route modules. This allows the Remix compiler to correctly prune these imports out of the browser build where they are not needed. We hope to eventually remove this caveat. -```tsx filename=app/routes/index.tsx lines=[4,8-9,15-16,20] +```tsx filename=app/routes/index.tsx lines=[8,12-13,19-10,24] +import type { + ActionArgs, + LoaderArgs, +} from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; import { userPrefs } from "~/cookies"; -export async function loader({ request }) { +export const loader = async ({ params }: LoaderArgs) => { const cookieHeader = request.headers.get("Cookie"); const cookie = (await userPrefs.parse(cookieHeader)) || {}; return json({ showBanner: cookie.showBanner }); -} +}; -export async function action({ request }) { +export const action = async ({ request }: ActionArgs) => { const cookieHeader = request.headers.get("Cookie"); const cookie = (await userPrefs.parse(cookieHeader)) || {}; @@ -1826,10 +1826,10 @@ export async function action({ request }) { "Set-Cookie": await userPrefs.serialize(cookie), }, }); -} +}; export default function Home() { - const { showBanner } = useLoaderData(); + const { showBanner } = useLoaderData(); return (

    @@ -1893,14 +1893,16 @@ Cookies that have one or more `secrets` will be stored and verified in a way tha Secrets may be rotated by adding new secrets to the front of the `secrets` array. Cookies that have been signed with old secrets will still be decoded successfully in `cookie.parse()`, and the newest secret (the first one in the array) will always be used to sign outgoing cookies created in `cookie.serialize()`. -```js -// app/cookies.js -const cookie = createCookie("user-prefs", { +```ts filename=app/cookies.ts +export const cookie = createCookie("user-prefs", { secrets: ["n3wsecr3t", "olds3cret"], }); +``` + +```ts filename=app/routes/route.tsx +import { cookie } from "~/cookies"; -// in your route module... -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const oldCookie = request.headers.get("Cookie"); // oldCookie may have been signed with "olds3cret", but still parses ok const value = await cookie.parse(oldCookie); @@ -2067,13 +2069,17 @@ You'll use methods to get access to sessions in your `loader` and `action` funct A login form might look something like this: -```tsx filename=app/routes/login.js lines=[4,7-9,11,16,20,26-28,39,44,49,54] +```tsx filename=app/routes/login.js lines=[8,11-13,15,20,24,30-32,43,48,53,58] +import type { + ActionArgs, + LoaderArgs, +} from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; import { getSession, commitSession } from "../sessions"; -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const session = await getSession( request.headers.get("Cookie") ); @@ -2092,7 +2098,7 @@ export async function loader({ request }) { }); } -export async function action({ request }) { +export async function action({ request }: ActionArgs) { const session = await getSession( request.headers.get("Cookie") ); @@ -2127,7 +2133,8 @@ export async function action({ request }) { } export default function Login() { - const { currentUser, error } = useLoaderData(); + const { currentUser, error } = + useLoaderData(); return (
    @@ -2154,9 +2161,7 @@ And then a logout form might look something like this: ```tsx import { getSession, destroySession } from "../sessions"; -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const session = await getSession( request.headers.get("Cookie") ); @@ -2412,8 +2417,8 @@ export { getSession, commitSession, destroySession }; After retrieving a session with `getSession`, the session object returned has a handful of methods and properties: -```js -export async function action({ request }) { +```tsx +export async function action({ request }: ActionArgs) { const session = await getSession( request.headers.get("Cookie") ); @@ -2443,10 +2448,15 @@ session.set("userId", "1234"); Sets a session value that will be unset the first time it is read. After that, it's gone. Most useful for "flash messages" and server-side form validation messages: -```js +```tsx +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno + import { getSession, commitSession } from "../sessions"; -export async function action({ request, params }) { +export async function action({ + params, + request, +}: ActionArgs) { const session = await getSession( request.headers.get("Cookie") ); @@ -2471,7 +2481,8 @@ Now we can read the message in a loader. You must commit the session whenever you read a `flash`. This is different than you might be used to where some type of middleware automatically sets the cookie header for you. -```jsx +```tsx +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { Meta, @@ -2482,7 +2493,7 @@ import { import { getSession, commitSession } from "./sessions"; -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const session = await getSession( request.headers.get("Cookie") ); @@ -2500,7 +2511,7 @@ export async function loader({ request }) { } export default function App() { - const { message } = useLoaderData(); + const { message } = useLoaderData(); return ( @@ -2538,8 +2549,8 @@ session.unset("name"); When using cookieSessionStorage, you must commit the session whenever you `unset` -```js -export async function loader({ request }) { +```tsx +export async function loader({ request }: LoaderArgs) { // ... return json(data, { @@ -2572,18 +2583,12 @@ import { AccordionPanel, } from "@reach/accordion"; -import type { Companies } from "~/utils/companies"; import { getCompanies } from "~/utils/companies"; -type LoaderData = { - companies: Array; -}; - -export const loader: LoaderFunction = async () => { - const data: LoaderData = { +export const loader = async () => { + return json({ companies: await getCompanies(), - }; - return json(data); + }); }; type Sort = "ASC" | "DESC"; @@ -2592,7 +2597,7 @@ export type ContextType = { }; export default function CompaniesRoute() { - const data = useLoaderData(); + const data = useLoaderData(); const [invoiceSort, setInvoiceSort] = React.useState("ASC"); @@ -2644,7 +2649,7 @@ This hook returns the context from the `` that rendered you. Continuing from the `` example above, here's what the child route could do to use the sort order. ```tsx filename=app/routes/companies/$companyId.tsx lines=[5,8,25,27-30] -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData, @@ -2653,21 +2658,14 @@ import { import type { ContextType } from "../companies"; -type LoaderData = { - company: Company; -}; - -export const loader: LoaderFunction = async ({ - params, -}) => { - const data: LoaderData = { +export const loader = async ({ params }: LoaderArgs) => { + return json({ company: await getCompany(params.companyId), - }; - return json(data); + }); }; export default function CompanyRoute() { - const data = useLoaderData(); + const data = useLoaderData(); const { invoiceSort } = useOutletContext(); const sortedInvoices = diff --git a/docs/guides/api-routes.md b/docs/guides/api-routes.md index 4d5d8f94bf6..789bedd010c 100644 --- a/docs/guides/api-routes.md +++ b/docs/guides/api-routes.md @@ -18,7 +18,7 @@ export async function loader() { } export default function Teams() { - return ; + return ()} />; } ``` @@ -33,7 +33,7 @@ You can `useFetcher` for cases like this. And once again, since Remix in the bro For example, you could have a route to handle the search: ```tsx filename=routes/city-search.tsx -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const url = new URL(request.url); return json( await searchCities(url.searchParams.get("q")) @@ -91,7 +91,7 @@ function CitySearchCombobox() { In other cases, you may need routes that are part of your application, but aren't part of your application's UI. Maybe you want a loader that renders a report as a PDF: ```tsx -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { const report = await getReport(params.id); const pdf = await generateReportPDF(report); return new Response(pdf, { diff --git a/docs/guides/bff.md b/docs/guides/bff.md index 168ca133a6b..5d7bf3fa10a 100644 --- a/docs/guides/bff.md +++ b/docs/guides/bff.md @@ -12,11 +12,12 @@ Mature apps already have a lot of backend application code in Ruby, Elixir, PHP, Because Remix polyfills the Web Fetch API, you can use `fetch` right from your loaders and actions to your backend. -```jsx lines=[8,14,18] +```tsx lines=[9,15,19] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import escapeHtml from "escape-html"; -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { let apiUrl = "http://api.example.com/some-data.json"; let res = await fetch(apiUrl, { headers: { diff --git a/docs/guides/constraints.md b/docs/guides/constraints.md index 36e2d824c0e..d217c613551 100644 --- a/docs/guides/constraints.md +++ b/docs/guides/constraints.md @@ -34,7 +34,7 @@ export function meta() { } export default function Posts() { - const posts = useLoaderData(); + const posts = useLoaderData(); return ; } ``` @@ -59,7 +59,7 @@ export function meta() { } export default function Posts() { - const posts = useLoaderData(); + const posts = useLoaderData(); return ; } ``` @@ -94,7 +94,7 @@ export function meta() { } export default function Posts() { - const posts = useLoaderData(); + const posts = useLoaderData(); return ; } ``` @@ -114,7 +114,7 @@ export function meta() { } export default function Posts() { - const posts = useLoaderData(); + const posts = useLoaderData(); return ; } ``` @@ -140,7 +140,7 @@ export function meta() { } export default function Posts() { - const posts = useLoaderData(); + const posts = useLoaderData(); return ; } ``` @@ -201,12 +201,12 @@ export function removeTrailingSlash(url) { And then use it like this: -```js bad filename=app/root.js +```tsx bad filename=app/root.tsx import { json } from "@remix-run/node"; // or cloudflare/deno import { removeTrailingSlash } from "~/http"; -export const loader = async ({ request }) => { +export const loader = async ({ request }: LoaderArgs) => { removeTrailingSlash(request.url); return json({ some: "data" }); }; @@ -214,9 +214,9 @@ export const loader = async ({ request }) => { It reads much nicer as well when you've got a lot of these: -```ts +```tsx // this -export const loader = async ({ request }) => { +export const loader = async ({ request }: LoaderArgs) => { return removeTrailingSlash(request.url, () => { return withSession(request, (session) => { return requireUser(session, (user) => { @@ -227,9 +227,9 @@ export const loader = async ({ request }) => { }; ``` -```ts +```tsx // vs. this -export const loader = async ({ request }) => { +export const loader = async ({ request }: LoaderArgs) => { removeTrailingSlash(request.url); const session = await getSession(request); const user = await requireUser(session); diff --git a/docs/guides/data-loading.md b/docs/guides/data-loading.md index 5ac288d7cbd..d56153e0ccb 100644 --- a/docs/guides/data-loading.md +++ b/docs/guides/data-loading.md @@ -21,12 +21,11 @@ One of the primary features of Remix is simplifying interactions with the server Each [route module][route-module] can export a component and a [`loader`][loader]. [`useLoaderData`][useloaderdata] will provide the loader's data to your component: -```tsx filename=app/routes/products.tsx lines=[1-3,5-10,13] -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno +```tsx filename=app/routes/products.tsx lines=[1-2,4-9,12] import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; -export const loader: LoaderFunction = async () => { +export const loader = async () => { return json([ { id: "1", name: "Pants" }, { id: "2", name: "Jacket" }, @@ -34,7 +33,7 @@ export const loader: LoaderFunction = async () => { }; export default function Products() { - const products = useLoaderData(); + const products = useLoaderData(); return (

    Products

    @@ -55,11 +54,9 @@ If your server side modules end up in client bundles, move the imports for those When you name a file with `$` like `routes/users/$userId.tsx` and `routes/users/$userId/projects/$projectId.tsx` the dynamic segments (the ones starting with `$`) will be parsed from the URL and passed to your loader on a `params` object. ```tsx filename=routes/users/$userId/projects/$projectId.tsx -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { console.log(params.userId); console.log(params.projectId); }; @@ -75,12 +72,10 @@ Given the following URLs, the params would be parsed as follows: These params are most useful for looking up data: ```tsx filename=routes/users/$userId/projects/$projectId.tsx lines=[8,9] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { return json( await fakeDb.project.findMany({ where: { @@ -96,13 +91,11 @@ export const loader: LoaderFunction = async ({ Because these params come from the URL and not your source code, you can't know for sure if they will be defined. That's why the types on the param's keys are `string | undefined`. It's good practice to validate before using them, especially in TypeScript to get type safety. Using `invariant` makes it easy. -```tsx filename=routes/users/$userId/projects/$projectId.tsx lines=[1,7-8] +```tsx filename=routes/users/$userId/projects/$projectId.tsx lines=[2,5-6] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import invariant from "tiny-invariant"; -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { invariant(params.userId, "Expected params.userId"); invariant(params.projectId, "Expected params.projectId"); @@ -126,7 +119,7 @@ export async function loader() { } export default function GistsRoute() { - const gists = useLoaderData(); + const gists = useLoaderData(); return (
      {gists.map((gist) => ( @@ -154,15 +147,13 @@ export { db }; And then your routes can import it and make queries against it: ```tsx filename=app/routes/products/$categoryId.tsx -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; import { db } from "~/db.server"; -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { return json( await db.product.findMany({ where: { @@ -173,7 +164,7 @@ export const loader: LoaderFunction = async ({ }; export default function ProductCategory() { - const products = useLoaderData(); + const products = useLoaderData(); return (

      {products.length} Products

      @@ -185,15 +176,13 @@ export default function ProductCategory() { If you are using TypeScript, you can use type inference to use Prisma Client generated types on when calling `useLoaderData`. This allows better type safety and intellisense when writing your code that uses the loaded data. -```tsx filename=tsx filename=app/routes/products/$productId.tsx -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno +```tsx filename=app/routes/products/$productId.tsx +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; import { db } from "~/db.server"; -type LoaderData = Awaited>; - async function getLoaderData(productId: string) { const product = await db.product.findUnique({ where: { @@ -209,16 +198,12 @@ async function getLoaderData(productId: string) { return product; } -export const loader: LoaderFunction = async ({ - params, -}) => { - return json( - await getLoaderData(params.productId) - ); +export const loader = async ({ params }: LoaderArgs) => { + return json(await getLoaderData(params.productId)); }; export default function Product() { - const product = useLoaderData(); + const product = useLoaderData(); return (

      Product {product.id}

      @@ -243,14 +228,14 @@ For the Cloudflare Workers environment you'll need to [do some other configurati This enables you to use the `PRODUCTS_KV` in a loader context (KV stores are added to loader context automatically by the Cloudflare Pages adapter): ```tsx -import type { LoaderFunction } from "@remix-run/cloudflare"; +import type { LoaderArgs } from "@remix-run/cloudflare"; import { json } from "@remix-run/cloudflare"; import { useLoaderData } from "@remix-run/react"; -export const loader: LoaderFunction = async ({ +export const loader = async ({ context, params, -}) => { +}: LoaderArgs) => { return json( await context.PRODUCTS_KV.get( `product-${params.productId}`, @@ -260,7 +245,7 @@ export const loader: LoaderFunction = async ({ }; export default function Product() { - const product = useLoaderData(); + const product = useLoaderData(); return (

      Product

      @@ -275,10 +260,10 @@ export default function Product() { While loading data it's common for a record to be "not found". As soon as you know you can't render the component as expected, `throw` a response and Remix will stop executing code in the current loader and switch over to the nearest [catch boundary][catch-boundary]. ```tsx lines=[10-13] -export const loader: LoaderFunction = async ({ +export const loader = async ({ params, request, -}) => { +}: LoaderArgs) => { const product = await db.product.findOne({ where: { id: params.productId }, }); @@ -302,13 +287,11 @@ export const loader: LoaderFunction = async ({ URL Search Params are the portion of the URL after a `?`. Other names for this are "query string", "search string", or "location search". You can access the values by creating a URL out of the `request.url`: -```tsx filename=routes/products.tsx lines=[7,8] +```tsx filename=routes/products.tsx lines=[5-6] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno -export const loader: LoaderFunction = async ({ - request, -}) => { +export const loader = async ({ request }: LoaderArgs) => { const url = new URL(request.url); const term = url.searchParams.get("term"); return json(await fakeProductSearch(term)); @@ -383,10 +366,11 @@ Then the url will be: `/products/shoes?brand=nike&brand=adidas` Note that `brand` is repeated in the URL search string since both checkboxes were named `"brand"`. In your loader you can get access to all of those values with [`searchParams.getAll`][search-params-getall] -```tsx lines=[5] +```tsx lines=[6] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const url = new URL(request.url); const brands = url.searchParams.getAll("brand"); return json(await getProducts({ brands })); @@ -726,7 +710,7 @@ export async function loader() { } export default function RouteComp() { - const data = useLoaderData(); + const data = useLoaderData(); console.log(data); // '{"date":"2021-11-27T23:54:26.384Z"}' } diff --git a/docs/guides/data-writes.md b/docs/guides/data-writes.md index eb03a5facf3..d6bd93119cb 100644 --- a/docs/guides/data-writes.md +++ b/docs/guides/data-writes.md @@ -106,8 +106,8 @@ When the user submits this form, the browser will serialize the fields into a re The data is made available to the server's request handler so you can create the record. After that, you return a response. In this case, you'd probably redirect to the newly created project. A remix action would look something like this: -```js filename=app/routes/projects -export async function action({ request }) { +```tsx filename=app/routes/projects.tsx +export async function action({ request }: ActionArgs) { const body = await request.formData(); const project = await createProject(body); return redirect(`/projects/${project.id}`); @@ -175,14 +175,14 @@ export default function NewProject() { Now add the route action. Any form submissions that are "post" will call your data "action". Any "get" submissions (``) will be handled by your "loader". -```tsx [5-11] -import type { ActionFunction } from "@remix-run/node"; // or cloudflare/deno +```tsx lines=[5-11] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno // Note the "action" export name, this will handle our form POST -export const action: ActionFunction = async ({ +export const action = async ({ request, -}) => { +}: ActionArgs) => { const formData = await request.formData(); const project = await createProject(formData); return redirect(`/projects/${project.id}`); @@ -211,10 +211,10 @@ const [errors, project] = await createProject(formData); If there are validation errors, we want to go back to the form and display them. -```tsx [5,7-10] -export const action: ActionFunction = async ({ +```tsx lines=[5,7-10] +export const action = async ({ request, -}) => { +}: ActionArgs) => { const formData = await request.formData(); const [errors, project] = await createProject(formData); @@ -229,18 +229,19 @@ export const action: ActionFunction = async ({ Just like `useLoaderData` returns the values from the `loader`, `useActionData` will return the data from the action. It will only be there if the navigation was a form submission, so you always have to check if you've got it or not. -```tsx [2,11,21,26-30,38,43-47] +```tsx lines=[3,12,22,27-31,39,44-48] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { useActionData } from "@remix-run/react"; -export const action: ActionFunction = async ({ +export const action = async ({ request, -}) => { +}: ActionArgs) => { // ... }; export default function NewProject() { - const actionData = useActionData(); + const actionData = useActionData(); return ( @@ -301,7 +302,7 @@ import { useActionData, Form } from "@remix-run/react"; // ... export default function NewProject() { - const actionData = useActionData(); + const actionData = useActionData(); return ( // note the capital "F" now @@ -332,7 +333,7 @@ export default function NewProject() { // when the form is being processed on the server, this returns different // transition states to help us build pending and optimistic UI. const transition = useTransition(); - const actionData = useActionData(); + const actionData = useActionData(); return ( @@ -434,7 +435,7 @@ Now we can wrap our old error messages in this new fancy component, and even tur ```tsx [21-24, 31-34, 44-48, 53-56] export default function NewProject() { const transition = useTransition(); - const actionData = useActionData(); + const actionData = useActionData(); return ( diff --git a/docs/guides/envvars.md b/docs/guides/envvars.md index 89b8521216e..3516f5ab50e 100644 --- a/docs/guides/envvars.md +++ b/docs/guides/envvars.md @@ -50,8 +50,8 @@ If you're using the `@remix-run/cloudflare-pages` adapter, env variables work a Then, in your `loader` functions, you can access environment variables directly on `context`: -```js -export const loader = async ({ context }) => { +```tsx +export const loader = async ({ context }:LoaderArgs) => { console.log(context.SOME_SECRET); }; ``` @@ -119,7 +119,7 @@ Instead we recommend keeping all of your environment variables on the server (al } export function Root() { - const data = useLoaderData(); + const data = useLoaderData(); return ( diff --git a/docs/guides/mdx.md b/docs/guides/mdx.md index 0eb3b22cda1..40ac3ec3546 100644 --- a/docs/guides/mdx.md +++ b/docs/guides/mdx.md @@ -89,14 +89,15 @@ export const handle = { someData: "abc", }; +import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; export const loader = async () => { - return { mamboNumber: 5 }; + return json({ mamboNumber: 5 }); }; export function ComponentUsingData() { - const { mamboNumber } = useLoaderData(); + const { mamboNumber } = useLoaderData(); return
      Mambo Number: {mamboNumber}
      ; } @@ -159,7 +160,7 @@ export async function loader() { } export default function Index() { - const posts = useLoaderData(); + const posts = useLoaderData(); return (
        diff --git a/docs/guides/not-found.md b/docs/guides/not-found.md index 2675b818622..a2d0f98d585 100644 --- a/docs/guides/not-found.md +++ b/docs/guides/not-found.md @@ -18,7 +18,7 @@ The first case is already handled by Remix, you don't have to throw a response y As soon as you know you don't have what the user is looking for you should _throw a response_. ```tsx filename=routes/page/$slug.js -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { const page = await db.page.findOne({ where: { slug: params.slug }, }); @@ -69,13 +69,14 @@ export function CatchBoundary() { Just like [errors], nested routes can export their own catch boundary to handle the 404 UI without taking down all of the parent layouts around it, and add some nice UX touches right in context. Bots are happy, SEO is happy, CDNs are happy, users are happy, and your code stays in context, so it seems like everybody involved is happy with this. ```tsx filename=app/routes/pages/$pageId.tsx +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { Form, useLoaderData, useParams, } from "@remix-run/react"; -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { const page = await db.page.findOne({ where: { slug: params.slug }, }); @@ -108,7 +109,7 @@ export function CatchBoundary() { } export default function Page() { - return ; + return ()} />; } ``` diff --git a/docs/guides/optimistic-ui.md b/docs/guides/optimistic-ui.md index 83033661f41..95d1b293b88 100644 --- a/docs/guides/optimistic-ui.md +++ b/docs/guides/optimistic-ui.md @@ -23,17 +23,18 @@ Remix can help you build optimistic UI with [`useTransition`][use-transition] an Consider the workflow for viewing and creating a new project. The project route loads the project and renders it. ```tsx filename=app/routes/project/$id.tsx +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; import { ProjectView } from "~/components/project"; -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { return json(await findProject(params.id)); } export default function ProjectRoute() { - const project = useLoaderData(); + const project = useLoaderData(); return ; } ``` @@ -59,14 +60,15 @@ export function ProjectView({ project }) { Now we can get to the fun part. Here's what a "new project" route might look like: ```tsx filename=app/routes/projects/new.tsx +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { Form } from "@remix-run/react"; import { createProject } from "~/utils"; -export const action: ActionFunction = async ({ +export const action = async ({ request, -}) => { +}: ActionArgs) => { const body = await request.formData(); const newProject = Object.fromEntries(body); const project = await createProject(newProject); @@ -92,15 +94,16 @@ export default function NewProject() { At this point, typically you'd render a busy spinner on the page while the user waits for the project to be sent to the server, added to the database, and sent back to the browser and then redirected to the project. Remix makes that pretty easy: -```tsx filename=app/routes/projects/new.tsx lines=[2,16,28,30-32] +```tsx filename=app/routes/projects/new.tsx lines=[3,17,29,31-33] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { Form, useTransition } from "@remix-run/react"; import { createProject } from "~/utils"; -export const action: ActionFunction = async ({ +export const action = async ({ request, -}) => { +}: ActionArgs) => { const body = await request.formData(); const newProject = Object.fromEntries(body); const project = await createProject(newProject); @@ -134,16 +137,17 @@ export default function NewProject() { Since we know that almost every time this form is submitted it's going to succeed, we can just skip the busy spinners and show the UI as we know it's going to be: the ``. -```tsx filename=app/routes/projects/new.tsx lines=[5,17-23] +```tsx filename=app/routes/projects/new.tsx lines=[6,18-24] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { Form, useTransition } from "@remix-run/react"; import { createProject } from "~/utils"; import { ProjectView } from "~/components/project"; -export const action: ActionFunction = async ({ +export const action = async ({ request, -}) => { +}: ActionArgs) => { const body = await request.formData(); const newProject = Object.fromEntries(body); const project = await createProject(newProject); @@ -182,20 +186,21 @@ One of the hardest parts about implementing optimistic UI is how to handle failu If you want to have more control over the UI when an error occurs and put the user right back where they were without losing any state, you can catch your own error and send it down through action data. -```tsx filename=app/routes/projects/new.tsx lines=[4-5,16-24,29,48] +```tsx filename=app/routes/projects/new.tsx lines=[5-6,17-25,30,49] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno import { Form, - useTransition, useActionData, + useTransition, } from "@remix-run/react"; import { createProject } from "~/utils"; import { ProjectView } from "~/components/project"; -export const action: ActionFunction = async ({ +export const action = async ({ request, -}) => { +}: ActionArgs) => { const body = await request.formData(); const newProject = Object.fromEntries(body); try { @@ -211,7 +216,7 @@ export const action: ActionFunction = async ({ export default function NewProject() { const transition = useTransition(); - const error = useActionData(); + const error = useActionData(); return transition.submission ? ( (); return (

        {report.name}

        @@ -42,7 +42,7 @@ export default function Report() { It's linking to a PDF version of the page. To make this work we can create a Resource Route below it. Notice that it has no component: that makes it a Resource Route. ```tsx filename=app/routes/reports/$id/pdf.ts -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { const report = await getReport(params.id); const pdf = await generateReportPDF(report); return new Response(pdf, { @@ -85,13 +85,11 @@ app/routes/reports/$id[.]pdf.ts To handle `GET` requests export a loader function: -```ts +```tsx +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno -export const loader: LoaderFunction = async ({ - request, -}) => { +export const loader = async ({ request }: LoaderArgs) => { // handle "GET" request return json({ success: true }, 200); @@ -100,12 +98,10 @@ export const loader: LoaderFunction = async ({ To handle `POST`, `PUT`, `PATCH` or `DELETE` requests export an action function: -```ts -import type { ActionFunction } from "@remix-run/node"; // or cloudflare/deno +```tsx +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { switch (request.method) { case "POST": { /* handle "POST" */ @@ -127,14 +123,12 @@ export const action: ActionFunction = async ({ Resource routes can be used to handle webhooks. For example, you can create a webhook that receives notifications from GitHub when a new commit is pushed to a repository: -```ts -import type { ActionFunction } from "@remix-run/node"; // or cloudflare/deno +```tsx +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import crypto from "crypto"; -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { if (request.method !== "POST") { return json({ message: "Method not allowed" }, 405); } diff --git a/docs/guides/routing.md b/docs/guides/routing.md index a59d4e5a735..c30c8cdcb19 100644 --- a/docs/guides/routing.md +++ b/docs/guides/routing.md @@ -308,11 +308,11 @@ For example, the `$invoiceId.jsx` route. When the url is `/sales/invoices/102000 ```jsx import { useParams } from "@remix-run/react"; -export function loader({ params }) { +export async function loader({ params }) { const id = params.invoiceId; } -export function action({ params }) { +export async function action({ params }) { const id = params.invoiceId; } @@ -362,7 +362,7 @@ app When the URL is `example.com/files/images/work/flyer.jpg`. The splat param will capture the trailing segments of the URL and be available to your app on `params["*"]` ```jsx -export function loader({ params }) { +export async function loader({ params }) { params["*"]; // "images/work/flyer.jpg" } ``` diff --git a/docs/guides/styling.md b/docs/guides/styling.md index d7806c7566c..22b55d66dde 100644 --- a/docs/guides/styling.md +++ b/docs/guides/styling.md @@ -253,7 +253,8 @@ Now Remix can prefetch, load, and unload the styles for `button.css`, `primary-b An initial reaction to this is that routes have to know more than you want them to. Keep in mind each component must be imported already, so it's not introducing a new dependency, just some boilerplate to get the assets. For example, consider a product category page like this: -```tsx filename=app/routes/$category.js lines=[4-8,24-31] +```tsx filename=app/routes/$category.js lines=[5-9,25-32] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; @@ -267,14 +268,14 @@ export function links() { return [{ rel: "stylesheet", href: styles }]; } -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { return json( await getProductsForCategory(params.category) ); } export default function Category() { - const products = useLoaderData(); + const products = useLoaderData(); return ( {products.map((product) => ( diff --git a/docs/other-api/serve.md b/docs/other-api/serve.md index baf7b537276..0d16aa527c2 100644 --- a/docs/other-api/serve.md +++ b/docs/other-api/serve.md @@ -21,12 +21,12 @@ In development, `remix-serve` will ensure the latest code is run by purging the - Any values in the module scope will be "reset" - ```ts [1-3] + ```tsx lines=[1-3] // this will be reset for every request because the module cache was // cleared and this will be required brand new const cache = new Map(); - export async function loader({ params }) { + export async function loader({ params }: LoaderArgs) { if (cache.has(params.foo)) { return json(cache.get(params.foo)); } @@ -39,7 +39,7 @@ In development, `remix-serve` will ensure the latest code is run by purging the If you need a workaround for preserving cache in development, you can store it in the global variable. - ```ts lines=[1-9] + ```tsx lines=[1-9] // since the cache is stored in global it will only // be recreated when you restart your dev server. const cache = () => { @@ -50,7 +50,7 @@ In development, `remix-serve` will ensure the latest code is run by purging the return global.uniqueCacheName; }; - export async function loader({ params }) { + export async function loader({ params }: LoaderArgs) { if (cache.has(params.foo)) { return json(cache.get(params.foo)); } @@ -63,7 +63,7 @@ In development, `remix-serve` will ensure the latest code is run by purging the - Any **module side effects** will remain in place! This may cause problems, but should probably be avoided anyway. - ```ts [3-6] + ```tsx lines=[3-6] import { json } from "@remix-run/node"; // or cloudflare/deno // this starts running the moment the module is imported diff --git a/docs/pages/faq.md b/docs/pages/faq.md index 0bcef2eec00..f37d31dcbec 100644 --- a/docs/pages/faq.md +++ b/docs/pages/faq.md @@ -41,7 +41,7 @@ export async function requireUserSession(request) { And now in any loader or action that requires a user session, you can call the function. ```tsx filename=app/routes/projects.jsx lines=[3] -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { // if the user isn't authenticated, this will redirect to login const session = await requireUserSession(request); @@ -55,8 +55,8 @@ export async function loader({ request }) { Even if you don't need the session information, the function will still protect the route: -```js -export async function loader({ request }) { +```tsx +export async function loader({ request }: LoaderArgs) { await requireUserSession(request); // continue } @@ -81,8 +81,8 @@ We find option (1) to be the simplest because you don't have to mess around with HTML buttons can send a value, so it's the easiest way to implement this: -```jsx filename=app/routes/projects/$id.jsx lines=[3-4,33,39] -export async function action({ request }) { +```tsx filename=app/routes/projects/$id.tsx lines=[3-4,33,39] +export async function action({ request }: ActionArgs) { let formData = await request.formData(); let intent = formData.get("intent"); switch (intent) { @@ -101,7 +101,7 @@ export async function action({ request }) { } export default function Projects() { - let project = useLoaderData(); + let project = useLoaderData(); return ( <>

        Update Project

        @@ -159,7 +159,7 @@ If you're wanting to send structured data simply to post arrays, you can use the Each checkbox has the name: "category". Since `FormData` can have multiple values on the same key, you don't need JSON for this. Access the checkbox values with `formData.getAll()` in your action. ```tsx -export async function action({ request }) { +export async function action({ request }: ActionArgs) { const formData = await request.formData(); let categories = formData.getAll("category"); // ["comedy", "music"] @@ -186,7 +186,7 @@ And then in your action: import queryString from "query-string"; // in your action: -export async function action({ request }) { +export async function action({ request }: ActionArgs) { // use `request.text()`, not `request.formData` to get the form data as a url // encoded form query string let formQueryString = await request.text(); @@ -209,7 +209,7 @@ Some folks even dump their JSON into a hidden field. Note that this approach won And then parse it in the action: ```tsx -export async function action({ request }) { +export async function action({ request }: ActionArgs) { let formData = await request.formData(); let obj = JSON.parse(formData.get("json")); } diff --git a/docs/pages/philosophy.md b/docs/pages/philosophy.md index 1fe63026b9e..248af957119 100644 --- a/docs/pages/philosophy.md +++ b/docs/pages/philosophy.md @@ -26,7 +26,7 @@ There are a lot of ways Remix helps you send less stuff over the network and we Consider [the Github Gist API][the-github-gist-api]. This payload is 75kb unpacked and 12kb over the network compressed. If you fetch it in the browser you make the user download all of it. It might look like this: -```jsx +```tsx export default function Gists() { const gists = useSomeFetchWrapper( "https://api.github.com/gists" @@ -55,26 +55,26 @@ export default function Gists() { With Remix, you can filter down the data _on the server_ before sending it to the user: -```js [3-16] +```tsx lines=[3-15] import { json } from "@remix-run/node"; // or cloudflare/deno export async function loader() { const res = await fetch("https://api.github.com/gists"); const gists = await res.json(); + return json( - gists.map((gist) => { - return { - description: gist.description, - url: gist.html_url, - files: Object.keys(gist.files), - owner: gist.owner.login, - }; - }) + gists.map((gist) => ({ + description: gist.description, + url: gist.html_url, + files: Object.keys(gist.files), + owner: gist.owner.login, + })) ); } export default function Gists() { - const gists = useLoaderData(); + const gists = useLoaderData(); + return (
          {gists.map((gist) => ( diff --git a/docs/pages/technical-explanation.md b/docs/pages/technical-explanation.md index daddaac55c4..3eaa39a71da 100644 --- a/docs/pages/technical-explanation.md +++ b/docs/pages/technical-explanation.md @@ -85,7 +85,7 @@ More often than not, a Remix route module can contain both the UI and the intera Route modules have three primary exports: `loader`, `action`, and `default` (component). -```jsx +```tsx // Loaders only run on the server and provide data // to your component on GET requests export async function loader() { @@ -95,7 +95,7 @@ export async function loader() { // Actions only run on the server and handle POST // PUT, PATCH, and DELETE. They can also provide data // to the component -export async function action({ request }) { +export async function action({ request }: ActionArgs) { const form = await request.formData(); const errors = validate(form); if (errors) { @@ -109,8 +109,8 @@ export async function action({ request }) { // rendered when a route matches the URL. This runs // both on the server and the client export default function Projects() { - const projects = useLoaderData(); - const actionData = useActionData(); + const projects = useLoaderData(); + const actionData = useActionData(); return (
          @@ -168,10 +168,10 @@ Taking our route module from before, here are a few small, but useful UX improve 2. Focus the input when server side form validation fails 3. Animate in the error messages -```jsx nocopy lines=[4-6,8-12,23-26,30-32] +```tsx nocopy lines=[4-6,8-12,23-26,30-32] export default function Projects() { - const projects = useLoaderData(); - const actionData = useActionData(); + const projects = useLoaderData(); + const actionData = useActionData(); const { state } = useTransition(); const busy = state === "submitting"; const inputRef = React.useRef(); diff --git a/docs/tutorials/blog.md b/docs/tutorials/blog.md index 4c135a7d065..b4c832b705c 100644 --- a/docs/tutorials/blog.md +++ b/docs/tutorials/blog.md @@ -149,7 +149,7 @@ export const loader = async () => { }; export default function Posts() { - const { posts } = useLoaderData(); + const { posts } = useLoaderData(); console.log(posts); return (
          @@ -169,61 +169,7 @@ import { Link, useLoaderData } from "@remix-run/react"; // ... export default function Posts() { - const { posts } = useLoaderData(); - return ( -
          -

          Posts

          -
            - {posts.map((post) => ( -
          • - - {post.title} - -
          • - ))} -
          -
          - ); -} -``` - -TypeScript is mad, so let's help it out: - -πŸ’Ώ Add the Post type and generic for `useLoaderData` - -```tsx filename=app/routes/posts/index.tsx lines=[4-7,9-11,14,29] -import { json } from "@remix-run/node"; -import { Link, useLoaderData } from "@remix-run/react"; - -type Post = { - slug: string; - title: string; -}; - -type LoaderData = { - posts: Array; -}; - -export const loader = async () => { - return json({ - posts: [ - { - slug: "my-first-post", - title: "My First Post", - }, - { - slug: "90s-mixtape", - title: "A Mixtape I Made Just For You", - }, - ], - }); -}; - -export default function Posts() { - const { posts } = useLoaderData() as LoaderData; + const { posts } = useLoaderData(); return (

          Posts

          @@ -288,15 +234,8 @@ import { Link, useLoaderData } from "@remix-run/react"; import { getPosts } from "~/models/post.server"; -type LoaderData = { - // this is a handy way to say: "posts is whatever type getPosts resolves to" - posts: Awaited>; -}; - export const loader = async () => { - return json({ - posts: await getPosts(), - }); + return json({ posts: await getPosts() }); }; // ... @@ -449,16 +388,17 @@ You can click one of your posts and should see the new page. πŸ’Ώ Add a loader to access the params -```tsx filename=app/routes/posts/$slug.tsx lines=[1-2,4-6,9,13] +```tsx filename=app/routes/posts/$slug.tsx lines=[1-3,5-7,10,14] +import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; -export const loader = async ({ params }) => { +export const loader = async ({ params }: LoaderArgs) => { return json({ slug: params.slug }); }; export default function PostSlug() { - const { slug } = useLoaderData(); + const { slug } = useLoaderData(); return (

          @@ -471,31 +411,13 @@ export default function PostSlug() { The part of the filename attached to the `$` becomes a named key on the `params` object that comes into your loader. This is how we'll look up our blog post. -πŸ’Ώ Let's get some help from TypeScript for the loader function signature. - -```tsx filename=app/routes/posts/$slug.tsx lines=[1,5] -import type { LoaderFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; - -export const loader: LoaderFunction = async ({ - params, -}) => { - return json({ slug: params.slug }); -}; -``` - Now, let's actually get the post contents from the database by its slug. πŸ’Ώ Add a `getPost` function to our post module -Update the `app/models/post.server.ts` file: - -```tsx filename=app/models/post.server.ts lines=[3,9-11] +```tsx filename=app/models/post.server.ts lines=[7-9] import { prisma } from "~/db.server"; -export type { Post } from "@prisma/client"; - export async function getPosts() { return prisma.post.findMany(); } @@ -505,26 +427,22 @@ export async function getPost(slug: string) { } ``` -If you see a TypeScript warning, such as `TS2305: Module '"@prisma/client"' has no exported member 'Post'.`, you may need to restart your editor. - πŸ’Ώ Use the new `getPost` function in the route -```tsx filename=app/routes/posts/$slug.tsx lines=[5,10-11,15,19] -import type { LoaderFunction } from "@remix-run/node"; +```tsx filename=app/routes/posts/$slug.tsx lines=[5,8-9,13,17] +import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { getPost } from "~/models/post.server"; -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { const post = await getPost(params.slug); return json({ post }); }; export default function PostSlug() { - const { post } = useLoaderData(); + const { post } = useLoaderData(); return (

          @@ -539,30 +457,25 @@ Check that out! We're now pulling our posts from a data source instead of includ Let's make TypeScript happy with our code: -```tsx filename=app/routes/posts/$slug.tsx lines=[4,6,9,14,17,19,23] -import type { LoaderFunction } from "@remix-run/node"; +```tsx filename=app/routes/posts/$slug.tsx lines=[4,9,12] +import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import invariant from "tiny-invariant"; -import type { Post } from "~/models/post.server"; import { getPost } from "~/models/post.server"; -type LoaderData = { post: Post }; - -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { invariant(params.slug, `params.slug is required`); const post = await getPost(params.slug); invariant(post, `Post not found: ${params.slug}`); - return json({ post }); + return json({ post }); }; export default function PostSlug() { - const { post } = useLoaderData() as LoaderData; + const { post } = useLoaderData(); return (

          @@ -589,32 +502,27 @@ npm add @types/marked -D Now that `marked` has been installed, we will need to restart our server. So stop the dev server and start it back up again with `npm run dev`. -```tsx filename=app/routes/post/$slug.ts lines=[1,10,20-21,25,31] -import { marked } from "marked"; -import type { LoaderFunction } from "@remix-run/node"; +```tsx filename=app/routes/post/$slug.ts lines=[4,15-16,20,26] +import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; +import { marked } from "marked"; import invariant from "tiny-invariant"; -import type { Post } from "~/models/post.server"; import { getPost } from "~/models/post.server"; -type LoaderData = { post: Post; html: string }; - -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { invariant(params.slug, `params.slug is required`); const post = await getPost(params.slug); invariant(post, `Post not found: ${params.slug}`); const html = marked(post.markdown); - return json({ post, html }); + return json({ post, html }); }; export default function PostSlug() { - const { post, html } = useLoaderData() as LoaderData; + const { post, html } = useLoaderData(); return (

          @@ -655,22 +563,17 @@ touch app/routes/posts/admin.tsx ``` ```tsx filename=app/routes/posts/admin.tsx -import type { LoaderFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Link, useLoaderData } from "@remix-run/react"; import { getPosts } from "~/models/post.server"; -type LoaderData = { - posts: Awaited>; -}; - -export const loader: LoaderFunction = async () => { +export const loader = async () => { return json({ posts: await getPosts() }); }; export default function PostAdmin() { - const { posts } = useLoaderData() as LoaderData; + const { posts } = useLoaderData(); return (

          @@ -732,8 +635,7 @@ If you refresh you're not going to see it yet. Every route inside of `app/routes πŸ’Ώ Add an outlet to the admin page -```tsx filename=app/routes/posts/admin.tsx lines=[5,42] -import type { LoaderFunction } from "@remix-run/node"; +```tsx filename=app/routes/posts/admin.tsx lines=[4,41] import { json } from "@remix-run/node"; import { Link, @@ -743,16 +645,12 @@ import { import { getPosts } from "~/models/post.server"; -type LoaderData = { - posts: Awaited>; -}; - -export const loader: LoaderFunction = async () => { +export const loader = async () => { return json({ posts: await getPosts() }); }; export default function PostAdmin() { - const { posts } = useLoaderData() as LoaderData; + const { posts } = useLoaderData(); return (

          @@ -875,12 +773,13 @@ export async function createPost(post) { πŸ’Ώ Call `createPost` from the new post route's action ```tsx filename=app/routes/posts/admin/new.tsx +import type { ActionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { Form } from "@remix-run/react"; import { createPost } from "~/models/post.server"; -export const action = async ({ request }) => { +export const action = async ({ request }: ActionArgs) => { const formData = await request.formData(); const title = formData.get("title"); @@ -901,12 +800,11 @@ In HTML an input's `name` attribute is sent over the network and available by th TypeScript is mad again, let's add some types. -πŸ’Ώ Add the types to both files we changed +πŸ’Ώ Add the types to `app/models/post.server.ts` -```tsx filename=app/models/post.server.ts lines=[2,8] +```tsx filename=app/models/post.server.ts lines=[2,7] // ... import type { Post } from "@prisma/client"; -export type { Post }; // ... @@ -917,60 +815,27 @@ export async function createPost( } ``` -```tsx filename=app/routes/posts/admin/new.tsx lines=[1,7] -import type { ActionFunction } from "@remix-run/node"; -import { redirect } from "@remix-run/node"; -import { Form } from "@remix-run/react"; - -import { createPost } from "~/models/post.server"; - -export const action: ActionFunction = async ({ - request, -}) => { - const formData = await request.formData(); - - const title = formData.get("title"); - const slug = formData.get("slug"); - const markdown = formData.get("markdown"); - - await createPost({ title, slug, markdown }); - - return redirect("/posts/admin"); -}; - -// ... -``` - Whether you're using TypeScript or not, we've got a problem when the user doesn't provide values on some of these fields (and TS is still mad about that call to `createPost`). Let's add some validation before we create the post. πŸ’Ώ Validate if the form data contains what we need, and return the errors if not -```tsx filename=app/routes/posts/admin/new.tsx lines=[2,7-13,23-33] -import type { ActionFunction } from "@remix-run/node"; +```tsx filename=app/routes/posts/admin/new.tsx lines=[2,14-24] +import type { ActionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form } from "@remix-run/react"; import { createPost } from "~/models/post.server"; -type ActionData = - | { - title: null | string; - slug: null | string; - markdown: null | string; - } - | undefined; -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const formData = await request.formData(); const title = formData.get("title"); const slug = formData.get("slug"); const markdown = formData.get("markdown"); - const errors: ActionData = { + const errors = { title: title ? null : "Title is required", slug: slug ? null : "Slug is required", markdown: markdown ? null : "Markdown is required", @@ -979,7 +844,7 @@ export const action: ActionFunction = async ({ (errorMessage) => errorMessage ); if (hasErrors) { - return json(errors); + return json(errors); } await createPost({ title, slug, markdown }); @@ -995,7 +860,7 @@ Notice we don't return a redirect this time, we actually return the errors. Thes πŸ’Ώ Add validation messages to the UI ```tsx filename=app/routes/posts/admin/new.tsx lines=[3,10,17-19,26-28,35-39] -import type { ActionFunction } from "@remix-run/node"; +import type { ActionArgs } from "@remix-run/node"; import { redirect, json } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; @@ -1004,7 +869,7 @@ import { Form, useActionData } from "@remix-run/react"; const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg`; export default function NewPost() { - const errors = useActionData(); + const errors = useActionData(); return ( @@ -1063,9 +928,7 @@ TypeScript is still mad, because someone could call our API with non-string valu import invariant from "tiny-invariant"; // .. -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { // ... invariant( typeof title === "string", @@ -1094,11 +957,9 @@ Let's slow this down and add some "pending UI" to our form. πŸ’Ώ Slow down our action with a fake delay -```tsx filename=app/routes/posts/admin/new.tsx lines=[5-6] +```tsx filename=app/routes/posts/admin/new.tsx lines=[3-4] // ... -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { // TODO: remove me await new Promise((res) => setTimeout(res, 1000)); @@ -1109,7 +970,8 @@ export const action: ActionFunction = async ({ πŸ’Ώ Add some pending UI with `useTransition` -```tsx filename=app/routes/posts/admin/new.tsx lines=[5,13-14,23,25] +```tsx filename=app/routes/posts/admin/new.tsx lines=[6,14-15,24,26] +import type { ActionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form, @@ -1120,7 +982,7 @@ import { // .. export default function NewPost() { - const errors = useActionData(); + const errors = useActionData(); const transition = useTransition(); const isCreating = Boolean(transition.submission); diff --git a/docs/tutorials/jokes.md b/docs/tutorials/jokes.md index b7f773e9444..f4ce86080ab 100644 --- a/docs/tutorials/jokes.md +++ b/docs/tutorials/jokes.md @@ -1573,24 +1573,20 @@ To _load_ data in a Remix route module, you use a [`loader`][loader]. This is si ```tsx nocopy // this is just an example. No need to copy/paste this πŸ˜„ -import type { LoaderFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import type { Sandwich } from "@prisma/client"; import { db } from "~/utils/db.server"; -type LoaderData = { sandwiches: Array }; - -export const loader: LoaderFunction = async () => { - const data: LoaderData = { +export const loader = async () => { + return json({ sandwiches: await db.sandwich.findMany(), - }; - return json(data); + }); }; export default function Sandwiches() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            {data.sandwiches.map((sandwich) => ( @@ -1615,12 +1611,8 @@ Remix and the `tsconfig.json` you get from the starter template are configured t app/routes/jokes.tsx -```tsx filename=app/routes/jokes.tsx lines=[3,5-6,10,13,20-22,24-29,32,56-60] -import type { - LinksFunction, - LoaderFunction, -} from "@remix-run/node"; -import type { Joke } from "@prisma/client"; +```tsx filename=app/routes/jokes.tsx lines=[2,6,10,16-20,23,47-51] +import type { LinksFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Link, @@ -1628,26 +1620,21 @@ import { useLoaderData, } from "@remix-run/react"; -import { db } from "~/utils/db.server"; import stylesUrl from "~/styles/jokes.css"; +import { db } from "~/utils/db.server"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: stylesUrl }]; }; -type LoaderData = { - jokeListItems: Array; -}; - -export const loader: LoaderFunction = async () => { - const data: LoaderData = { +export const loader = async () => { + return json({ jokeListItems: await db.joke.findMany(), - }; - return json(data); + }); }; export default function JokesRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -1701,20 +1688,15 @@ And here's what we have with that now: I want to call out something specific in my solution. Here's my loader: -```tsx lines=[2,8-10] -type LoaderData = { - jokeListItems: Array<{ id: string; name: string }>; -}; - -export const loader: LoaderFunction = async () => { - const data: LoaderData = { +```tsx lines=[4-6] +export const loader = async () => { + return json({ jokeListItems: await db.joke.findMany({ take: 5, select: { id: true, name: true }, orderBy: { createdAt: "desc" }, }), - }; - return json(data); + }); }; ``` @@ -1735,9 +1717,7 @@ So the only way to really be 100% positive that your data is correct, you should Before we get to the `/jokes/:jokeId` route, here's a quick example of how you can access params (like `:jokeId`) in your loader. ```tsx nocopy -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { console.log(params); // <-- {jokeId: "123"} }; ``` @@ -1758,29 +1738,25 @@ const joke = await db.joke.findUnique({ app/routes/jokes/$jokeId.tsx -```tsx filename=app/routes/jokes/$jokeId.tsx lines=[4,6,8,10-19,22,27-28] -import type { LoaderFunction } from "@remix-run/node"; +```tsx filename=app/routes/jokes/$jokeId.tsx lines=[1-3,5,7-15,18,23-24] +import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Link, useLoaderData } from "@remix-run/react"; -import type { Joke } from "@prisma/client"; import { db } from "~/utils/db.server"; -type LoaderData = { joke: Joke }; - -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { const joke = await db.joke.findUnique({ where: { id: params.jokeId }, }); - if (!joke) throw new Error("Joke not found"); - const data: LoaderData = { joke }; - return json(data); + if (!joke) { + throw new Error("Joke not found"); + } + return json({ joke }); }; export default function JokeRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -1819,29 +1795,25 @@ const [randomJoke] = await db.joke.findMany({ app/routes/jokes/index.tsx -```tsx filename=app/routes/jokes/index.tsx lines=[4,6,8,10-19,22] -import type { LoaderFunction } from "@remix-run/node"; +```tsx filename=app/routes/jokes/index.tsx lines=[5,7-15,18] +import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData, Link } from "@remix-run/react"; -import type { Joke } from "@prisma/client"; import { db } from "~/utils/db.server"; -type LoaderData = { randomJoke: Joke }; - -export const loader: LoaderFunction = async () => { +export const loader = async () => { const count = await db.joke.count(); const randomRowNumber = Math.floor(Math.random() * count); const [randomJoke] = await db.joke.findMany({ take: 1, skip: randomRowNumber, }); - const data: LoaderData = { randomJoke }; - return json(data); + return json({ randomJoke }); }; export default function JokesIndexRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -1908,15 +1880,13 @@ const joke = await db.joke.create({ app/routes/jokes/new.tsx -```tsx filename=app/routes/jokes/new.tsx lines=[1-2,4,6-25] -import type { ActionFunction } from "@remix-run/node"; +```tsx filename=app/routes/jokes/new.tsx lines=[1-2,4,6-23] +import type { ActionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { db } from "~/utils/db.server"; -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const form = await request.formData(); const name = form.get("name"); const content = form.get("content"); @@ -1993,12 +1963,13 @@ But if there's an error, you can return an object with the error messages and th app/routes/jokes/new.tsx -```tsx filename=app/routes/jokes/new.tsx lines=[2-3,7-11,13-17,19-29,31-32,44-46,49-52,54-56,63,74,76-84,87-95,101,103-111,114-122,125-132] -import type { ActionFunction } from "@remix-run/node"; +```tsx filename=app/routes/jokes/new.tsx lines=[2-3,6,8-12,14-18,28-32,35-38,40-46,53,64,66-74,77-85,91,93-101,104-112,115-122] +import type { ActionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { useActionData } from "@remix-run/react"; import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; function validateJokeContent(content: string) { if (content.length < 10) { @@ -2012,24 +1983,7 @@ function validateJokeName(name: string) { } } -type ActionData = { - formError?: string; - fieldErrors?: { - name: string | undefined; - content: string | undefined; - }; - fields?: { - name: string; - content: string; - }; -}; - -const badRequest = (data: ActionData) => - json(data, { status: 400 }); - -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const form = await request.formData(); const name = form.get("name"); const content = form.get("content"); @@ -2038,6 +1992,8 @@ export const action: ActionFunction = async ({ typeof content !== "string" ) { return badRequest({ + fieldErrors: null, + fields: null, formError: `Form not submitted correctly.`, }); } @@ -2048,7 +2004,11 @@ export const action: ActionFunction = async ({ }; const fields = { name, content }; if (Object.values(fieldErrors).some(Boolean)) { - return badRequest({ fieldErrors, fields }); + return badRequest({ + fieldErrors, + fields, + formError: null, + }); } const joke = await db.joke.create({ data: fields }); @@ -2056,7 +2016,7 @@ export const action: ActionFunction = async ({ }; export default function NewJokeRoute() { - const actionData = useActionData(); + const actionData = useActionData(); return (
            @@ -2138,6 +2098,23 @@ export default function NewJokeRoute() { +
            + +app/utils/request.server.ts + +```ts filename=app/utils/request.server.ts +import { json } from "@remix-run/node"; + +/** + * This helper function helps us returning the accurate HTTP status, + * 400 Bad Request, to the client. + */ +export const badRequest = (data: T) => + json(data, { status: 400 }); +``` + +
            + Great! You should now have a form that validates the fields on the server and displays those errors on the client: ![New joke form with validation errors][new-joke-form-with-validation-errors] @@ -2441,11 +2418,11 @@ fieldset > :not(:last-child) { import type { LinksFunction } from "@remix-run/node"; import { Link, useSearchParams } from "@remix-run/react"; -import stylesUrl from "../styles/login.css"; +import stylesUrl from "~/styles/login.css"; -export const links: LinksFunction = () => { - return [{ rel: "stylesheet", href: stylesUrl }]; -}; +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: stylesUrl }, +]; export default function Login() { const [searchParams] = useSearchParams(); @@ -2535,24 +2512,25 @@ Great, now that we've got the UI looking nice, let's add some logic. This will b app/routes/login.tsx -```tsx filename=app/routes/login.tsx +```tsx filename=app/routes/login.tsx lines=[2,5,8,13-14,20-24,26-30,32-38,40-112,115,138-141,150-153,164-172,174-182,190-198,200-208,210-219] import type { - ActionFunction, + ActionArgs, LinksFunction, } from "@remix-run/node"; import { json } from "@remix-run/node"; import { - useActionData, Link, + useActionData, useSearchParams, } from "@remix-run/react"; -import { db } from "~/utils/db.server"; import stylesUrl from "~/styles/login.css"; +import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; -export const links: LinksFunction = () => { - return [{ rel: "stylesheet", href: stylesUrl }]; -}; +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: stylesUrl }, +]; function validateUsername(username: unknown) { if (typeof username !== "string" || username.length < 3) { @@ -2566,8 +2544,7 @@ function validatePassword(password: unknown) { } } -function validateUrl(url: any) { - console.log(url); +function validateUrl(url: string) { let urls = ["/jokes", "/", "https://remix.run"]; if (urls.includes(url)) { return url; @@ -2575,25 +2552,7 @@ function validateUrl(url: any) { return "/jokes"; } -type ActionData = { - formError?: string; - fieldErrors?: { - username: string | undefined; - password: string | undefined; - }; - fields?: { - loginType: string; - username: string; - password: string; - }; -}; - -const badRequest = (data: ActionData) => - json(data, { status: 400 }); - -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const form = await request.formData(); const loginType = form.get("loginType"); const username = form.get("username"); @@ -2608,6 +2567,8 @@ export const action: ActionFunction = async ({ typeof redirectTo !== "string" ) { return badRequest({ + fieldErrors: null, + fields: null, formError: `Form not submitted correctly.`, }); } @@ -2617,8 +2578,13 @@ export const action: ActionFunction = async ({ username: validateUsername(username), password: validatePassword(password), }; - if (Object.values(fieldErrors).some(Boolean)) - return badRequest({ fieldErrors, fields }); + if (Object.values(fieldErrors).some(Boolean)) { + return badRequest({ + fieldErrors, + fields, + formError: null, + }); + } switch (loginType) { case "login": { @@ -2626,6 +2592,7 @@ export const action: ActionFunction = async ({ // if there's no user, return the fields and a formError // if there is a user, create their session and redirect to /jokes return badRequest({ + fieldErrors: null, fields, formError: "Not implemented", }); @@ -2636,6 +2603,7 @@ export const action: ActionFunction = async ({ }); if (userExists) { return badRequest({ + fieldErrors: null, fields, formError: `User with username ${username} already exists`, }); @@ -2643,12 +2611,14 @@ export const action: ActionFunction = async ({ // create the user // create their session and redirect to /jokes return badRequest({ + fieldErrors: null, fields, formError: "Not implemented", }); } default: { return badRequest({ + fieldErrors: null, fields, formError: `Login type invalid`, }); @@ -2657,7 +2627,7 @@ export const action: ActionFunction = async ({ }; export default function Login() { - const actionData = useActionData(); + const actionData = useActionData(); const [searchParams] = useSearchParams(); return (
            @@ -2731,13 +2701,11 @@ export default function Login() { -Great, with that in place, now we can update `app/routes/login.tsx` to use it: +Great, with that in place, we can now update `app/routes/login.tsx` to use it:
            app/routes/login.tsx -```tsx filename=app/routes/login.tsx lines=[4,15-22] nocopy +```tsx filename=app/routes/login.tsx lines=[6,14-22] nocopy // ... +import stylesUrl from "~/styles/login.css"; import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; import { login } from "~/utils/session.server"; -import stylesUrl from "~/styles/login.css"; // ... -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { // ... switch (loginType) { case "login": { @@ -2863,12 +2830,14 @@ export const action: ActionFunction = async ({ console.log({ user }); if (!user) { return badRequest({ + fieldErrors: null, fields, formError: `Username/Password combination is incorrect`, }); } // if there is a user, create their session and redirect to /jokes return badRequest({ + fieldErrors: null, fields, formError: "Not implemented", }); @@ -2985,20 +2954,20 @@ export async function createUserSession( app/routes/login.tsx -```tsx filename=app/routes/login.tsx lines=[6,26] nocopy +```tsx filename=app/routes/login.tsx lines=[7,27] nocopy // ... +import stylesUrl from "~/styles/login.css"; import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; import { - login, createUserSession, + login, } from "~/utils/session.server"; // ... -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { // ... switch (loginType) { @@ -3007,6 +2976,7 @@ export const action: ActionFunction = async ({ if (!user) { return badRequest({ + fieldErrors: null, fields, formError: `Username/Password combination is incorrect`, }); @@ -3155,12 +3125,13 @@ You may also notice that our solution makes use of the `login` route's `redirect app/routes/jokes/new.tsx -```tsx filename=app/routes/jokes/new.tsx lines=[6,38,61] -import type { ActionFunction } from "@remix-run/node"; +```tsx filename=app/routes/jokes/new.tsx lines=[7,22,61] +import type { ActionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { useActionData } from "@remix-run/react"; import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; import { requireUserId } from "~/utils/session.server"; function validateJokeContent(content: string) { @@ -3175,24 +3146,7 @@ function validateJokeName(name: string) { } } -type ActionData = { - formError?: string; - fieldErrors?: { - name: string | undefined; - content: string | undefined; - }; - fields?: { - name: string; - content: string; - }; -}; - -const badRequest = (data: ActionData) => - json(data, { status: 400 }); - -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const userId = await requireUserId(request); const form = await request.formData(); const name = form.get("name"); @@ -3202,6 +3156,8 @@ export const action: ActionFunction = async ({ typeof content !== "string" ) { return badRequest({ + fieldErrors: null, + fields: null, formError: `Form not submitted correctly.`, }); } @@ -3212,7 +3168,11 @@ export const action: ActionFunction = async ({ }; const fields = { name, content }; if (Object.values(fieldErrors).some(Boolean)) { - return badRequest({ fieldErrors, fields }); + return badRequest({ + fieldErrors, + fields, + formError: null, + }); } const joke = await db.joke.create({ @@ -3222,7 +3182,7 @@ export const action: ActionFunction = async ({ }; export default function NewJokeRoute() { - const actionData = useActionData(); + const actionData = useActionData(); return (
            @@ -3444,7 +3404,7 @@ export async function createUserSession( import type { User } from "@prisma/client"; import type { LinksFunction, - LoaderFunction, + LoaderArgs, } from "@remix-run/node"; import { json } from "@remix-run/node"; import { @@ -3461,14 +3421,7 @@ export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: stylesUrl }]; }; -type LoaderData = { - user: Awaited>; - jokeListItems: Array<{ id: string; name: string }>; -}; - -export const loader: LoaderFunction = async ({ - request, -}) => { +export const loader = async ({ request }: LoaderArgs) => { const jokeListItems = await db.joke.findMany({ take: 5, orderBy: { createdAt: "desc" }, @@ -3476,15 +3429,14 @@ export const loader: LoaderFunction = async ({ }); const user = await getUser(request); - const data: LoaderData = { + return json({ jokeListItems, user, - }; - return json(data); + }); }; export default function JokesRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -3548,20 +3500,18 @@ export default function JokesRoute() { ```tsx filename=app/routes/logout.tsx import type { - ActionFunction, - LoaderFunction, + ActionArgs, + LoaderArgs, } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { logout } from "~/utils/session.server"; -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { return logout(request); }; -export const loader: LoaderFunction = async () => { +export const loader = async () => { return redirect("/"); }; ``` @@ -3731,29 +3681,30 @@ export async function createUserSession( app/routes/login.tsx -```tsx filename=app/routes/login.tsx lines=[16,100-117] +```tsx filename=app/routes/login.tsx lines=[18,102-110] import type { - ActionFunction, + ActionArgs, LinksFunction, } from "@remix-run/node"; import { json } from "@remix-run/node"; import { + Link, useActionData, useSearchParams, - Link, } from "@remix-run/react"; +import stylesUrl from "~/styles/login.css"; import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; import { createUserSession, login, register, } from "~/utils/session.server"; -import stylesUrl from "~/styles/login.css"; -export const links: LinksFunction = () => { - return [{ rel: "stylesheet", href: stylesUrl }]; -}; +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: stylesUrl }, +]; function validateUsername(username: unknown) { if (typeof username !== "string" || username.length < 3) { @@ -3767,7 +3718,7 @@ function validatePassword(password: unknown) { } } -function validateUrl(url: any) { +function validateUrl(url: string) { let urls = ["/jokes", "/", "https://remix.run"]; if (urls.includes(url)) { return url; @@ -3775,25 +3726,7 @@ function validateUrl(url: any) { return "/jokes"; } -type ActionData = { - formError?: string; - fieldErrors?: { - username: string | undefined; - password: string | undefined; - }; - fields?: { - loginType: string; - username: string; - password: string; - }; -}; - -const badRequest = (data: ActionData) => - json(data, { status: 400 }); - -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const form = await request.formData(); const loginType = form.get("loginType"); const username = form.get("username"); @@ -3808,6 +3741,8 @@ export const action: ActionFunction = async ({ typeof redirectTo !== "string" ) { return badRequest({ + fieldErrors: null, + fields: null, formError: `Form not submitted correctly.`, }); } @@ -3817,14 +3752,20 @@ export const action: ActionFunction = async ({ username: validateUsername(username), password: validatePassword(password), }; - if (Object.values(fieldErrors).some(Boolean)) - return badRequest({ fieldErrors, fields }); + if (Object.values(fieldErrors).some(Boolean)) { + return badRequest({ + fieldErrors, + fields, + formError: null, + }); + } switch (loginType) { case "login": { const user = await login({ username, password }); if (!user) { return badRequest({ + fieldErrors: null, fields, formError: `Username/Password combination is incorrect`, }); @@ -3837,6 +3778,7 @@ export const action: ActionFunction = async ({ }); if (userExists) { return badRequest({ + fieldErrors: null, fields, formError: `User with username ${username} already exists`, }); @@ -3844,6 +3786,7 @@ export const action: ActionFunction = async ({ const user = await register({ username, password }); if (!user) { return badRequest({ + fieldErrors: null, fields, formError: `Something went wrong trying to create a new user.`, }); @@ -3852,6 +3795,7 @@ export const action: ActionFunction = async ({ } default: { return badRequest({ + fieldErrors: null, fields, formError: `Login type invalid`, }); @@ -3860,7 +3804,7 @@ export const action: ActionFunction = async ({ }; export default function Login() { - const actionData = useActionData(); + const actionData = useActionData(); const [searchParams] = useSearchParams(); return (
            @@ -3934,13 +3878,11 @@ export default function Login() { ); } -// 60 + export function ErrorBoundary({ error }: { error: Error }) { return ( @@ -4088,7 +4030,7 @@ export function ErrorBoundary({ error }: { error: Error }) { app/routes/jokes/$jokeId.tsx -```tsx filename=app/routes/jokes/$jokeId.tsx nocopy +```tsx filename=app/routes/jokes/$jokeId.tsx lines=[6,11-16] nocopy // ... import { @@ -4284,24 +4226,19 @@ export function ErrorBoundary({ error }: { error: Error }) { app/routes/jokes/$jokeId.tsx -```tsx filename=app/routes/jokes/$jokeId.tsx lines=[6,21-25,42-53] -import type { LoaderFunction } from "@remix-run/node"; +```tsx filename=app/routes/jokes/$jokeId.tsx lines=[5,16-20,36-47] +import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Link, - useLoaderData, useCatch, + useLoaderData, useParams, } from "@remix-run/react"; -import type { Joke } from "@prisma/client"; import { db } from "~/utils/db.server"; -type LoaderData = { joke: Joke }; - -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { const joke = await db.joke.findUnique({ where: { id: params.jokeId }, }); @@ -4310,12 +4247,11 @@ export const loader: LoaderFunction = async ({ status: 404, }); } - const data: LoaderData = { joke }; - return json(data); + return json({ joke }); }; export default function JokeRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -4353,21 +4289,17 @@ export function ErrorBoundary() { app/routes/jokes/index.tsx -```tsx filename=app/routes/jokes/index.tsx lines=[6,21-25,44-57] -import type { LoaderFunction } from "@remix-run/node"; +```tsx filename=app/routes/jokes/index.tsx lines=[4,17-21,39-52] import { json } from "@remix-run/node"; import { - useLoaderData, Link, useCatch, + useLoaderData, } from "@remix-run/react"; -import type { Joke } from "@prisma/client"; import { db } from "~/utils/db.server"; -type LoaderData = { randomJoke: Joke }; - -export const loader: LoaderFunction = async () => { +export const loader = async () => { const count = await db.joke.count(); const randomRowNumber = Math.floor(Math.random() * count); const [randomJoke] = await db.joke.findMany({ @@ -4379,12 +4311,11 @@ export const loader: LoaderFunction = async () => { status: 404, }); } - const data: LoaderData = { randomJoke }; - return json(data); + return json({ randomJoke }); }; export default function JokesIndexRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -4427,27 +4358,26 @@ export function ErrorBoundary() { app/routes/jokes/new.tsx -```tsx filename=app/routes/jokes/new.tsx lines=[8,18-26,166-177] +```tsx filename=app/routes/jokes/new.tsx lines=[3,7,9,15,19-25,154-165] import type { - ActionFunction, - LoaderFunction, + ActionArgs, + LoaderArgs, } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { + Link, useActionData, useCatch, - Link, } from "@remix-run/react"; import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; import { - requireUserId, getUserId, + requireUserId, } from "~/utils/session.server"; -export const loader: LoaderFunction = async ({ - request, -}) => { +export const loader = async ({ request }: LoaderArgs) => { const userId = await getUserId(request); if (!userId) { throw new Response("Unauthorized", { status: 401 }); @@ -4467,24 +4397,7 @@ function validateJokeName(name: string) { } } -type ActionData = { - formError?: string; - fieldErrors?: { - name: string | undefined; - content: string | undefined; - }; - fields?: { - name: string; - content: string; - }; -}; - -const badRequest = (data: ActionData) => - json(data, { status: 400 }); - -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const userId = await requireUserId(request); const form = await request.formData(); const name = form.get("name"); @@ -4494,6 +4407,8 @@ export const action: ActionFunction = async ({ typeof content !== "string" ) { return badRequest({ + fieldErrors: null, + fields: null, formError: `Form not submitted correctly.`, }); } @@ -4504,7 +4419,11 @@ export const action: ActionFunction = async ({ }; const fields = { name, content }; if (Object.values(fieldErrors).some(Boolean)) { - return badRequest({ fieldErrors, fields }); + return badRequest({ + fieldErrors, + fields, + formError: null, + }); } const joke = await db.joke.create({ @@ -4514,7 +4433,7 @@ export const action: ActionFunction = async ({ }; export default function NewJokeRoute() { - const actionData = useActionData(); + const actionData = useActionData(); return (
            @@ -4637,12 +4556,13 @@ One thing to keep in mind with `delete` is that HTML forms only support `method= ```tsx - - + ``` -And then the `action` can determine whether the intention is to delete based on the `request.formData().get('_method')`. +And then the `action` can determine whether the intention is to delete based on the `request.formData().get('intent')`. πŸ’Ώ Add a delete capability to `app/routes/jokes/$jokeId.tsx` route. @@ -4650,28 +4570,23 @@ And then the `action` can determine whether the intention is to delete based on app/routes/jokes/$jokeId.tsx -```tsx filename=app/routes/jokes/$jokeId.tsx lines=[6,15,34-64,74-83,92-98,106-112] -import type { Joke } from "@prisma/client"; +```tsx filename=app/routes/jokes/$jokeId.tsx lines=[2,5,14,28-56,66-75,83-91,94,97-108] import type { - ActionFunction, - LoaderFunction, + ActionArgs, + LoaderArgs, } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Link, - useLoaderData, useCatch, + useLoaderData, useParams, } from "@remix-run/react"; import { db } from "~/utils/db.server"; import { requireUserId } from "~/utils/session.server"; -type LoaderData = { joke: Joke }; - -export const loader: LoaderFunction = async ({ - params, -}) => { +export const loader = async ({ params }: LoaderArgs) => { const joke = await db.joke.findUnique({ where: { id: params.jokeId }, }); @@ -4680,18 +4595,17 @@ export const loader: LoaderFunction = async ({ status: 404, }); } - const data: LoaderData = { joke }; - return json(data); + return json({ joke }); }; -export const action: ActionFunction = async ({ - request, +export const action = async ({ params, -}) => { + request, +}: ActionArgs) => { const form = await request.formData(); - if (form.get("_method") !== "delete") { + if (form.get("intent") !== "delete") { throw new Response( - `The _method ${form.get("_method")} is not supported`, + `The intent ${form.get("intent")} is not supported`, { status: 400 } ); } @@ -4707,9 +4621,7 @@ export const action: ActionFunction = async ({ if (joke.jokesterId !== userId) { throw new Response( "Pssh, nice try. That's not your joke", - { - status: 403, - } + { status: 403 } ); } await db.joke.delete({ where: { id: params.jokeId } }); @@ -4717,7 +4629,7 @@ export const action: ActionFunction = async ({ }; export default function JokeRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -4725,12 +4637,12 @@ export default function JokeRoute() {

            {data.joke.content}

            {data.joke.name} Permalink
            - -
            @@ -4769,8 +4681,7 @@ export function CatchBoundary() { } } -export function ErrorBoundary({ error }: { error: Error }) { - console.error(error); +export function ErrorBoundary() { const { jokeId } = useParams(); return (
            {`There was an error loading joke by the id ${jokeId}. Sorry.`}
            @@ -4786,17 +4697,16 @@ Now that people will get a proper error message if they try to delete a joke tha app/routes/jokes/$jokeId.tsx -```tsx filename=app/routes/jokes/$jokeId.tsx lines=[16,20,26,37,82-93] -import type { Joke } from "@prisma/client"; +```tsx filename=app/routes/jokes/$jokeId.tsx lines=[15,21,23,34,76,87] import type { - ActionFunction, - LoaderFunction, + ActionArgs, + LoaderArgs, } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Link, - useLoaderData, useCatch, + useLoaderData, useParams, } from "@remix-run/react"; @@ -4806,12 +4716,10 @@ import { requireUserId, } from "~/utils/session.server"; -type LoaderData = { joke: Joke; isOwner: boolean }; - -export const loader: LoaderFunction = async ({ - request, +export const loader = async ({ params, -}) => { + request, +}: LoaderArgs) => { const userId = await getUserId(request); const joke = await db.joke.findUnique({ where: { id: params.jokeId }, @@ -4821,21 +4729,20 @@ export const loader: LoaderFunction = async ({ status: 404, }); } - const data: LoaderData = { + return json({ joke, isOwner: userId === joke.jokesterId, - }; - return json(data); + }); }; -export const action: ActionFunction = async ({ - request, +export const action = async ({ params, -}) => { + request, +}: ActionArgs) => { const form = await request.formData(); - if (form.get("_method") !== "delete") { + if (form.get("intent") !== "delete") { throw new Response( - `The _method ${form.get("_method")} is not supported`, + `The intent ${form.get("intent")} is not supported`, { status: 400 } ); } @@ -4851,9 +4758,7 @@ export const action: ActionFunction = async ({ if (joke.jokesterId !== userId) { throw new Response( "Pssh, nice try. That's not your joke", - { - status: 403, - } + { status: 403 } ); } await db.joke.delete({ where: { id: params.jokeId } }); @@ -4861,7 +4766,7 @@ export const action: ActionFunction = async ({ }; export default function JokeRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -4870,12 +4775,12 @@ export default function JokeRoute() { {data.joke.name} Permalink {data.isOwner ? (
            - -
            @@ -4915,9 +4820,7 @@ export function CatchBoundary() { } } -export function ErrorBoundary({ error }: { error: Error }) { - console.error(error); - +export function ErrorBoundary() { const { jokeId } = useParams(); return (
            {`There was an error loading joke by the id ${jokeId}. Sorry.`}
            @@ -5102,38 +5005,37 @@ export default function IndexRoute() { app/routes/login.tsx -```tsx filename=app/routes/login.tsx lines=[4,25-31] +```tsx filename=app/routes/login.tsx lines=[4,22-25] import type { - ActionFunction, + ActionArgs, LinksFunction, MetaFunction, } from "@remix-run/node"; import { json } from "@remix-run/node"; import { + Link, useActionData, useSearchParams, - Link, } from "@remix-run/react"; +import stylesUrl from "~/styles/login.css"; import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; import { createUserSession, login, register, } from "~/utils/session.server"; -import stylesUrl from "~/styles/login.css"; -export const links: LinksFunction = () => { - return [{ rel: "stylesheet", href: stylesUrl }]; -}; +export const meta: MetaFunction = () => ({ + description: + "Login to submit your own jokes to Remix Jokes!", + title: "Remix Jokes | Login", +}); -export const meta: MetaFunction = () => { - return { - title: "Remix Jokes | Login", - description: - "Login to submit your own jokes to Remix Jokes!", - }; -}; +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: stylesUrl }, +]; function validateUsername(username: unknown) { if (typeof username !== "string" || username.length < 3) { @@ -5147,7 +5049,7 @@ function validatePassword(password: unknown) { } } -function validateUrl(url: any) { +function validateUrl(url: string) { let urls = ["/jokes", "/", "https://remix.run"]; if (urls.includes(url)) { return url; @@ -5155,25 +5057,7 @@ function validateUrl(url: any) { return "/jokes"; } -type ActionData = { - formError?: string; - fieldErrors?: { - username: string | undefined; - password: string | undefined; - }; - fields?: { - loginType: string; - username: string; - password: string; - }; -}; - -const badRequest = (data: ActionData) => - json(data, { status: 400 }); - -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const form = await request.formData(); const loginType = form.get("loginType"); const username = form.get("username"); @@ -5188,6 +5072,8 @@ export const action: ActionFunction = async ({ typeof redirectTo !== "string" ) { return badRequest({ + fieldErrors: null, + fields: null, formError: `Form not submitted correctly.`, }); } @@ -5197,14 +5083,20 @@ export const action: ActionFunction = async ({ username: validateUsername(username), password: validatePassword(password), }; - if (Object.values(fieldErrors).some(Boolean)) - return badRequest({ fieldErrors, fields }); + if (Object.values(fieldErrors).some(Boolean)) { + return badRequest({ + fieldErrors, + fields, + formError: null, + }); + } switch (loginType) { case "login": { const user = await login({ username, password }); if (!user) { return badRequest({ + fieldErrors: null, fields, formError: `Username/Password combination is incorrect`, }); @@ -5217,6 +5109,7 @@ export const action: ActionFunction = async ({ }); if (userExists) { return badRequest({ + fieldErrors: null, fields, formError: `User with username ${username} already exists`, }); @@ -5224,6 +5117,7 @@ export const action: ActionFunction = async ({ const user = await register({ username, password }); if (!user) { return badRequest({ + fieldErrors: null, fields, formError: `Something went wrong trying to create a new user.`, }); @@ -5232,6 +5126,7 @@ export const action: ActionFunction = async ({ } default: { return badRequest({ + fieldErrors: null, fields, formError: `Login type invalid`, }); @@ -5240,7 +5135,7 @@ export const action: ActionFunction = async ({ }; export default function Login() { - const actionData = useActionData(); + const actionData = useActionData(); const [searchParams] = useSearchParams(); return (
            @@ -5314,13 +5209,11 @@ export default function Login() { app/routes/jokes/$jokeId.tsx -```tsx filename=app/routes/jokes/$jokeId.tsx lines=[4,21-36] +```tsx filename=app/routes/jokes/$jokeId.tsx lines=[4,20-33] import type { - ActionFunction, - LoaderFunction, + ActionArgs, + LoaderArgs, MetaFunction, } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Link, - useLoaderData, useCatch, + useLoaderData, useParams, } from "@remix-run/react"; -import type { Joke } from "@prisma/client"; import { db } from "~/utils/db.server"; import { @@ -5394,10 +5286,8 @@ import { requireUserId, } from "~/utils/session.server"; -export const meta: MetaFunction = ({ +export const meta: MetaFunction = ({ data, -}: { - data: LoaderData | undefined; }) => { if (!data) { return { @@ -5411,12 +5301,10 @@ export const meta: MetaFunction = ({ }; }; -type LoaderData = { joke: Joke; isOwner: boolean }; - -export const loader: LoaderFunction = async ({ - request, +export const loader = async ({ params, -}) => { + request, +}: LoaderArgs) => { const userId = await getUserId(request); const joke = await db.joke.findUnique({ where: { id: params.jokeId }, @@ -5426,21 +5314,20 @@ export const loader: LoaderFunction = async ({ status: 404, }); } - const data: LoaderData = { + return json({ joke, isOwner: userId === joke.jokesterId, - }; - return json(data); + }); }; -export const action: ActionFunction = async ({ - request, +export const action = async ({ params, -}) => { + request, +}: ActionArgs) => { const form = await request.formData(); - if (form.get("_method") !== "delete") { + if (form.get("intent") !== "delete") { throw new Response( - `The _method ${form.get("_method")} is not supported`, + `The intent ${form.get("intent")} is not supported`, { status: 400 } ); } @@ -5456,9 +5343,7 @@ export const action: ActionFunction = async ({ if (joke.jokesterId !== userId) { throw new Response( "Pssh, nice try. That's not your joke", - { - status: 401, - } + { status: 403 } ); } await db.joke.delete({ where: { id: params.jokeId } }); @@ -5466,7 +5351,7 @@ export const action: ActionFunction = async ({ }; export default function JokeRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return (
            @@ -5475,12 +5360,12 @@ export default function JokeRoute() { {data.joke.name} Permalink {data.isOwner ? (
            - -
            @@ -5507,7 +5392,7 @@ export function CatchBoundary() {
            ); } - case 401: { + case 403: { return (
            Sorry, but {params.jokeId} is not your joke. @@ -5549,7 +5434,7 @@ For this one, you'll probably want to at least peek at the example unless you wa app/routes/jokes[.]rss.tsx ```tsx filename=app/routes/jokes[.]rss.tsx -import type { LoaderFunction } from "@remix-run/node"; +import type { LoaderArgs } from "@remix-run/node"; import { db } from "~/utils/db.server"; @@ -5566,9 +5451,7 @@ function escapeHtml(s: string) { .replace(/'/g, "'"); } -export const loader: LoaderFunction = async ({ - request, -}) => { +export const loader = async ({ request }: LoaderArgs) => { const jokes = await db.joke.findMany({ take: 100, orderBy: { createdAt: "desc" }, @@ -5816,18 +5699,18 @@ Note, you'll probably want to create a new file in `app/components/` called `jok app/components/joke.tsx -```tsx filename=app/components/joke.tsx -import { Link, Form } from "@remix-run/react"; +```tsx filename=app/components/joke.tsx lines=[19,22,29] import type { Joke } from "@prisma/client"; +import { Form, Link } from "@remix-run/react"; export function JokeDisplay({ - joke, - isOwner, canDelete = true, + isOwner, + joke, }: { - joke: Pick; - isOwner: boolean; canDelete?: boolean; + isOwner: boolean; + joke: Pick; }) { return (
            @@ -5836,15 +5719,12 @@ export function JokeDisplay({ {joke.name} Permalink {isOwner ? (
            - @@ -5861,31 +5741,29 @@ export function JokeDisplay({ app/routes/jokes/$jokeId.tsx -```tsx filename=app/routes/jokes/$jokeId.tsx lines=[19,97] +```tsx filename=app/routes/jokes/$jokeId.tsx lines=[14,89] import type { - LoaderFunction, - ActionFunction, + ActionArgs, + LoaderArgs, MetaFunction, } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { - useLoaderData, + Link, useCatch, + useLoaderData, useParams, } from "@remix-run/react"; -import type { Joke } from "@prisma/client"; +import { JokeDisplay } from "~/components/joke"; import { db } from "~/utils/db.server"; import { getUserId, requireUserId, } from "~/utils/session.server"; -import { JokeDisplay } from "~/components/joke"; -export const meta: MetaFunction = ({ +export const meta: MetaFunction = ({ data, -}: { - data: LoaderData | undefined; }) => { if (!data) { return { @@ -5899,14 +5777,11 @@ export const meta: MetaFunction = ({ }; }; -type LoaderData = { joke: Joke; isOwner: boolean }; - -export const loader: LoaderFunction = async ({ - request, +export const loader = async ({ params, -}) => { + request, +}: LoaderArgs) => { const userId = await getUserId(request); - const joke = await db.joke.findUnique({ where: { id: params.jokeId }, }); @@ -5915,21 +5790,20 @@ export const loader: LoaderFunction = async ({ status: 404, }); } - const data: LoaderData = { + return json({ joke, isOwner: userId === joke.jokesterId, - }; - return json(data); + }); }; -export const action: ActionFunction = async ({ - request, +export const action = async ({ params, -}) => { + request, +}: ActionArgs) => { const form = await request.formData(); - if (form.get("_method") !== "delete") { + if (form.get("intent") !== "delete") { throw new Response( - `The _method ${form.get("_method")} is not supported`, + `The intent ${form.get("intent")} is not supported`, { status: 400 } ); } @@ -5945,9 +5819,7 @@ export const action: ActionFunction = async ({ if (joke.jokesterId !== userId) { throw new Response( "Pssh, nice try. That's not your joke", - { - status: 401, - } + { status: 403 } ); } await db.joke.delete({ where: { id: params.jokeId } }); @@ -5955,10 +5827,10 @@ export const action: ActionFunction = async ({ }; export default function JokeRoute() { - const data = useLoaderData(); + const data = useLoaderData(); return ( - + ); } @@ -5980,7 +5852,7 @@ export function CatchBoundary() {
            ); } - case 401: { + case 403: { return (
            Sorry, but {params.jokeId} is not your joke. @@ -5993,9 +5865,7 @@ export function CatchBoundary() { } } -export function ErrorBoundary({ error }: { error: Error }) { - console.error(error); - +export function ErrorBoundary() { const { jokeId } = useParams(); return (
            {`There was an error loading joke by the id ${jokeId}. Sorry.`}
            @@ -6009,12 +5879,12 @@ export function ErrorBoundary({ error }: { error: Error }) { app/routes/jokes/new.tsx -```tsx filename=app/routes/jokes/new.tsx lines=[11,14,91-111] +```tsx filename=app/routes/jokes/new.tsx lines=[7,11,14,79,81-99,104,173] import type { - ActionFunction, - LoaderFunction, + ActionArgs, + LoaderArgs, } from "@remix-run/node"; -import { redirect, json } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; import { Form, Link, @@ -6025,14 +5895,13 @@ import { import { JokeDisplay } from "~/components/joke"; import { db } from "~/utils/db.server"; +import { badRequest } from "~/utils/request.server"; import { - requireUserId, getUserId, + requireUserId, } from "~/utils/session.server"; -export const loader: LoaderFunction = async ({ - request, -}) => { +export const loader = async ({ request }: LoaderArgs) => { const userId = await getUserId(request); if (!userId) { throw new Response("Unauthorized", { status: 401 }); @@ -6052,24 +5921,7 @@ function validateJokeName(name: string) { } } -type ActionData = { - formError?: string; - fieldErrors?: { - name: string | undefined; - content: string | undefined; - }; - fields?: { - name: string; - content: string; - }; -}; - -const badRequest = (data: ActionData) => - json(data, { status: 400 }); - -export const action: ActionFunction = async ({ - request, -}) => { +export const action = async ({ request }: ActionArgs) => { const userId = await requireUserId(request); const form = await request.formData(); const name = form.get("name"); @@ -6079,6 +5931,8 @@ export const action: ActionFunction = async ({ typeof content !== "string" ) { return badRequest({ + fieldErrors: null, + fields: null, formError: `Form not submitted correctly.`, }); } @@ -6089,7 +5943,11 @@ export const action: ActionFunction = async ({ }; const fields = { name, content }; if (Object.values(fieldErrors).some(Boolean)) { - return badRequest({ fieldErrors, fields }); + return badRequest({ + fieldErrors, + fields, + formError: null, + }); } const joke = await db.joke.create({ @@ -6099,7 +5957,7 @@ export const action: ActionFunction = async ({ }; export default function NewJokeRoute() { - const actionData = useActionData(); + const actionData = useActionData(); const transition = useTransition(); if (transition.submission) { @@ -6212,9 +6070,7 @@ export function CatchBoundary() { } } -export function ErrorBoundary({ error }: { error: Error }) { - console.error(error); - +export function ErrorBoundary() { return (
            Something unexpected went wrong. Sorry about that. From 75d133ff3e443708a9ba77e3c9387a4e25516fd5 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 30 Nov 2022 21:31:00 +0000 Subject: [PATCH 15/77] chore: format --- docs/guides/api-routes.md | 4 +++- docs/guides/data-writes.md | 12 +++--------- docs/guides/envvars.md | 2 +- docs/guides/optimistic-ui.md | 16 ++++------------ 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/docs/guides/api-routes.md b/docs/guides/api-routes.md index 789bedd010c..3d0d578fc30 100644 --- a/docs/guides/api-routes.md +++ b/docs/guides/api-routes.md @@ -18,7 +18,9 @@ export async function loader() { } export default function Teams() { - return ()} />; + return ( + ()} /> + ); } ``` diff --git a/docs/guides/data-writes.md b/docs/guides/data-writes.md index d6bd93119cb..ed52e81af47 100644 --- a/docs/guides/data-writes.md +++ b/docs/guides/data-writes.md @@ -180,9 +180,7 @@ import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno // Note the "action" export name, this will handle our form POST -export const action = async ({ - request, -}: ActionArgs) => { +export const action = async ({ request }: ActionArgs) => { const formData = await request.formData(); const project = await createProject(formData); return redirect(`/projects/${project.id}`); @@ -212,9 +210,7 @@ const [errors, project] = await createProject(formData); If there are validation errors, we want to go back to the form and display them. ```tsx lines=[5,7-10] -export const action = async ({ - request, -}: ActionArgs) => { +export const action = async ({ request }: ActionArgs) => { const formData = await request.formData(); const [errors, project] = await createProject(formData); @@ -234,9 +230,7 @@ import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { useActionData } from "@remix-run/react"; -export const action = async ({ - request, -}: ActionArgs) => { +export const action = async ({ request }: ActionArgs) => { // ... }; diff --git a/docs/guides/envvars.md b/docs/guides/envvars.md index 3516f5ab50e..a4ad4241c2d 100644 --- a/docs/guides/envvars.md +++ b/docs/guides/envvars.md @@ -51,7 +51,7 @@ If you're using the `@remix-run/cloudflare-pages` adapter, env variables work a Then, in your `loader` functions, you can access environment variables directly on `context`: ```tsx -export const loader = async ({ context }:LoaderArgs) => { +export const loader = async ({ context }: LoaderArgs) => { console.log(context.SOME_SECRET); }; ``` diff --git a/docs/guides/optimistic-ui.md b/docs/guides/optimistic-ui.md index 95d1b293b88..7f64687f981 100644 --- a/docs/guides/optimistic-ui.md +++ b/docs/guides/optimistic-ui.md @@ -66,9 +66,7 @@ import { Form } from "@remix-run/react"; import { createProject } from "~/utils"; -export const action = async ({ - request, -}: ActionArgs) => { +export const action = async ({ request }: ActionArgs) => { const body = await request.formData(); const newProject = Object.fromEntries(body); const project = await createProject(newProject); @@ -101,9 +99,7 @@ import { Form, useTransition } from "@remix-run/react"; import { createProject } from "~/utils"; -export const action = async ({ - request, -}: ActionArgs) => { +export const action = async ({ request }: ActionArgs) => { const body = await request.formData(); const newProject = Object.fromEntries(body); const project = await createProject(newProject); @@ -145,9 +141,7 @@ import { Form, useTransition } from "@remix-run/react"; import { createProject } from "~/utils"; import { ProjectView } from "~/components/project"; -export const action = async ({ - request, -}: ActionArgs) => { +export const action = async ({ request }: ActionArgs) => { const body = await request.formData(); const newProject = Object.fromEntries(body); const project = await createProject(newProject); @@ -198,9 +192,7 @@ import { import { createProject } from "~/utils"; import { ProjectView } from "~/components/project"; -export const action = async ({ - request, -}: ActionArgs) => { +export const action = async ({ request }: ActionArgs) => { const body = await request.formData(); const newProject = Object.fromEntries(body); try { From 44321745bf8cbe5fccfc0e9bb2b1af13cc2f2e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Wed, 30 Nov 2022 23:23:56 +0100 Subject: [PATCH 16/77] docs: fix code highlighting (#4723) --- docs/guides/data-writes.md | 6 +-- docs/guides/optimistic-ui.md | 92 ++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/docs/guides/data-writes.md b/docs/guides/data-writes.md index ed52e81af47..51d96d3d17b 100644 --- a/docs/guides/data-writes.md +++ b/docs/guides/data-writes.md @@ -175,7 +175,7 @@ export default function NewProject() { Now add the route action. Any form submissions that are "post" will call your data "action". Any "get" submissions (``) will be handled by your "loader". -```tsx lines=[5-11] +```tsx lines=[1,5-9] import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno @@ -209,7 +209,7 @@ const [errors, project] = await createProject(formData); If there are validation errors, we want to go back to the form and display them. -```tsx lines=[5,7-10] +```tsx lines=[3,5-8] export const action = async ({ request }: ActionArgs) => { const formData = await request.formData(); const [errors, project] = await createProject(formData); @@ -225,7 +225,7 @@ export const action = async ({ request }: ActionArgs) => { Just like `useLoaderData` returns the values from the `loader`, `useActionData` will return the data from the action. It will only be there if the navigation was a form submission, so you always have to check if you've got it or not. -```tsx lines=[3,12,22,27-31,39,44-48] +```tsx lines=[3,10,20,25-29,37,42-46] import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { useActionData } from "@remix-run/react"; diff --git a/docs/guides/optimistic-ui.md b/docs/guides/optimistic-ui.md index 7f64687f981..ed050289ce1 100644 --- a/docs/guides/optimistic-ui.md +++ b/docs/guides/optimistic-ui.md @@ -79,7 +79,7 @@ export default function NewProject() {

            New Project