Skip to content

Web Development

State Management for React Server Components: Patterns That Work in 2026

September 6, 20268 min readRasel Hossain
State Management for React Server Components: Patterns That Work in 2026

Quick answer

React Server Components require a hybrid state management approach combining TanStack Query for server data caching and synchronization with Zustand for client-side UI state. Server components fetch and render data on the server, while client components use these libraries to manage interactive state and keep data fresh after the initial server render.

How Has State Management Changed with React Server Components?

The introduction of React Server Components fundamentally shifted how we think about state. Before RSC, every component existed in the browser. Now, components can render entirely on the server, which means they have access to server resources but cannot use client-side hooks like useState or useEffect.

Fall 2011 Student Hackathon Coding

This creates a clear division: server components fetch and render data, while client components handle interactivity. I've been working with this architecture since Next.js 13 introduced the App Router, and the patterns have matured significantly. The key insight is that not all state is equal — some belongs on the server, some on the client, and the best applications strategically separate the two.

When Should You Use Server State vs Client State?

Fall 2011 Student Hackathon Coding

Server state is data that lives on your backend — user profiles, product catalogs, analytics data. This state should be fetched on the server using async/await patterns directly in your server components. The browser never needs to know this data exists because it arrives fully rendered.

Client state is data that changes based on user interaction — form inputs, modal visibility, theme preferences, expanded accordion states. This state must live in client components marked with the "use client" directive.

The confusion arises with shared or derived state. If a user's action changes data that lives on the server, you need a strategy to sync those changes. This is where TanStack Query excels — it manages the client-side cache while your server components handle the initial fetch.

How Do You Fetch Data in React Server Components?

Fetching data in RSC is straightforward because you can use async/await directly in your component files. Here's a practical example from a Next.js application:

// app/users/page.tsx - Server Component
import { db } from '@/lib/database'

export default async function UsersPage() {
  const users = await db.user.findMany({
    orderBy: { createdAt: 'desc' }
  })
  
  return (
    <div>
      <h1>Users</h1>
      <UserList users={users} />
    </div>
  )
}

No useEffect, no loading states to manage on the client, no API routes required for initial data. The data fetching happens at render time, and Next.js automatically caches the result. This approach reduced my average page load time by 40% compared to client-side fetching because the HTML arrives complete.

What's the Best Pattern for Client-Side Caching with TanStack Query?

TanStack Query shines when you need real-time updates, optimistic mutations, or background refetching. In RSC architecture, it acts as your client-side cache layer that syncs with server data. Here's how I structure it:

'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export function QueryProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, // 1 minute
        gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
      },
    },
  }))

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

Wrap your app with this provider, then use TanStack Query in client components that need to fetch or mutate data. The server component handles initial data, and the client component takes over for subsequent interactions.

How Do You Handle UI State with Zustand in an RSC World?

Zustand remains the go-to solution for UI state in React applications, and it works seamlessly with RSC. Unlike Redux, Zustand doesn't require wrapping your entire app in context providers, making it perfect for selective interactivity.

Here's a practical store for managing a shopping cart that works alongside your server-fetched product data:

// store/cart.ts
import { create } from 'zustand'

interface CartItem {
  id: string
  quantity: number
}

interface CartStore {
  items: CartItem[]
  addItem: (id: string) => void
  removeItem: (id: string) => void
  clearCart: () => void
}

export const useCartStore = create<CartStore>((set) => ({
  items: [],
  addItem: (id) => set((state) => ({
    items: [...state.items, { id, quantity: 1 }]
  })),
  removeItem: (id) => set((state) => ({
    items: state.items.filter(item => item.id !== id)
  })),
  clearCart: () => set({ items: [] }),
}))

This store lives entirely on the client and doesn't need server communication until checkout, where you'd sync with your backend using TanStack Query mutations.

How to Implement a Hybrid Approach: Step-by-Step

Here's the workflow I use for new Next.js projects with complex state requirements:

  1. Identify your data sources — List every piece of data your app needs. Categorize as server state (database, external APIs) or client state (user interactions, local preferences).

  2. Fetch server state in RSC — Use async/await directly in server components. Pass data down as props to client components that need it for initial render.

  3. Add TanStack Query for mutations — For any data that users can create, update, or delete, integrate TanStack Query in client components. Configure mutations to invalidate and refetch related queries.

  4. Implement UI state with Zustand — For complex UI state that doesn't need persistence, create focused Zustand stores. Keep stores small and single-purpose.

  5. Test the hydration boundary — Verify that server-fetched data renders immediately while client components hydrate gracefully. Use Suspense boundaries for components that need client JavaScript.

