Last active
March 9, 2022 22:50
-
-
Save ryanflorence/ddc5604a8ae9e068ef4e4478e8fa845a to your computer and use it in GitHub Desktop.
The Anatomy of a Remix Route
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The Anatomy of a Remix Route | |
*/ | |
import { parseFormBody, json, redirect } from "@remix-run/data"; | |
import { | |
Form, | |
useRouteData, | |
usePendingFormSubmit, | |
preload, | |
} from "@remix-run/react"; | |
import slugify from "slugify"; | |
import { useState } from "react"; | |
import type { Loader, Action } from "@remix-run/data"; | |
import * as Post from "../models/post"; | |
import type { IPost } from "../models/post"; | |
/** | |
* This function runs on the server and provides data for the component on both | |
* the initial server rendered document GET request and the fetch requests in | |
* the browser on client side navigations. So whether the user got here by | |
* navigating in the browser with React Router, or landed here initially, you | |
* can handle the data loading the same way and know that it's only ever going | |
* to run on the server. | |
*/ | |
export let loader: Loader = async ({ params }) => { | |
/** | |
* The `params` are parsed from the URL, and defined by the name of the file. | |
* In this case, the file name is $post.edit.tsx, which becomes the url | |
* pattern `/:post/edit`, so whatever is in the URL at the `:post` position | |
* becomes `params.post` in here. (Dots in file names become `/` in the URL, | |
* you can also nest files in folder to create `/` in a URL, but that also | |
* becomes a nested React tree, but that's a different conversation!) | |
*/ | |
let post = await Post.getBySlug(params.post); | |
return json(post.data, { | |
headers: { | |
"cache-control": "max-age=0, no-cache, no-store, must-revalidate", | |
}, | |
}); | |
}; | |
/** | |
* Like `loader`, this function also runs on the server, however it gets called | |
* for all non-GET http requests. When apps use the Remix's Form `<Form>`, | |
* remix serializes the form and makes the POST to the server the very same as | |
* a native `<form>` (in fact, you can use either one, you just can't do custom | |
* loading indication or optimistic UI with `<form>`, and often that's fine!). | |
*/ | |
export let action: Action = async ({ request, params }) => { | |
let formData = await parseFormBody(request); | |
let updates = (Object.fromEntries(formData) as unknown) as IPost; | |
await Post.updateBySlug(params.post, updates); | |
return redirect(`/${params.post}?latest`); | |
}; | |
/** | |
* This function provides the HTTP headers on the initial server rendered | |
* document request. For the browser navigation fetch requests, the loader's | |
* headers are used. | |
*/ | |
export function headers({ loaderHeaders }: { loaderHeaders: Headers }) { | |
return { | |
// cache control is usually best handled by the data loader, so the loader | |
// headers are passed here | |
"cache-control": loaderHeaders.get("cache-control"), | |
}; | |
} | |
/** | |
* While not so relevant for an "edit post" page, meta tags are an important | |
* part of SEO optimization. Data is passed to this function so your meta tags | |
* can be data driven. While defined in routes and nested routes, your meta | |
* tags will server render in the head of the document, and stay up to date as | |
* the user navigates around. | |
*/ | |
export function meta({ data: post }: { data: IPost }) { | |
return { | |
title: `Edit: ${post.title}`, | |
description: "Make changes to this post.", | |
}; | |
} | |
/** | |
* This function allows you to add `<link/>` tags to the head when this route | |
* is rendered: like a new favicon, or more likely, preloading resources with | |
* `<link rel="preload"/>`. | |
*/ | |
export let links: Links = ({ parent }) => { | |
return parent.concat([ | |
/** | |
* Because Remix created your browser bundles, it can help you preload | |
* assets to other routes. | |
* | |
* After this form is submit, we know the app will navigate to the post, so | |
* we can preload the code split bundle for that route to speed up the | |
* transition without blocking the initial render (the browser will preload | |
* this in idle time). | |
* | |
* You can preload the JS, CSS, and even data for another route if you | |
* want. By default, Remix only downloads the code for the page the user is | |
* looking at, you are in control of the network tab on every page in | |
* Remix. | |
* | |
* This renders: | |
* <link rel="preload" href="/build/[routeId]-[hash].js" as="script" /> | |
* <link rel="preload" href="/build/[routeId]-[hash].css" as="style" /> | |
*/ | |
preload.route("routes/$post", "script,style"), | |
]); | |
}; | |
/** | |
* Any errors you didn't expect and didn't handle yourself in this route will | |
* cause this component to be rendered instead of the default exported | |
* component. Whether the error was thrown in your component render cycle, the | |
* loader, or the action, this is the code path that will be taken. With react | |
* router's nested routes (parts of the URL are represented by nested React | |
* component trees), Remix will still render the parent layout route this route | |
* is inside of. This makes it really easy for the user to recover from | |
* unexpected errors: a lot of the page still rendered correctly so they can | |
* click a different link (or the same one) in the layout route. | |
*/ | |
export function ErrorBoundary({ error }: { error: Error }) { | |
console.error(error); | |
return ( | |
<div> | |
<h2>Oops!</h2> | |
<p> | |
There was an unexpected error on the edit post page. Try reloading the | |
page. | |
</p> | |
</div> | |
); | |
} | |
/** | |
* The only required export is the actual React Component for this route. | |
*/ | |
export default function EditPost() { | |
/** | |
* Whatever data was returned from the `loader` is returned from this hook. | |
*/ | |
let post = useRouteData<IPost>(); | |
/** | |
* When a `<Form>` is submit, Remix serializes the form and makes the POST to | |
* the `action` function in this component on the server. While that request | |
* is pending, the pending form submit is returned from this hook, allowing | |
* you to disable the form, creating loading indicators, or build optimistic | |
* UI. In this component we just disable the fields. | |
*/ | |
let pendingForm = usePendingFormSubmit(); | |
let [customSlug, setCustomSlug] = useState(post.slug); | |
let slug = slugify(customSlug, { | |
lower: true, | |
strict: true, | |
}); | |
return ( | |
/** | |
* The <Form> component works just like <form> except it allows you to | |
* build custom loading experiences and optimistic UI without having to | |
* manage form state or deal with pending/error states in the request to | |
* the server. | |
*/ | |
<Form method="post"> | |
<fieldset disabled={!!usePendingFormSubmit()}> | |
<h1>Edit Post</h1> | |
<p> | |
<label htmlFor="title">Title</label> | |
<br /> | |
<input | |
type="text" | |
name="title" | |
id="title" | |
defaultValue={post.title} | |
/> | |
</p> | |
<p> | |
<label htmlFor="slug">URL Slug</label> | |
<br /> | |
<input | |
type="text" | |
id="slug" | |
defaultValue={slug} | |
onChange={(e) => setCustomSlug(e.target.value)} | |
/> | |
<br /> | |
<i style={{ color: "#888" }}>/{slug}</i> | |
</p> | |
<p> | |
<label htmlFor="body">Body</label> | |
<br /> | |
<textarea | |
defaultValue={post.body} | |
name="body" | |
id="body" | |
rows={20} | |
cols={50} | |
/> | |
</p> | |
<p> | |
<button type="submit">Update</button> | |
</p> | |
</fieldset> | |
</Form> | |
); | |
} | |
/** | |
* And that is the anatomy of a Remix Route. | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment