Refine provides first-class support for Next.js, enabling you to build powerful server-side rendered (SSR) applications. This guide covers both the App Router and Pages Router approaches.
Installation
Install the Next.js router package:
npm install @refinedev/nextjs-router
Or use the CLI to scaffold a new Next.js project:
npm create refine-app@latest -- --preset refine-nextjs my-app
App Router Setup
Basic Configuration
Setup Refine in your app/layout.tsx:
// app/layout.tsx
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import routerProvider from "@refinedev/nextjs-router";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Refine
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
routerProvider={routerProvider}
resources={[
{
name: "posts",
list: "/posts",
show: "/posts/show/:id",
create: "/posts/create",
edit: "/posts/edit/:id",
},
]}
>
{children}
</Refine>
</body>
</html>
);
}
Server Components with Data Fetching
Fetch data in server components:
// app/posts/page.tsx
import dataProvider from "@refinedev/simple-rest";
const API_URL = "https://api.fake-rest.refine.dev";
interface Post {
id: number;
title: string;
content: string;
}
export default async function PostsPage() {
const { data, total } = await getData();
return (
<div>
<h1>Posts ({total})</h1>
{data.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</div>
))}
</div>
);
}
async function getData() {
const response = await dataProvider(API_URL).getList<Post>({
resource: "posts",
pagination: { current: 1, pageSize: 10 },
});
return {
data: response.data,
total: response.total,
};
}
Client Components with Refine Hooks
Use Refine hooks in client components:
// app/posts/page.tsx
"use client";
import { useTable } from "@refinedev/core";
import Link from "next/link";
interface Post {
id: number;
title: string;
status: string;
}
export default function PostList() {
const {
result,
tableQuery: { isLoading },
} = useTable<Post>({
resource: "posts",
});
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Posts</h1>
<ul>
{result?.data.map((post) => (
<li key={post.id}>
<Link href={`/posts/show/${post.id}`}>
{post.title}
</Link>
</li>
))}
</ul>
</div>
);
}
Hybrid Approach: SSR with Client Hydration
Combine server and client rendering:
// app/posts/page.tsx
import { Suspense } from "react";
import dataProvider from "@refinedev/simple-rest";
import { PostListClient } from "./post-list-client";
const API_URL = "https://api.fake-rest.refine.dev";
export default async function PostsPage() {
const initialData = await getInitialData();
return (
<Suspense fallback={<div>Loading...</div>}>
<PostListClient initialData={initialData} />
</Suspense>
);
}
async function getInitialData() {
const response = await dataProvider(API_URL).getList({
resource: "posts",
pagination: { current: 1, pageSize: 10 },
});
return response;
}
// app/posts/post-list-client.tsx
"use client";
import { useTable } from "@refinedev/core";
interface Props {
initialData: any;
}
export function PostListClient({ initialData }: Props) {
const { result } = useTable({
resource: "posts",
queryOptions: {
initialData,
},
});
return (
<div>
{result?.data.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
Persisting Table State in SSR
Handle query parameters for table state:
// app/posts/page.tsx
import { parseTableParams } from "@refinedev/nextjs-router";
import dataProvider from "@refinedev/simple-rest";
interface PostListProps {
searchParams?: Record<string, string>;
}
export default async function PostList({ searchParams }: PostListProps) {
const tableParams = parseTableParams(searchParams);
const { data, total } = await getData(tableParams);
return (
<div>
<h1>Posts ({total})</h1>
{/* Render posts */}
</div>
);
}
async function getData(params: any) {
const response = await dataProvider(API_URL).getList({
resource: "posts",
pagination: params.pagination,
filters: params.filters,
sorters: params.sorters,
});
return {
data: response.data,
total: response.total,
};
}
Pages Router Setup
Basic Configuration
Setup Refine in _app.tsx:
// pages/_app.tsx
import type { AppProps } from "next/app";
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import routerProvider from "@refinedev/nextjs-router/pages";
function MyApp({ Component, pageProps }: AppProps) {
return (
<Refine
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
routerProvider={routerProvider}
resources={[
{
name: "posts",
list: "/posts",
show: "/posts/show/:id",
},
]}
>
<Component {...pageProps} />
</Refine>
);
}
export default MyApp;
SSR with getServerSideProps
// pages/posts/index.tsx
import { GetServerSideProps } from "next";
import { useTable } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
const API_URL = "https://api.fake-rest.refine.dev";
interface Post {
id: number;
title: string;
}
interface Props {
initialData: any;
}
export default function PostList({ initialData }: Props) {
const { result } = useTable<Post>({
queryOptions: {
initialData,
},
});
return (
<div>
<h1>Posts</h1>
<ul>
{result?.data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
export const getServerSideProps: GetServerSideProps = async () => {
const data = await dataProvider(API_URL).getList<Post>({
resource: "posts",
pagination: { current: 1, pageSize: 10 },
});
return {
props: {
initialData: data,
},
};
};
Static Generation with getStaticProps
// pages/posts/[id].tsx
import { GetStaticProps, GetStaticPaths } from "next";
import { useShow } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
const API_URL = "https://api.fake-rest.refine.dev";
interface Props {
initialData: any;
}
export default function PostShow({ initialData }: Props) {
const { result } = useShow({
queryOptions: {
initialData,
},
});
return (
<div>
<h1>{result?.data?.title}</h1>
<p>{result?.data?.content}</p>
</div>
);
}
export const getStaticPaths: GetStaticPaths = async () => {
const { data } = await dataProvider(API_URL).getList({
resource: "posts",
pagination: { current: 1, pageSize: 100 },
});
const paths = data.map((post) => ({
params: { id: post.id.toString() },
}));
return {
paths,
fallback: "blocking",
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const data = await dataProvider(API_URL).getOne({
resource: "posts",
id: params?.id as string,
});
return {
props: {
initialData: data,
},
revalidate: 60, // Revalidate every 60 seconds
};
};
Persisting Table State in Pages Router
// pages/posts/index.tsx
import { GetServerSideProps } from "next";
import { parseTableParams } from "@refinedev/nextjs-router/pages";
import dataProvider from "@refinedev/simple-rest";
const API_URL = "https://api.fake-rest.refine.dev";
export const getServerSideProps: GetServerSideProps = async ({ resolvedUrl }) => {
const tableParams = parseTableParams(resolvedUrl?.split("?")[1] ?? "");
const data = await dataProvider(API_URL).getList({
resource: "posts",
...tableParams,
});
return {
props: {
initialData: data,
},
};
};
export default function PostList({ initialData }: any) {
const { result } = useTable({
queryOptions: {
initialData,
},
});
return <div>{/* Render table */}</div>;
}
Authentication in SSR
App Router Authentication
// app/posts/layout.tsx
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { authProvider } from "@providers/auth-provider";
export default async function PostsLayout({
children,
}: {
children: React.ReactNode;
}) {
const { authenticated } = await checkAuth();
if (!authenticated) {
redirect("/login");
}
return <div>{children}</div>;
}
async function checkAuth() {
const cookieStore = cookies();
const auth = cookieStore.get("auth");
return {
authenticated: !!auth,
};
}
Pages Router Authentication
// pages/posts/index.tsx
import { GetServerSideProps } from "next";
import { authProvider } from "@providers/auth-provider";
export const getServerSideProps: GetServerSideProps = async (context) => {
const { authenticated, redirectTo } = await authProvider.check(context);
if (!authenticated && redirectTo) {
return {
redirect: {
destination: redirectTo,
permanent: false,
},
};
}
return {
props: {},
};
};
Using Client and Server Providers
Separate providers for client and server:
// providers/data-provider/data-provider.server.ts
import dataProvider from "@refinedev/simple-rest";
export const dataProviderServer = dataProvider(API_URL);
// providers/data-provider/data-provider.client.ts
"use client";
import dataProvider from "@refinedev/simple-rest";
export const dataProviderClient = dataProvider(API_URL);
Since Refine components use React Context, they must be client components. Create separate provider instances for server and client when needed.
Best Practices
-
Use server components for initial data: Fetch data on the server for better SEO and performance
-
Hydrate with initial data: Pass server-fetched data to client components via
initialData
-
Handle loading states: Show appropriate loading indicators during client-side navigation
-
Cache strategically: Use Next.js caching features with
revalidate in getStaticProps
-
Implement error boundaries: Handle errors gracefully in both server and client
-
Use environment variables: Store API URLs and secrets in environment variables
-
Optimize images: Use Next.js
<Image> component for automatic optimization
-
Implement pagination: Handle large datasets with proper pagination
Handling 404s
App Router
// app/not-found.tsx
import { Suspense } from "react";
export default function NotFound() {
return (
<Suspense>
<div>
<h1>404 - Page Not Found</h1>
</div>
</Suspense>
);
}
Pages Router
// pages/404.tsx
export default function Custom404() {
return <h1>404 - Page Not Found</h1>;
}
- Use Incremental Static Regeneration (ISR):
export const getStaticProps: GetStaticProps = async () => {
// ...
return {
props: { data },
revalidate: 60, // Revalidate every 60 seconds
};
};
-
Implement partial prerendering: Use
fallback: "blocking" in getStaticPaths
-
Optimize data fetching: Fetch only necessary data
-
Use React Query caching: Leverage Refine’s built-in caching
-
Implement code splitting: Use dynamic imports for large components
Further Reading