As the best modern software combo
Tanstack Router is the best React router and Tanstack Query is the best server state manager
// posts.tsx /posts
export const Route = createFileRoute("/posts")({
loader: async () => {
const posts = await getPosts();
const categories = await getCategories();
return { posts, categories };
},
component: Posts,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
notFoundComponent: NotFound,
errorComponent: ErrorComponent,
pendingComponent: PendingComponent,
onError: sendInfoToSentry,
onEnter: sendToAnalyticsEnter,
onLeave: sendToAnalyticsLeave,
});
const Posts = () => {
const { posts, categories } =
useLoaderData({ from: "/posts" });
return (
<div>
<DisplayPosts posts={posts} />
<Outlet />
</div>
);
};// posts.tsx /posts
export const Route = createFileRoute("/posts")({
loader: async () => {
const posts = await getPosts();
const categories = await getCategories();
return { posts, categories };
},
component: Posts,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
notFoundComponent: NotFound,
errorComponent: ErrorComponent,
pendingComponent: PendingComponent,
onEnter: sendToAnalyticsEnter,
onLeave: sendToAnalyticsLeave,
onError: sendInfoToSentry,
});
// posts.tsx /posts
export const Route = createFileRoute("/posts")({
loader: async () => {
const posts = await getPosts();
const categories = await getCategories();
return { posts, categories };
},
component: Posts,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
notFoundComponent: NotFound,
errorComponent: ErrorComponent,
pendingComponent: PendingComponent,
onEnter: sendToAnalyticsEnter,
onLeave: sendToAnalyticsLeave,
onError: sendInfoToSentry,
});
const PostsSearchSchema = z.object({
limit: z.number().min(1).max(100).default(10).optional(),
offset: z.number().min(0).default(0).optional(),
category: z.enum(["IT", "Business", "Marketing"]).optional(),
});// posts.tsx /posts
export const Route = createFileRoute("/posts")({
validateSearch: zodSearchValidator(PostsSearchSchema),
loaderDeps: ({ search }) => {
return {
limit: search.limit,
offset: search.offset,
category: search.category,
};
},
loader: async () => {
const posts = await getPosts();
const categories = await getCategories();
return { posts, categories };
},
component: Posts,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
notFoundComponent: NotFound,
errorComponent: ErrorComponent,
pendingComponent: PendingComponent,
onEnter: sendToAnalyticsEnter,
onLeave: sendToAnalyticsLeave,
onError: sendInfoToSentry,
});
const PostsSearchSchema = z.object({
limit: z.number().min(1).max(100).default(10).optional(),
offset: z.number().min(0).default(0).optional(),
category: z.enum(["IT", "Business", "Marketing"]).optional(),
});
// posts.tsx /posts
export const Route = createFileRoute("/posts")({
validateSearch: zodSearchValidator(PostsSearchSchema),
loaderDeps: ({ search }) => {
return {
limit: search.limit,
offset: search.offset,
category: search.category,
};
},
loader: async () => {
const posts = await getPosts();
const categories = await getCategories();
return { posts, categories };
},
component: Posts,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
notFoundComponent: NotFound,
errorComponent: ErrorComponent,
pendingComponent: PendingComponent,
onEnter: sendToAnalyticsEnter,
onLeave: sendToAnalyticsLeave,
onError: sendInfoToSentry,
search: {
middlewares: [
retainSearchParams(["limit", "offset"]),
stripSearchParams(["limit", "offset"]),
],
},
});
const PostsSearchSchema = z.object({
limit: z.number().min(1).max(100).default(10).optional(),
offset: z.number().min(0).default(0).optional(),
category: z.enum(["IT", "Business", "Marketing"]).optional(),
});
// posts.tsx /posts
export const Route = createFileRoute("/posts")({
validateSearch: zodSearchValidator(PostsSearchSchema),
loaderDeps: ({ search }) => {
return {
limit: search.limit,
offset: search.offset,
category: search.category,
};
},
loader: async ({ deps: { limit, offset, category } }) => {
const posts = await getPosts({ limit, offset, category });
const categories = await getCategories();
return { posts, categories };
},
component: Posts,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
notFoundComponent: NotFound,
errorComponent: ErrorComponent,
pendingComponent: PendingComponent,
onEnter: sendToAnalyticsEnter,
onLeave: sendToAnalyticsLeave,
onError: sendInfoToSentry,
search: {
middlewares: [
retainSearchParams(["limit", "offset"]),
stripSearchParams(["limit", "offset"]),
],
},
});
const PostsSearchSchema = z.object({
limit: z.number().min(1).max(100).default(10).optional(),
offset: z.number().min(0).default(0).optional(),
category: z.enum(["IT", "Business", "Marketing"]).optional(),
});
// posts.tsx /posts
export const Route = createFileRoute("/posts")({
validateSearch: zodSearchValidator(PostsSearchSchema),
loaderDeps: ({ search }) => {
return {
limit: search.limit,
offset: search.offset,
category: search.category,
};
},
loader: async ({ deps: { limit, offset, category } }) => {
const posts = await getPosts({ limit, offset, category });
const categories = await getCategories();
return { posts, categories };
},
component: Posts,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
notFoundComponent: NotFound,
errorComponent: ErrorComponent,
pendingComponent: PendingComponent,
onEnter: sendToAnalyticsEnter,
onLeave: sendToAnalyticsLeave,
onError: sendInfoToSentry,
search: {
middlewares: [
retainSearchParams(["limit", "offset"]),
stripSearchParams(["limit", "offset"]),
],
},
});
const PostsSearchSchema = z.object({
limit: z.number().min(1).max(100).default(10).optional(),
offset: z.number().min(0).default(0).optional(),
category: z.enum(["IT", "Business", "Marketing"]).optional(),
modal: z.object({
isOpen: z.boolean().default(false),
postId: z.string().optional(),
action: z.enum(['create', 'edit', 'delete']).optional()
}).default({ isOpen: false }).optional()
});
// posts.$postId.tsx /posts/$postId
export const Route = createFileRoute("/posts/$postId")({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId);
return post;
},
component: PostComponent,
});
const PostComponent = () => {
const { categories } = useLoaderData({ from: "/posts" });
const postData = useLoaderData({ from: "/posts/$postId" });
return ...
};// posts.$postId.tsx /posts/$postId
export const Route = createFileRoute("/posts/$postId")({
loader: async ({ params: { postId }, context: { queryClient } }) => {
await queryClient.ensureQueryData(postQueryOptions(postId));
await queryClient.ensureQueryData(categoriesQueryOptions());
},
component: PostComponent,
});
const PostComponent = () => {
const postId = useParams({ from: "/posts/$postId" }).postId;
const {
data: { author, content, id },
} = useSuspenseQuery(postQueryOptions(postId));
const { data: categories } = useSuspenseQuery(categoriesQueryOptions());
return ...;
};No server-side*
SEO
Less popular than Remix and Next.js
Vendor lock-in ❔
JavaScript is required to run the page