One of the primary features of Remix is the way it automatically keeps your UI in sync with persistent server state. It happens in three steps:
Let's consider a user account edit route. The route module has three exports that we'll fill in and talk about:
export async function loader() {
// provides data to the component
}
export default function Component() {
// renders the UI
}
export async function action() {
// updates persistent data
}
Route files can export a loader
function that provides data to the route component. When the user navigates to a matching route, the data is first loaded and then the page is rendered.
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}
export default function Component() {
// ...
}
export async function action() {
// ...
}
The default export of the route file is the component that renders. It reads the loader data with useLoaderData
:
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}
export default function Component() {
const user = useLoaderData<typeof loader>();
return (
<Form action="/account">
<h1>Settings for {user.displayName}</h1>
<input
name="displayName"
defaultValue={user.displayName}
/>
<input name="email" defaultValue={user.email} />
<button type="submit">Save</button>
</Form>
);
}
export async function action() {
// ...
}
Finally, the action on the route matching the form's action attribute is called when the form is submitted. In this example it's the same route. The values in the form fields will be available on the standard request.formData()
API. Note the name
attribute on the inputs is coupled to the formData.get(fieldName)
getter.
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}
export default function Component() {
const user = useLoaderData<typeof loader>();
return (
<Form action="/account">
<h1>Settings for {user.displayName}</h1>
<input
name="displayName"
defaultValue={user.displayName}
/>
<input name="email" defaultValue={user.email} />
<button type="submit">Save</button>
</Form>
);
}
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const user = await getUser(request);
await updateUser(user.id, {
email: formData.get("email"),
displayName: formData.get("displayName"),
});
return json({ ok: true });
}
When the user submits the form:
useLoaderData
returns the updated values from the server.In this way, the UI is kept in sync with server state without writing any code for that synchronization.
There are various ways to submit a form besides an HTML form element (like in response to drag and drop, or an onChange event). There is also a lot more to talk about around form validation, error handling, pending states, etc. We'll get to all of that later, but this is the gist of data flow in Remix.