From a3c538f95cb568d37c166a18ece72396317ae42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Mon, 12 Dec 2022 21:39:03 +0100 Subject: [PATCH] docs: fix `useActionData`/`useLoaderData` usage --- docs/file-conventions/routes-files.md | 28 +++---- docs/guides/file-uploads.md | 15 ++-- docs/guides/routing.md | 10 +-- docs/hooks/use-action-data.md | 18 +++-- docs/hooks/use-fetcher.md | 20 ++--- docs/hooks/use-loader-data.md | 2 +- docs/hooks/use-submit.md | 5 +- docs/route/action.md | 7 +- docs/route/loader.md | 73 ++++++++++--------- docs/route/meta.md | 14 ++-- docs/route/should-reload.md | 19 +++-- docs/utils/cookies.md | 24 +++--- docs/utils/json.md | 9 +-- docs/utils/parse-multipart-form-data.md | 6 +- docs/utils/redirect.md | 7 +- docs/utils/sessions.md | 49 +++++++------ .../unstable-create-file-upload-handler.md | 4 +- .../unstable-create-memory-upload-handler.md | 4 +- 18 files changed, 158 insertions(+), 156 deletions(-) diff --git a/docs/file-conventions/routes-files.md b/docs/file-conventions/routes-files.md index 61db542523b..8646c239dcd 100644 --- a/docs/file-conventions/routes-files.md +++ b/docs/file-conventions/routes-files.md @@ -81,21 +81,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); }; @@ -244,21 +240,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["*"]); }; diff --git a/docs/guides/file-uploads.md b/docs/guides/file-uploads.md index b6a9cbf4314..108bd02330c 100644 --- a/docs/guides/file-uploads.md +++ b/docs/guides/file-uploads.md @@ -9,13 +9,16 @@ 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}"; -// writeAsyncIterableToWritable is a Node-only utility -import { writeAsyncIterableToWritable } from "@remix-run/node"; + unstable_parseMultipartFormData, +} from "@remix-run/node"; // or cloudflare/deno +import { writeAsyncIterableToWritable } from "@remix-run/node"; // `writeAsyncIterableToWritable` is a Node-only utility import type { UploadApiOptions, UploadApiResponse, @@ -51,9 +54,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( diff --git a/docs/guides/routing.md b/docs/guides/routing.md index c30c8cdcb19..886d2bc710d 100644 --- a/docs/guides/routing.md +++ b/docs/guides/routing.md @@ -305,14 +305,14 @@ Prefixing a file name with `$` will make that route path a **dynamic segment**. For example, the `$invoiceId.jsx` route. When the url is `/sales/invoices/102000`, Remix will provide the string value `102000` to your loaders, actions, and components by the same name as the filename segment: -```jsx +```tsx import { useParams } from "@remix-run/react"; -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { const id = params.invoiceId; } -export async function action({ params }) { +export async function action({ params }: ActionArgs) { const id = params.invoiceId; } @@ -361,8 +361,8 @@ 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 async function loader({ params }) { +```tsx +export async function loader({ params }: LoaderArgs) { params["*"]; // "images/work/flyer.jpg" } ``` diff --git a/docs/hooks/use-action-data.md b/docs/hooks/use-action-data.md index 77da0de9c49..d10724f9ae6 100644 --- a/docs/hooks/use-action-data.md +++ b/docs/hooks/use-action-data.md @@ -6,18 +6,19 @@ title: useActionData 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"; +import { Form, useActionData } 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 (

@@ -34,11 +35,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] -import { redirect, json } from "@remix-run/node"; // or cloudflare/deno +```tsx lines=[23,32,40-42,46-48] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno +import { json, redirect } 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"); @@ -65,7 +67,7 @@ export async function action({ request }) { } export default function Signup() { - const errors = useActionData(); + const errors = useActionData(); return ( <> diff --git a/docs/hooks/use-fetcher.md b/docs/hooks/use-fetcher.md index 73e96395025..305289b8916 100644 --- a/docs/hooks/use-fetcher.md +++ b/docs/hooks/use-fetcher.md @@ -182,13 +182,13 @@ 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); - return json({ ok: true }); + return json({ error: null, ok: true }); } catch (error) { - return json({ error: error.message }); + return json({ error: error.message, ok: false }); } } ``` @@ -243,12 +243,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 (

