Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active March 9, 2022 22:50
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanflorence/ddc5604a8ae9e068ef4e4478e8fa845a to your computer and use it in GitHub Desktop.
Save ryanflorence/ddc5604a8ae9e068ef4e4478e8fa845a to your computer and use it in GitHub Desktop.
The Anatomy of a Remix Route
/**
* 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