How to manage state in Next.js?
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.