Managing State in the New World of React Server Components

Preston MayiekaPreston Mayieka
5 min read

Where Did the State Go?

React has changed how apps get built.

With Server Components becoming the default in Next.js, managing state isn’t as straightforward as calling useState anymore.

And if you’ve tried building something with the App Router, this question often comes up:

Where should states be stored?

Let’s clear that up.

🏠 Think of Your App Like a House

Let’s imagine your app as a house.

The server is the person who owns the house, responsible for setting up the furniture, preparing food and getting everything ready for guests.

The client is the guest who visits the house. They bring their backpack with the essentials they need, such as a notebook, snacks, or a phone.

Now here’s the important part:

  • The owner of the house doesn’t know what’s in the guest's backpack.

  • The guest can open it, take things out, for example: phone, notebook and interact with them.

When building an app:

  • The server sets everything up, like the rooms and furniture.

  • The client (the part your users interact with) handles the changes like writing in your notebook or eating your snacks.

That backpack? That’s where your data or state lives. You (the guest) can use it. The builder prepares the space.

This means that the server builds the app, but the client handles things that change, such as typing in a form, clicking buttons, or switching tabs.

🧱 Server vs Client Components

React now splits components into two types:

Component TypeRuns OnCan Use useState?Good For
Server ComponentThe server❌ NoFetching data, SEO, performance
Client ComponentThe browser✅ YesHandling interactions, UI context

By default, components in Next.js App Router are Server Components.

If you require interactivity (such as clicking buttons or updating forms), you must instruct React to render that component in the browser.

You do that by adding 'use client' at the top of the jsx or tsx file.

"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
  );
}

Common Pattern: Fetch on Server, Manage on Client

The most common (and safest) pattern is to:

  1. Fetch data from the server

  2. Pass it to a client component

  3. Let the client manage the state

Here’s what that looks like in a Next.js app:

// app/page.tsx (Server Component)
import UserForm from "./UserForm";

export default async function Page() {
  const user = await getUserFromDatabase();
  return <UserForm initialUser={user} />;
}
// app/UserForm.tsx (Client Component)
"use client";

import { useState } from "react";

export default function UserForm({ initialUser }) {
  const [user, setUser] = useState(initialUser);
  return <div>Hello, {user.name}</div>;
}

What If You Need Global State?

Let’s say you need to share state across various components (like theme settings or a shopping cart). In that case, you can still use tools like:

Important: These must live inside client components.

Server components won't hold state or use stateful hooks.

// CartContext.tsx
'use client';
import { createContext, useContext, useState } from 'react';

// 1. Create the context
const CartContext = createContext();

// 2. Make a provider component
export function CartProvider({ children }) {
  const [items, setItems] = useState([]);

  const addItem = (item) => {
    setItems((prev) => [...prev, item]);
  };

  return (
    <CartContext.Provider value={{ items, addItem }}>
      {children}
    </CartContext.Provider>
  );
}

// 3. Create a hook to use the context
export function useCart() {
  return useContext(CartContext);
}
// app/layout.tsx or a specific page layout
import { CartProvider } from './CartContext';

export default function Layout({ children }) {
  return <CartProvider>{children}</CartProvider>;
}

❌ What You Should Avoid

Some common mistakes to watch out for:

  • Don’t use useState or useEffect In a server component. It will crash.

  • Don’t fetch everything on the client to make useState it work. That kills performance.

  • Don’t mix server and client logic in the same file; it leads to confusion.

Using Server Actions for Updates

Next.js 14 introduced Server Actions.

These let you run a function directly on the server (without writing a full API route).

// app/actions/saveUser.ts
"use server";

export async function saveUser(data) {
  await db.save(data);
}

And you can call that from a form in your client component:

"use client";
import { saveUser } from "./actions/saveUser";

<form action={saveUser}>
  <input name="name" />
  <button type="submit">Save</button>
</form>;

This keeps your code clean and avoids messy client-side fetch logic.

Conclusion

React Server Components give us faster apps by letting the server do more of the heavy lifting.

They also change how we think about managing state.

Here’s what to keep in mind:

  • Use useState and other React hooks only in Client Components.

  • Let the server fetch and prepare the data.

  • Let the client interact with and manage that data.

  • Use global state tools like Zustand or React Context, but only on the client side.

  • Don’t force everything to the client — that defeats the purpose of Server Components.

The key is knowing what to do where.

Found this useful, leave a like or comment down below if you found this helpful.

0
Subscribe to my newsletter

Read articles from Preston Mayieka directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Preston Mayieka
Preston Mayieka

I enjoy solving technical problems, researching and developing new technologies, designing software applications for different platforms.