Page data in Remix comes from a "loader" defined inside of your Route Module. While they live in the same file as the React Component, these loaders are only ever run server side. This means you can write sever side code right next to your component, like a direct connection to your database. Remix will remove the server-side code from the browser bundle, so you don't have to worry about it causing problems in the browser (if you ever suspect there's a problem, read Module Constraints).
Inside of your app/routes/gists.tsx
file, export a loader
function that fetches the latest gists from the public:
import React from "react";
import type { LoaderFunction } from "remix";
export let loader: LoaderFunction = () => {
return fetch("https://api.github.com/gists");
};
export default function Gists() {
/* ... */
}
Now that we have a loader in place, you can access that data with the useLoaderData
hook.
import React from "react";
import type { LoaderFunction } from "remix";
import { useLoaderData } from "remix";
export let loader: LoaderFunction = () => {
return fetch("https://api.github.com/gists");
};
export default function Gists() {
let data = useLoaderData();
console.log(data);
return (
<div>
<h2>Public Gists</h2>
</div>
);
}
If you visit "/gists" you should see a list of gists in both the browser console and the terminal since we're server rendering in development.
You might be scratching your head at that last bit. Why didn't we have to unwrap the fetch response with the usual await res.json()
?
If you've been around the Node.js world for a while you'll recognize that there are many different versions of "request" and "response". The express API req, res
is probably the most ubiquitous, but wherever you go it's always a little different.
When browsers shipped the Fetch API, they didn't just create a spec for window.fetch
, but they also created a spec for what a fetch sends and returns, Request
, Headers
, and Response
. You can read about these APIs on MDN.
Instead of coming up with our own API, we built Remix on top of the Web Fetch API. Loaders can return Responses, like this:
return new Response(JSON.stringify({ teapot: true }), {
status: 418,
headers: {
"Content-Type": "application/json",
"Cache-Control": "max-age=3600"
}
});
So back to our question, why didn't we have to await the fetch and then await the res.json
? Because Remix awaits your loader, and fetch
resolves to response, and Remix is expecting exactly that type of object.
Most of the time you'll want to use one of Remix's built-in response helpers in your loaders.
The json
helper will deal with the content type automatically while still giving you control over the headers, status code, etc.
import { json } from "remix";
export let loader = async () => {
let arrayOfStuff = await db.query(someQuery);
return json(arrayOfStuff);
};
Here's how you could send a 404 page to the user and the browser:
import { json } from "remix";
export let loader = async ({ params }) => {
let record = await findSomeRecord(params.id);
if (record == null) {
return json({ notFound: true }, { status: 404 });
}
return json(record);
};
You don't have to build up a full response or use a helper, loaders can return plain objects, you just lose control over your headers this way:
export function loader() {
return { anything: "you want" };
}
Whew, okay, back to our app. Go ahead and map over that array however you'd like, here's a suggestion:
export default function Gists() {
let data = useLoaderData();
return (
<div>
<h2>Public Gists</h2>
<ul>
{data.map((gist: any) => (
<li key={gist.id}>
<a href={gist.html_url}>
{Object.keys(gist.files)[0]}
</a>
</li>
))}
</ul>
</div>
);
}
Bit lazy on the type there, but hopefully you'll forgive us! Alright, refresh and you should see a beautiful list of gists (a glist?).
Like headers, meta tags pretty much always depend on data too, so Remix passes the data to your meta tag function. Open up app/routes/gists.tsx
again and update your meta function:
export function meta({ data }) {
return {
title: "Public Gists",
description: `View the latest ${data.length} gists from the public`
};
}
Now if somebody posts a link to this site on social media, the preview will include the description with that data-driven meta description 🙌.
Here's the full code using all of the Route APIs we've introduced so far, as well as a quick type for a Gist.
import React from "react";
import { useLoaderData } from "remix";
export let loader: LoaderFunction = () => {
let res = await fetch("https://api.github.com/gists");
let gists = await res.json();
return json(gists, {
headers: {
"Cache-Control": "max-age=300",
},
});
};
// The title and meta tags for the document's <head>
export function meta({ data }: { data: Gist[] }) {
return {
title: "Public Gists",
description: `View the latest ${data.length} public gists`,
};
}
// The HTTP headers for the server rendered request, just
// use the cache control from the loader.
export function headers({ loaderHeaders } {
return {
"Cache-Control": loaderHeaders.get("Cache-Control"),
};
}
export default function Gists() {
let data = useLoaderData();
return (
<div>
<h2>Public Gists</h2>
<ul>
{data.map((gist) => (
<li key={gist.id}>
<a href={gist.html_url}>
{Object.keys(gist.files)[0]}
</a>
</li>
))}
</ul>
</div>
);
}