@@ -306,7 +306,7 @@ import { Form } from "@remix-run/react"; import { NewsletterForm } from "~/NewsletterSignup"; export default function NewsletterSignupRoute() { - const data = useActionData(); + const data = useActionData(); return ( (); const [showDetails, setShowDetails] = useState(false); useEffect(() => { @@ -382,7 +382,7 @@ function UserAvatar({ partialUser }) { If the user needs to select a city, you could have a loader that returns a list of cities based on a query and plug it into a Reach UI combobox: ```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("city-query")) @@ -390,7 +390,7 @@ export async function loader({ request }) { } function CitySearchCombobox() { - const cities = useFetcher(); + const cities = useFetcher(); return ( diff --git a/docs/hooks/use-loader-data.md b/docs/hooks/use-loader-data.md index ca19dcf3d62..7b5311328b0 100644 --- a/docs/hooks/use-loader-data.md +++ b/docs/hooks/use-loader-data.md @@ -17,7 +17,7 @@ export async function loader() { } export default function Invoices() { - const invoices = useLoaderData(); + const invoices = useLoaderData(); // ... } ``` diff --git a/docs/hooks/use-submit.md b/docs/hooks/use-submit.md index 1d39e97fb8c..6e6bc86e8da 100644 --- a/docs/hooks/use-submit.md +++ b/docs/hooks/use-submit.md @@ -8,7 +8,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"; @@ -16,7 +17,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"); } diff --git a/docs/route/action.md b/docs/route/action.md index b45b2765b2f..fc1ca2fe31a 100644 --- a/docs/route/action.md +++ b/docs/route/action.md @@ -13,17 +13,18 @@ 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"; -import { fakeGetTodos, fakeCreateTodo } from "~/utils/db"; import { TodoList } from "~/components/TodoList"; +import { fakeCreateTodo, fakeGetTodos } from "~/utils/db"; 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"), @@ -32,7 +33,7 @@ export async function action({ request }) { } export default function Todos() { - const data = useLoaderData(); + const data = useLoaderData(); return (

diff --git a/docs/route/loader.md b/docs/route/loader.md index a87b02d361f..b72bf2a6650 100644 --- a/docs/route/loader.md +++ b/docs/route/loader.md @@ -8,9 +8,11 @@ title: loader Each route can define a "loader" function that provides data to the route when rendering. -```js +```tsx +import { json } from "@remix-run/node"; // or cloudflare/deno + export const loader = async () => { - return { ok: true }; + return json({ ok: true }); }; ``` @@ -20,18 +22,17 @@ This means you can talk directly to your database, use server only API secrets, Using the database ORM Prisma as an example: -```tsx lines=[3,5-8] +```tsx lines=[3,5-7] import { useLoaderData } from "@remix-run/react"; import { prisma } from "../db"; export async function loader() { - const users = await prisma.user.findMany(); - return users; + return json(await prisma.user.findMany()); } export default function Users() { - const data = useLoaderData(); + const data = useLoaderData(); return (
    {data.map((user) => ( @@ -46,14 +47,14 @@ Because `prisma` is only used in the loader it will be removed from the browser ## Type Safety -You can get type safety over the network for your loader and component with `DataFunctionArgs` and `useLoaderData`. +You can get type safety over the network for your loader and component with `LoaderArgs` and `useLoaderData`. -```tsx lines=[5,10] +```tsx lines=[1,5,10] +import type { LoaderArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import type { DataFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; -export async function loader(args: DataFunctionArgs) { +export async function loader(args: LoaderArgs) { return json({ name: "Ryan", date: new Date() }); } @@ -69,21 +70,21 @@ export default function SomeRoute() { Route params are defined by route file names. If a segment begins with `$` like `$invoiceId`, the value from the URL for that segment will be passed to your loader. -```ts filename=app/routes/invoices/$invoiceId.jsx +```tsx filename=app/routes/invoices/$invoiceId.tsx nocopy // if the user visits /invoices/123 -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { params.invoiceId; // "123" } ``` Params are mostly useful for looking up records by ID: -```ts filename=app/routes/invoices/$invoiceId.jsx +```tsx filename=app/routes/invoices/$invoiceId.tsx // if the user visits /invoices/123 -export async function loader({ params }) { +export async function loader({ params }: LoaderArgs) { const invoice = await fakeDb.getInvoice(params.invoiceId); if (!invoice) throw new Response("", { status: 404 }); - return invoice; + return json(invoice); } ``` @@ -94,7 +95,7 @@ This is a [Fetch Request][request] instance. You can read the MDN docs to see al The most common use cases in loaders are reading headers (like cookies) and URL [URLSearchParams][urlsearchparams] from the request: ```tsx -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { // read a cookie const cookie = request.headers.get("Cookie"); @@ -130,8 +131,8 @@ app.all( And then your loader can access it. -```ts filename=routes/some-route.tsx -export async function loader({ context }) { +```tsx filename=routes/some-route.tsx +export async function loader({ context }: LoaderArgs) { const { expressUser } = context; // ... } @@ -141,7 +142,7 @@ export async function loader({ context }) { You need to return a [Fetch Response][response] from your loader. -```ts +```tsx export async function loader() { const users = await db.users.findMany(); const body = JSON.stringify(users); @@ -153,12 +154,12 @@ export async function loader() { } ``` -Using the `json` helper simplifies this so you don't have to construct them yourself, but these two examples are effectively the same! +Using the `json` helper simplifies this, so you don't have to construct them yourself, but these two examples are effectively the same! ```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); }; @@ -169,9 +170,7 @@ You can see how `json` just does a little of the work to make your loader a lot ```tsx 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 }, }); @@ -232,15 +231,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; @@ -250,15 +248,18 @@ 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); @@ -270,7 +271,7 @@ export default function InvoiceRoute() { } export function CatchBoundary() { - // this returns { status, statusText, data } + // this returns { data, status, statusText } const caught = useCatch(); switch (caught.status) { diff --git a/docs/route/meta.md b/docs/route/meta.md index 6fe7df14429..e41b3d11a16 100644 --- a/docs/route/meta.md +++ b/docs/route/meta.md @@ -90,26 +90,22 @@ 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 -const loader = () => { +```tsx filename=app/routes/sales.tsx +export const loader = async () => { 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 = () => { +export const loader = async () => { return json({ name: "Customer name" }); }; const meta: MetaFunction< typeof loader, - { - "routes/sales": SalesLoader; - } + { "routes/sales": typeof salesLoader } > = ({ data, parentsData }) => { const { name } = data; // ^? string diff --git a/docs/route/should-reload.md b/docs/route/should-reload.md index 3694ef59d2a..29e8d70f3fb 100644 --- a/docs/route/should-reload.md +++ b/docs/route/should-reload.md @@ -29,15 +29,15 @@ export const unstable_shouldReload: ShouldReloadFunction = }) => false; // or `true`; ``` -During client-side transitions, Remix will optimize reloading of routes that are already rendering, like not reloading layout routes that aren't changing. In other cases, like form submissions or search param changes, Remix doesn't know which routes need to be reloaded so it reloads them all to be safe. This ensures data mutations from the submission or changes in the search params are reflected across the entire page. +During client-side transitions, Remix will optimize reloading of routes that are already rendering, like not reloading layout routes that aren't changing. In other cases, like form submissions or search param changes, Remix doesn't know which routes need to be reloaded, so it reloads them all to be safe. This ensures data mutations from the submission or changes in the search params are reflected across the entire page. -This function lets apps further optimize by returning `false` when Remix is about to reload a route. There are three cases when Remix will reload a route and you have the opportunity to optimize: +This function lets apps further optimize by returning `false` when Remix is about to reload a route. There are three cases when Remix will reload a route, and you have the opportunity to optimize: - if the `url.search` changes (while the `url.pathname` is the same) - after actions are called - "refresh" link clicks (click link to same URL) -Otherwise Remix will reload the route and you have no choice: +Otherwise, Remix will reload the route, and you have no choice: - A route matches the new URL that didn't match before - The `url.pathname` changed (including route params) @@ -48,7 +48,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: { @@ -74,7 +74,7 @@ Consider these routes: └── activity.tsx ``` -And lets say the UI looks something like this: +And let's say the UI looks something like this: ``` +------------------------------+ @@ -93,8 +93,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=[2,8] +export async function loader({ + params, + request, +}: LoaderArgs) { const url = new URL(request.url); return json( await exampleDb.activity.findAll({ @@ -114,7 +117,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/utils/cookies.md b/docs/utils/cookies.md index f25192bb0ed..56c2f1f065c 100644 --- a/docs/utils/cookies.md +++ b/docs/utils/cookies.md @@ -30,7 +30,11 @@ 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-20,24] +import type { + ActionArgs, + LoaderArgs, +} from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData, @@ -40,14 +44,14 @@ import { import { userPrefs } from "~/cookies"; -export async function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const cookieHeader = request.headers.get("Cookie"); const cookie = (await userPrefs.parse(cookieHeader)) || {}; return json({ showBanner: cookie.showBanner }); } -export async function action({ request }) { +export async function action({ request }: ActionArgs) { const cookieHeader = request.headers.get("Cookie"); const cookie = (await userPrefs.parse(cookieHeader)) || {}; @@ -65,7 +69,7 @@ export async function action({ request }) { } export default function Home() { - const { showBanner } = useLoaderData(); + const { showBanner } = useLoaderData(); return (
    @@ -129,14 +133,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"], }); +``` + +```tsx 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); diff --git a/docs/utils/json.md b/docs/utils/json.md index c1575a36d88..6b947cbd24b 100644 --- a/docs/utils/json.md +++ b/docs/utils/json.md @@ -6,11 +6,10 @@ title: json This is a shortcut for creating `application/json` responses. It assumes you are using `utf-8` encoding. -```ts lines=[2,6] -import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno +```tsx lines=[1,5] import { json } from "@remix-run/node"; // or cloudflare/deno -export const loader: LoaderFunction = async () => { +export const loader = async () => { // So you can write this: return json({ any: "thing" }); @@ -25,8 +24,8 @@ export const loader: LoaderFunction = async () => { You can also pass a status code and headers: -```ts lines=[4-9] -export const loader: LoaderFunction = async () => { +```tsx lines=[4-9] +export const loader = async () => { return json( { not: "coffee" }, { diff --git a/docs/utils/parse-multipart-form-data.md b/docs/utils/parse-multipart-form-data.md index 17a74ee80f7..07d6d76d2c9 100644 --- a/docs/utils/parse-multipart-form-data.md +++ b/docs/utils/parse-multipart-form-data.md @@ -17,10 +17,8 @@ It's to be used in place of `request.formData()`. For example: -```tsx lines=[4-7,9,25] -export const action: ActionFunction = async ({ - request, -}) => { +```tsx lines=[2-5,7,23] +export const action = async ({ request }: ActionArgs) => { const formData = await unstable_parseMultipartFormData( request, uploadHandler // <-- we'll look at this deeper next diff --git a/docs/utils/redirect.md b/docs/utils/redirect.md index 6e3c8a6b51c..6d5d04dbd30 100644 --- a/docs/utils/redirect.md +++ b/docs/utils/redirect.md @@ -6,11 +6,10 @@ title: redirect This is shortcut for sending 30x responses. -```ts lines=[2,8] -import type { ActionFunction } from "@remix-run/node"; // or cloudflare/deno +```tsx 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) { @@ -21,7 +20,7 @@ export const action: ActionFunction = async () => { }; ``` -By default it sends 302, but you can change it to whichever redirect status code you'd like: +By default, it sends 302, but you can change it to whichever redirect status code you'd like: ```ts redirect(path, 301); diff --git a/docs/utils/sessions.md b/docs/utils/sessions.md index 4a93c5ad5be..ac5beef9cd7 100644 --- a/docs/utils/sessions.md +++ b/docs/utils/sessions.md @@ -57,13 +57,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") ); @@ -82,7 +86,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") ); @@ -117,7 +121,8 @@ export async function action({ request }) { } export default function Login() { - const { currentUser, error } = useLoaderData(); + const { currentUser, error } = + useLoaderData(); return (
    @@ -144,9 +149,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") ); @@ -276,8 +279,7 @@ This storage keeps all the cookie information in your server's memory. This should only be used in development. Use one of the other methods in production. -```js -// app/sessions.js +```js filename=app/sessions.js import { createCookie, createMemorySessionStorage, @@ -334,8 +336,7 @@ For [Cloudflare KV][cloudflare-kv] backed sessions, use `createCloudflareKVSessi The advantage of KV backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a globally replicated, low-latency data store with exceptionally high read volumes with low-latency. -```js -// app/sessions.server.js +```js filename=app/sessions.server.js import { createCookie, createCloudflareKVSessionStorage, @@ -370,8 +371,7 @@ sessions _ttl TTL ``` -```js -// app/sessions.server.js +```js filename=app/sessions.server.js import { createCookie, createArcTableSessionStorage, @@ -402,8 +402,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") ); @@ -433,10 +433,13 @@ 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 -import { getSession, commitSession } from "../sessions"; +```tsx +import { commitSession, getSession } from "../sessions"; -export async function action({ request, params }) { +export async function action({ + params, + request, +}: ActionArgs) { const session = await getSession( request.headers.get("Cookie") ); @@ -461,7 +464,7 @@ 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 { json } from "@remix-run/node"; // or cloudflare/deno import { Meta, @@ -472,7 +475,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") ); @@ -490,7 +493,7 @@ export async function loader({ request }) { } export default function App() { - const { message } = useLoaderData(); + const { message } = useLoaderData(); return ( @@ -528,8 +531,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, { diff --git a/docs/utils/unstable-create-file-upload-handler.md b/docs/utils/unstable-create-file-upload-handler.md index 2bbedbdb1b3..4133d8d592e 100644 --- a/docs/utils/unstable-create-file-upload-handler.md +++ b/docs/utils/unstable-create-file-upload-handler.md @@ -9,9 +9,9 @@ A Node.js upload handler that will write parts with a filename to disk to keep t **Example:** ```tsx -export const action: ActionFunction = async ({ +export const action = async ({ request, -}) => { +}: ActionArgs) => { const uploadHandler = unstable_composeUploadHandlers( unstable_createFileUploadHandler({ maxPartSize: 5_000_000, diff --git a/docs/utils/unstable-create-memory-upload-handler.md b/docs/utils/unstable-create-memory-upload-handler.md index 841da5fa0f8..341537665ff 100644 --- a/docs/utils/unstable-create-memory-upload-handler.md +++ b/docs/utils/unstable-create-memory-upload-handler.md @@ -7,9 +7,9 @@ title: unstable_createMemoryUploadHandler **Example:** ```tsx -export const action: ActionFunction = async ({ +export const action = async ({ request, -}) => { +}: ActionArgs) => { const uploadHandler = unstable_createMemoryUploadHandler({ maxPartSize: 500_000, });