Next.js provides multiple state management solutions, and developers can choose the appropriate approach based on project requirements. Here are the commonly used state management methods in Next.js:
1. React Built-in State Management
useState Hook
Used for managing component local state.
javascript'use client'; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> ); }
useReducer Hook
Used for managing complex state logic.
javascript'use client'; import { useReducer } from 'react'; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } export default function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </div> ); }
useContext Hook
Used for sharing state across components.
javascript'use client'; import { createContext, useContext, useState } from 'react'; const ThemeContext = createContext(); export function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within ThemeProvider'); } return context; } // Usage export default function App() { return ( <ThemeProvider> <Header /> <Content /> </ThemeProvider> ); } function Header() { const { theme, setTheme } = useTheme(); return ( <header> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> </header> ); }
2. Global State Management Libraries
Zustand
Lightweight, simple state management library.
javascript// store/useStore.js import { create } from 'zustand'; export const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })); // Usage 'use client'; import { useStore } from '@/store/useStore'; export default function Counter() { const { count, increment, decrement, reset } = useStore(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>Reset</button> </div> ); }
Redux Toolkit
Powerful state management library suitable for large applications.
javascript// store/slices/counterSlice.js import { createSlice } from '@reduxjs/toolkit'; export const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer; // store/index.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './slices/counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer, }, }); // store/hooks.js import { useDispatch, useSelector } from 'react-redux'; import type { TypedUseSelectorHook } from 'react-redux'; import type { RootState, AppDispatch } from './index'; export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; // Usage 'use client'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { increment, decrement, incrementByAmount } from '@/store/slices/counterSlice'; export default function Counter() { const count = useAppSelector((state) => state.counter.value); const dispatch = useAppDispatch(); return ( <div> <p>Count: {count}</p> <button onClick={() => dispatch(increment())}>+</button> <button onClick={() => dispatch(decrement())}>-</button> <button onClick={() => dispatch(incrementByAmount(10))}>+10</button> </div> ); }
Jotai
Atomic state management, similar to Recoil.
javascript// store/atoms.js import { atom } from 'jotai'; export const countAtom = atom(0); export const doubledCountAtom = atom((get) => get(countAtom) * 2); // Usage 'use client'; import { useAtom } from 'jotai'; import { countAtom, doubledCountAtom } from '@/store/atoms'; export default function Counter() { const [count, setCount] = useAtom(countAtom); const [doubledCount] = useAtom(doubledCountAtom); return ( <div> <p>Count: {count}</p> <p>Doubled: {doubledCount}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> ); }
3. Server State Management
SWR
Used for data fetching and caching.
javascript'use client'; import useSWR from 'swr'; const fetcher = (url) => fetch(url).then((res) => res.json()); export default function UserProfile() { const { data, error, isLoading } = useSWR('/api/user', fetcher); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ); }
React Query (TanStack Query)
Powerful data fetching and state management library.
javascript'use client'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; async function fetchUser() { const res = await fetch('/api/user'); return res.json(); } async function updateUser(data) { const res = await fetch('/api/user', { method: 'PUT', body: JSON.stringify(data), }); return res.json(); } export default function UserProfile() { const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery({ queryKey: ['user'], queryFn: fetchUser, }); const mutation = useMutation({ mutationFn: updateUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['user'] }); }, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> <button onClick={() => mutation.mutate({ name: 'New Name' })}> Update Name </button> </div> ); }
4. Form State Management
React Hook Form
High-performance form state management.
javascript'use client'; import { useForm } from 'react-hook-form'; export default function ContactForm() { const { register, handleSubmit, formState: { errors } } = useForm(); const onSubmit = (data) => { console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label>Name</label> <input {...register('name', { required: true })} /> {errors.name && <span>This field is required</span>} </div> <div> <label>Email</label> <input {...register('email', { required: true, pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i })} /> {errors.email && <span>Invalid email</span>} </div> <button type="submit">Submit</button> </form> ); }
5. URL State Management
Using URL Query Parameters
javascript'use client'; import { useRouter, useSearchParams } from 'next/navigation'; export default function ProductList() { const router = useRouter(); const searchParams = useSearchParams(); const page = parseInt(searchParams.get('page') || '1'); const category = searchParams.get('category') || 'all'; const handlePageChange = (newPage) => { const params = new URLSearchParams(searchParams.toString()); params.set('page', newPage.toString()); router.push(`?${params.toString()}`); }; const handleCategoryChange = (newCategory) => { const params = new URLSearchParams(searchParams.toString()); params.set('category', newCategory); params.set('page', '1'); router.push(`?${params.toString()}`); }; return ( <div> <select value={category} onChange={(e) => handleCategoryChange(e.target.value)} > <option value="all">All</option> <option value="electronics">Electronics</option> <option value="clothing">Clothing</option> </select> <div>Current page: {page}</div> <button onClick={() => handlePageChange(page - 1)}>Previous</button> <button onClick={() => handlePageChange(page + 1)}>Next</button> </div> ); }
6. Server Component State
Using Server Components
javascript// Server components don't need client-side state management async function ProductList() { const products = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } }).then(r => r.json()); return ( <div> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> ); }
State Management Best Practices
-
Choose the right tool:
- Simple state: useState, useReducer
- Cross-component state: useContext
- Global state: Zustand, Redux Toolkit
- Server state: SWR, React Query
- Form state: React Hook Form
-
Minimize state: Only store necessary state, derive other state
-
Server-first: Use server components as much as possible to reduce client-side state
-
Avoid over-engineering: Don't introduce complex state management libraries for simple state
-
Type safety: Use TypeScript to ensure type safety
-
Performance optimization: Use React.memo, useMemo, useCallback to optimize performance
-
Persistence: Use localStorage or IndexedDB to persist important state
By properly choosing and using these state management methods, you can build efficient and maintainable Next.js applications.