Displaying content in a React Router front end
You’ve configured your Studio with a post document type and learned how to query from your hosted dataset. Before deploying the Studio, let’s query and display this content on the front-end framework of your choice.

1Install a new React Router 7 (Remix) application
If you have an existing application, skip this first step and adapt the rest of the lesson to install Sanity dependencies to fetch and render content.
Run the following in a new tab or window in your Terminal (keep the Studio running) to create a new React Router 7 application with Tailwind CSS and TypeScript.
You should now have your Studio and React Router 7 application in two separate, adjacent folders:
# outside your studio directory
npx create-react-router@latest react-router-{{PROJECT_NAME_SLUGIFIED}} -y
cd react-router-{{PROJECT_NAME_SLUGIFIED}}
├─ /react-router-{{PROJECT_NAME_SLUGIFIED}}
└─ /studio-{{PROJECT_NAME_SLUGIFIED}}
2Install Sanity dependencies
Run the following inside the react-router-hello-world
directory to install:
@sanity/client
for fetching content from Sanity@sanity/image-url
helper functions to take image data from Sanity and create a URL@portabletext/react
to render Portable Text as React components
npm install @sanity/client @sanity/image-url @portabletext/react
3Start the development server
Run the following command and open http://localhost:5173 in your browser.
npm run dev
4Configure the Sanity client
To fetch content from Sanity, you’ll first need to configure a Sanity Client.
Create a directory react-router-hello-world/app/sanity
and within it create a client.ts
file, with the following code:
import { createClient } from "@sanity/client";
export const client = createClient({
projectId: "your-project-id",
dataset: "production",
apiVersion: "2024-01-01",
useCdn: false,
});
5Display content on the home page
React Router uses a loader
function exported from routes for server-side fetching of data. Routes are configured in the app/routes.ts
file.
The default home page can be found at app/routes/home.tsx
Update it to render a list of posts fetched from your Sanity dataset using the code below.
import { SanityDocument } from "@sanity/client";
import { Link } from "react-router";
import { client } from "~/sanity/client";
import { Route } from "./+types/home";
const POSTS_QUERY = `*[
_type == "post"
&& defined(slug.current)
]|order(publishedAt desc)[0...12]{_id, title, slug, publishedAt}`;
export async function loader() {
return { posts: await client.fetch<SanityDocument[]>(POSTS_QUERY) };
}
export default function IndexPage({ loaderData }: Route.ComponentProps) {
const { posts } = loaderData;
return (
<main className="container mx-auto min-h-screen max-w-3xl p-8">
<h1 className="text-4xl font-bold mb-8">Posts</h1>
<ul className="flex flex-col gap-y-4">
{posts.map((post) => (
<li className="hover:underline" key={post._id}>
<Link to={`/${post.slug.current}`}>
<h2 className="text-xl font-semibold">{post.title}</h2>
<p>{new Date(post.publishedAt).toLocaleDateString()}</p>
</Link>
</li>
))}
</ul>
</main>
);
}
6Display individual posts
Create a new route for individual post pages.
The dynamic value of a slug when visiting /:post
in the URL is used as a parameter in the GROQ query used by Sanity Client.
Notice that we’re using Tailwind CSS Typography’s prose
class name to style the post’s body
block content. Install it in your project following their documentation.
Update the routes.ts
configuration file to load this route when individual post links are clicked.
import { Link } from "react-router";
import imageUrlBuilder from "@sanity/image-url";
import { SanityDocument } from "@sanity/client";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import { PortableText } from "@portabletext/react";
import { Route } from "../routes/+types/post";
import { client } from "~/sanity/client";
const { projectId, dataset } = client.config();
const urlFor = (source: SanityImageSource) =>
projectId && dataset
? imageUrlBuilder({ projectId, dataset }).image(source)
: null;
const POST_QUERY = `*[_type == "post" && slug.current == $slug][0]`;
export async function loader({ params }: Route.LoaderArgs) {
return { post: await client.fetch<SanityDocument>(POST_QUERY, params) };
}
export default function Component({ loaderData }: Route.ComponentProps) {
const { post } = loaderData;
const postImageUrl = post.image
? urlFor(post.image)?.width(550).height(310).url()
: null;
return (
<main className="container mx-auto min-h-screen max-w-3xl p-8 flex flex-col gap-4">
<Link to="/" className="hover:underline">
← Back to posts
</Link>
{postImageUrl && (
<img
src={postImageUrl}
alt={post.title}
className="aspect-video rounded-xl"
width="550"
height="310"
/>
)}
<h1 className="text-4xl font-bold mb-8">{post.title}</h1>
<div className="prose">
<p>Published: {new Date(post.publishedAt).toLocaleDateString()}</p>
{Array.isArray(post.body) && <PortableText value={post.body} />}
</div>
</main>
);
}
Was this page helpful?