This episode dissects data loading and actions in Remix: loaders as server-side data handlers, form submission with actions, using redirects and cookies, and deferred data with streaming using defer and Await.

In episode 4 you mastered routing — the application map. Now we fill that map with data. This is Remix's heart: loader and action, two functions that control the entire flow of data between the server and the UI.
This model is completely different from an SPA. In an SPA, components call fetch inside useEffect and manage loading and error themselves. In Remix, the loader runs on the server before the page is sent, and the action processes data changes when a form is submitted. The result: less code, fewer race conditions, and more consistent UX.
Episode 5 dissects loader and action thoroughly, then adds two important tools: redirect for diverting requests, and defer for streaming slow data.
The loader function runs on the server when the route is requested. It returns data that the component can access through the useLoaderData hook. This data is serialized and sent along with the HTML, so the page already contains content before JavaScript runs.
import { useLoaderData } from "@remix-run/react";
export async function loader() {
return { judul: "Selamat datang", jumlahPosting: 42 };
}
export default function Beranda() {
const data = useLoaderData();
return <h1>{data.judul}</h1>;
}The loader receives context consisting of request, params, and context, then returns data that's serialized to the client. No need to call fetch against your own API — the data is already available when the component first renders.
Because the loader runs on the server, you can safely read the URL, headers, cookies, and environment variables. The most common uses:
request to read the URL, method, and headers.params for dynamic segments like id.context for values from entry.server.tsx, such as a database connection.When a form is submitted, Remix calls the action on that route. The action receives a request containing the form data, processes it on the server, then returns a response — usually validation data or a redirect.
import { useActionData, Form } from "@remix-run/react";
export async function action({ request }) {
const formData = await request.formData();
const nama = formData.get("nama");
return { pesan: `Halo ${nama}` };
}
export default function FormNama() {
const data = useActionData();
return (
<Form method="post">
<input name="nama" />
<button type="submit">Kirim</button>
{data?.pesan}
</Form>
);
}Form from @remix-run/react sends a request to the action on the same route, and useActionData reads the action's result. Note that the form works like a plain HTML form — that's the foundation of progressive enhancement.
A common pattern is for the action to validate on the server, then return field errors. The component displays the errors via useActionData. Validation is covered in depth in episode 7; what matters here is the flow: data goes into the action, gets processed on the server, and the result comes back to the component.
After an action successfully saves data, the page usually needs to move. The redirect function from @remix-run/node sends a 302 response to the destination URL:
import { redirect } from "@remix-run/node";
export async function action({ request }) {
const formData = await request.formData();
await simpanPost(formData);
return redirect("/posts");
}redirect("/posts") returns a response that tells the browser to navigate to the page. Because the action runs on the server, the redirect is also sent as a valid HTTP response.
Cookies can be read and written directly in loaders or actions through helpers in @remix-run/node. Using cookies directly is suitable for small data like preferences; for authentication, Remix provides session storage, which we'll cover in episode 11.
Not all data is equally fast. With defer, the loader returns some data immediately and other parts as a promise that's streamed when ready. The page still renders first, and the slow data appears later.
import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";
export async function loader() {
return defer({
cepat: "langsung tersedia",
lambat: ambilDataBesar(),
});
}
export default function Halaman() {
const data = useLoaderData();
return (
<Suspense fallback={<p>Memuat data besar...</p>}>
<Await resolve={data.lambat}>{(nilai) => <p>{nilai}</p>}</Await>
</Suspense>
);
}Await waits for the promise from defer and renders the fallback via Suspense. This pattern is very useful for analytics data, comments, or recommendations that shouldn't block the main page.
Episode 5 gives you full control over data: loaders to read on the server, actions to write via forms, redirect to divert, cookies for small state, and defer for streaming slow data. This is the foundation used by almost every upcoming episode.
The key takeaways:
In the next episode, episode 6, we'll discuss UI and component composition — creating reusable React components, styling with CSS Modules, Tailwind, or styled-components, client-side interactivity in Remix, and optimizing server-rendered UI. Routing and data are done; now it's time to make everything look beautiful.