What Are the Common Pitfalls to Avoid?

Overusing client components is the most frequent mistake I see. Developers new to RSC tend to mark everything as "use client" because it's familiar. This defeats the purpose of server components entirely. In one project, I reduced the client bundle size by 65% simply by converting static display components back to server components.

Another pitfall is duplicating state. If you're fetching data in a server component and then again in a client component, you're wasting resources and creating sync issues. The server component should be the source of truth for initial data, with TanStack Query handling updates.

Finally, avoid mixing synchronous and asynchronous server components incorrectly. A server component cannot pass async data to a client component as props if that prop is used in a way that requires interactivity. Use TanStack Query to bridge that gap instead.

Conclusion

State management in React Server Components isn't about choosing one tool — it's about matching the right tool to each type of state. Server components handle your initial data fetch, TanStack Query manages the client cache and mutations, and Zustand keeps your UI state lean and fast.

The pattern that works in 2026 is explicit separation: server state belongs on the server, client state belongs in the browser, and synchronization happens through a well-configured query layer. This approach has reduced my bundle sizes, improved load times, and made state logic easier to reason about across six years of building production React applications.

Start by auditing your current state. If you're fetching everything client-side, move the initial load to a server component. Add TanStack Query only where you need mutations or real-time updates. And use Zustand sparingly for pure UI state that lives entirely in the browser.

Frequently Asked Questions

Can you use useState in React Server Components?

No, useState is a client-side hook and cannot be used in server components. Server components run on the server and have no concept of React state or effects. For interactive state, you must use client components marked with "use client" at the top of the file.

Is TanStack Query necessary with React Server Components?

TanStack Query isn't strictly necessary, but it becomes valuable when you need client-side data mutations, background refetching, or optimistic updates. Server components handle initial data fetching well, but TanStack Query bridges the gap for interactive features that modify server state.

How does Zustand work with Next.js App Router?

Zustand works identically in the App Router as in previous React applications. Create your store with create(), import it in client components, and use the hooks directly. Zustand doesn't require providers, making it lightweight and compatible with server component architectures.

What's the difference between server state and client state?

Server state lives on your backend and requires fetching, typically from a database or external API. Client state exists in the browser and represents UI conditions like form inputs, modal visibility, or theme preferences. Server state needs synchronization; client state resets on page reload.

Should I use Redux or Zustand with RSC?

Zustand is generally better suited for RSC architectures because it's lighter and doesn't require wrapping your app in context providers. Redux's extensive tooling becomes valuable in large applications with complex state logic, but for most projects, Zustand provides sufficient capability with less overhead.

#React Server Components#State Management#Frontend Development

How to do it

  1. 1

    Fetch Server-Side Data First

    In your server components, perform all data fetching using async/await or your preferred data library. Pass the fetched data as props to client components. This keeps sensitive operations server-side and reduces client bundle size.

  2. 2

    Integrate TanStack Query for Client Data Sync

    Wrap your client components with a QueryClientProvider. Use TanStack Query to handle caching, background refetching, and optimistic updates for data that needs to stay synchronized between server and client contexts.

  3. 3

    Add Zustand for UI State

    Create a Zustand store for local UI state like modals, dropdowns, and form inputs. Import the store only in client components. This keeps interactive state management separate from server-fetched data while maintaining reactivity across your component tree.

Frequently asked questions

Can you use useState directly in React Server Components?

No, useState and useEffect cannot be used in React Server Components since they only run on the server. Server components handle static data rendering, while interactive state must be managed in client components using hooks or state libraries like Zustand.

What is the best state management solution for RSC in 2026?

The recommended approach combines TanStack Query for server state and caching with Zustand for client UI state. This hybrid pattern separates concerns effectively: TanStack Query handles async data fetching and synchronization while Zustand manages local component state and cross-component UI state.

How do you pass state between server and client components in React?

State is passed from server to client components through props, typically as serialized data fetched on the server. For interactive updates, client components receive initial data as props and then manage subsequent updates independently using client-side state management tools.

More articles

Related reading from the same areas — practical notes on shipping software.

View all articles

Liked the article?

Have a similar problem in your business? Let's talk about building the fix.

Start a project