Home Quizzes Leaderboard Competitions Learn Hire Us
About Contact
Log In Sign Up
Learn Next.js State Management --- Zustand & React Query

State Management --- Zustand & React Query

⏱ 18 min read read
When You Need State Management:

Server Components + fetch caching = most data needs (no client lib
needed).

Zustand = lightweight global CLIENT state (cart, UI state,
preferences).

React Query = client-side data that needs auto-refetch, optimistic
updates.

Context = simple shared state across a few components.

Redux = legacy / large teams (usually overkill in Next.js 13+).

Zustand --- Minimal Global State:

// npm install zustand

import { create } from 'zustand';

interface CartStore {

items: CartItem[];

addItem: (item: CartItem) => void;

removeItem:(id: string) => void;

clear: () => void;

}

export const useCartStore = create<CartStore>((set) => ({

items: [],

addItem: (item) => set((s) => ({ items: [...s.items, item] })),

removeItem: (id) => set((s) => ({ items: s.items.filter(i => i.id
!== id) })),

clear: () => set({ items: [] }),

}));

// Usage in any Client Component

const { items, addItem } = useCartStore();

TanStack Query --- Server State on the Client:

// npm install @tanstack/react-query

import { useQuery, useMutation, useQueryClient } from
'@tanstack/react-query';

// Fetch + cache + refetch automatically

const { data, isLoading, error } = useQuery({

queryKey: ['students'],

queryFn: () => fetch('/api/students').then(r => r.json()),

staleTime: 60_000, // 1 minute before re-fetch

});

// Mutation with cache invalidation

const qc = useQueryClient();

const add = useMutation({

mutationFn: (data) => fetch('/api/students', { method: 'POST',
body: JSON.stringify(data) }),

onSuccess: () => qc.invalidateQueries({ queryKey: ['students']
}),

});
Code Example
// store/useStudentStore.ts --- Zustand store

import { create } from 'zustand';

import { persist } from 'zustand/middleware'; // persist to
localStorage

interface Student { id: number; name: string; grade: number; }

interface StudentStore {

students: Student[];

addStudent: (s: Student) => void;

removeStudent:(id: number) => void;

updateGrade: (id: number, grade: number) => void;

}

export const useStudentStore = create<StudentStore>()(persist(

(set) => ({

students: [],

addStudent: (s) => set(state => ({ students: [...state.students,
s] })),

removeStudent: (id) => set(state => ({ students:
state.students.filter(s => s.id !== id) })),

updateGrade: (id, grade) => set(state => ({

students: state.students.map(s => s.id === id ? { ...s, grade } :
s),

})),

}),

{ name: 'student-storage' } // localStorage key

));

// components/StudentDashboard.tsx

'use client';

import { useStudentStore } from '@/store/useStudentStore';

import { useState } from 'react';

export default function StudentDashboard() {

const { students, addStudent, removeStudent } = useStudentStore();

const [name, setName] = useState('');

const [grade, setGrade] = useState('');

const avg = students.length

? (students.reduce((acc, s) => acc + s.grade, 0) /
students.length).toFixed(1)

: '---';

return (

<div>

<h2>Students ({students.length}) --- Avg: {avg}</h2>

<div>

<input value={name} onChange={e => setName(e.target.value)}
placeholder="Name" />

<input value={grade} onChange={e => setGrade(e.target.value)}
placeholder="Grade" type="number" />

<button onClick={() => {

addStudent({ id: Date.now(), name, grade: Number(grade) });

setName(''); setGrade('');

}}>Add</button>

</div>

<ul>

{students.map(s => (

<li key={s.id}>

{s.name}: {s.grade}

<button onClick={() => removeStudent(s.id)}>Remove</button>

</li>

))}

</ul>

</div>

);

}
← Navigation & Middleware Performance --- Caching, Suspense & Stre →

Log in to track your progress and earn badges as you complete lessons.

Log In to Track Progress