Back to Blogs

How to do state management on the server side of Next.js

media.getImageUrl(post.image).altText

In Next.js 15, server-side state management can be handled in a few different ways depending on your use case — especially with the App Router (introduced in Next 13) and React Server Components (RSC) becoming more central.


Here’s a breakdown of how to do server-side state management effectively in Next.js 15:


Server-Side State Management Approaches


1.Using React Server Components (RSC)


Next.js 15 supports Server Components by default in the app/ directory.


You can “manage state” by fetching data on the server and passing it as props to client components (if needed).


// app/page.tsx (server component)
import { getUserData } from '@/lib/api';
import UserCard from './UserCard';

export default async function Page() {
  const user = await getUserData(); // server-side state/data

  return (
    <div>
      <h1>Welcome back!</h1>
      <UserCard user={user} />
    </div>
  );
}

This avoids needing global state libraries entirely unless you’re doing complex client-side state interactions.

 

2.Storing State in Middleware or Cookies (Session-like)


If your “state” is user/session-based, use:

  • Middleware for request-level logic

  • Cookies (next/headers or next/response-cookies) for storing server-accessible state


// app/api/route.ts
import { cookies } from 'next/headers';

export async function GET() {
  const userToken = cookies().get('token')?.value;
  // Use token to fetch user data
}

This is great for:

  • Auth state

  • User preferences

  • Locale/language settings

 

3.API Routes + Database or Cache Layer


For persistent or sharable state, keep it in:

  • Postgres / MongoDB

  • Redis / Upstash

  • Edge config


Then access it via:


// app/api/settings/route.ts
import { db } from '@/lib/db';

export async function GET() {
  const settings = await db.getUserSettings();
  return Response.json(settings);
}
 

When You Do Need Client-Side State (and Bridge It)


You can combine server-side logic with a hydrated client component:


// app/page.tsx
import ClientCounter from './ClientCounter';

export default function Page() {
  return <ClientCounter initial={5} />;
}
 
'use client';

export default function ClientCounter({ initial }: { initial: number }) {
  const [count, setCount] = useState(initial);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}


 

Tools to Help


  • next/headers, next/response-cookies: Manage request/response state

  • cache() from React or Next.js: For memoizing server-side data

  • Zustand or Redux (if you must use client-global state)


TL;DR: Server State in Next.js 15


Use Case

Approach

Data fetched on request

Server Components + fetch

Persisted state (auth, prefs)

Cookies / Middleware

Shared state across components

React Context (server or client)

Client interactivity

Hydrated client components

Heavy state mgmt

Zustand / Redux (client)

 

Bonus Round


URL query parameters as a form of state management in Next.js 15, in the App Router.


This is actually a super clean and powerful way to manage state — especially when you want the state to be shareable, bookmarkable, and synchronized with navigation (like filters, tabs, pagination, etc.).

 

In a Server Component


You can access query parameters using the searchParams prop (available in server components):

// app/page.tsx (Server Component)
type Props = {
  searchParams: { [key: string]: string | string[] | undefined };
};

export default function Page({ searchParams }: Props) {
  const tab = searchParams.tab ?? 'overview';

  return <div>Current Tab: {tab}</div>;
}

No client state needed — the URL is the state.

 

In a Client Component


Use useSearchParams() from next/navigation to read query params reactively:


'use client';

import { useSearchParams, useRouter } from 'next/navigation';

export default function TabSelector() {
  const searchParams = useSearchParams();
  const router = useRouter();

  const activeTab = searchParams.get('tab') || 'overview';

  const changeTab = (tab: string) => {
    const params = new URLSearchParams(searchParams.toString());
    params.set('tab', tab);
    router.push(`?${params.toString()}`);
  };

  return (
    <>
      <button onClick={() => changeTab('overview')}>Overview</button>
      <button onClick={() => changeTab('analytics')}>Analytics</button>
      <p>Current tab: {activeTab}</p>
    </>
  );
}
 

When should you use this?


Use case

Use query params?

Filters on a page (search, tags)

✅ Yes

Pagination

✅ Yes

Tabs or views

✅ Yes

Auth or sensitive state

❌ No

Transient UI state (hover, drag)

❌ No

 

Bonus: Deep Linking


This method makes it easy to:


  • Share filtered views via URLs

  • Maintain state when the user refreshes

  • Let users hit “Back” and go to the previous state

  • Avoid relying on heavy client state management

How to do state management on the server side of Next.js | Luis Amador Portfolio