Back to Blog

Where Should This State Live?

The Problem

You have a piece of state. Where does it go?

  • useState in the component?
  • Lift it to a parent?
  • Put it in Context?
  • Use a state manager like Zustand?
  • Store it in the URL?
  • Keep it on the server?

Wrong choice = prop drilling hell, unnecessary re-renders, or a global store that knows what every modal is doing.

The Decision Framework

Ask these questions in order:

1. Can this state be derived from other state?
   └─ YES → Don't store it. Compute it.

2. Does only ONE component need this?
   └─ YES → useState in that component

3. Do a parent and its children need this?
   └─ YES → useState in the parent, pass down

4. Do siblings need to share this?
   └─ YES → Lift to common ancestor

5. Is this state tied to a URL (filter, pagination, search)?
   └─ YES → Store in URL (searchParams)

6. Is this server data that multiple components read?
   └─ YES → React Query / SWR / Server Components

7. Does most of the app need this (auth, theme, locale)?
   └─ YES → Context or lightweight store (Zustand)

The Solutions

1. Derived State — Don't Store It

// ❌ Storing derived state
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);

// Every time items changes, you have to remember to update itemCount

// ✅ Compute it
const [items, setItems] = useState([]);
const itemCount = items.length;  // Always correct

2. Local State — useState

// Only this component cares about whether dropdown is open
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && <Menu />}
    </div>
  );
}

3. Shared Between Parent-Child — Lift Up

// Parent owns the state, children receive it
function ProductFilters() {
  const [priceRange, setPriceRange] = useState([0, 100]);

  return (
    <>
      <PriceSlider value={priceRange} onChange={setPriceRange} />
      <AppliedFilters priceRange={priceRange} />
    </>
  );
}

4. URL State — searchParams

// ✅ Filters, pagination, search — use URL
// Users can bookmark, share, refresh without losing state

// app/products/page.tsx
function ProductsPage({ searchParams }: { searchParams: { q?: string, page?: string } }) {
  const query = searchParams.q ?? '';
  const page = parseInt(searchParams.page ?? '1');

  const products = await searchProducts(query, page);

  return (
    <>
      <SearchInput defaultValue={query} />
      <ProductList products={products} />
      <Pagination currentPage={page} />
    </>
  );
}

// Update URL instead of useState
'use client';
import { useRouter, useSearchParams } from 'next/navigation';

function SearchInput({ defaultValue }) {
  const router = useRouter();
  const searchParams = useSearchParams();

  function handleSearch(value: string) {
    const params = new URLSearchParams(searchParams);
    params.set('q', value);
    params.set('page', '1');
    router.push(`?${params.toString()}`);
  }

  return <input defaultValue={defaultValue} onChange={(e) => handleSearch(e.target.value)} />;
}

5. Server Data — React Query / Server Components

// ❌ Fetching in useEffect, storing in useState
const [user, setUser] = useState(null);
useEffect(() => {
  fetchUser().then(setUser);
}, []);

// ✅ Let the data layer manage it
// Server Component
async function UserProfile() {
  const user = await getUser();  // Cached, deduped automatically
  return <Profile user={user} />;
}

// Or with React Query (Client)
function UserProfile() {
  const { data: user } = useQuery({ queryKey: ['user'], queryFn: getUser });
  return <Profile user={user} />;
}

6. App-Wide State — Context or Zustand

// Theme, auth, locale — truly global
// Use Context for static-ish values, Zustand for frequently changing

// Zustand — simple, no Provider needed
import { create } from 'zustand';

const useAuthStore = create((set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
}));

// Use anywhere
function Header() {
  const user = useAuthStore((state) => state.user);
  return <span>{user?.name}</span>;
}

function LoginButton() {
  const login = useAuthStore((state) => state.login);
  return <button onClick={() => login({ name: 'John' })}>Login</button>;
}

Quick Reference

State TypeWhereExample
UI state (single component)useStateModal open, input value
UI state (few components)Lift to parentAccordion expanded items
URL-worthy statesearchParamsFilters, pagination, tabs
Server dataReact Query / RSCUser, products, orders
Auth/theme/localeContext or ZustandCurrent user, dark mode
Complex formsReact Hook FormMulti-step form state

The Anti-Pattern

Don't put everything in global state:

// ❌ Global store for modal state
const useStore = create((set) => ({
  isModalOpen: false,
  modalContent: null,
  openModal: (content) => set({ isModalOpen: true, modalContent: content }),
  closeModal: () => set({ isModalOpen: false, modalContent: null }),
}));

// ✅ Modal state belongs in the component that owns the modal
function ProductCard({ product }) {
  const [showDetails, setShowDetails] = useState(false);

  return (
    <>
      <button onClick={() => setShowDetails(true)}>View</button>
      {showDetails && <ProductModal product={product} onClose={() => setShowDetails(false)} />}
    </>
  );
}

TL;DR

  1. Derive what you can — don't store computed values
  2. Start local — useState in the component that owns it
  3. Lift only when needed — when siblings need to share
  4. URL for shareable state — filters, search, pagination
  5. Data layer for server state — React Query, SWR, or Server Components
  6. Global store sparingly — auth, theme, maybe cart

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.