Skip to content

React Router v7

@scribe-atp/react-router-framework provides loader factories for React Router v7 framework mode. The factories wire up request.signal automatically so fetches are cancelled if the user navigates away before the response arrives.

Terminal window
npm install @scribe-atp/react-router-framework

Use createSiteLoader to create a loader for a route that displays the full site — groups and article listings.

app/routes/blog.tsx
import { createSiteLoader } from '@scribe-atp/react-router-framework';
import { useLoaderData } from 'react-router';
export const loader = createSiteLoader('alice.bsky.social', 'alice-bsky-social');
export default function Blog() {
const site = useLoaderData<typeof loader>();
return (
<main>
<h1>{site.title}</h1>
{site.groups.map((group) => (
<section key={group.slug}>
<h2>{group.title}</h2>
<ul>
{group.articles.map((article) => (
<li key={article.uri}>
<a href={`/blog/${group.slug}/${article.url}`}>{article.title}</a>
</li>
))}
</ul>
</section>
))}
</main>
);
}

For routes where the slug comes from URL params, use fetchArticle from @scribe-atp/core directly inside your loader:

// app/routes/blog.$group.$slug.tsx
import type { LoaderFunctionArgs } from 'react-router';
import { fetchArticle } from '@scribe-atp/core';
import { useLoaderData } from 'react-router';
export async function loader({ request, params }: LoaderFunctionArgs) {
return fetchArticle('alice.bsky.social', params.slug!, request.signal);
}
export default function Article() {
const article = useLoaderData<typeof loader>();
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
);
}

All types from @scribe-atp/core are re-exported from @scribe-atp/react-router-framework:

import type { Site, Article, ArticleRef, SiteGroup } from '@scribe-atp/react-router-framework';