Appearance
第8章:状态管理
8.1 客户端状态管理(新手循序渐进)
useState、useReducer(基础状态管理)
useState 和 useReducer 是 React 内置的状态管理 hooks,适用于简单的状态管理场景。
useState
示例:
jsx
// app/components/Counter.jsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}useReducer
当状态逻辑复杂时,可以使用 useReducer。
示例:
jsx
// app/components/TodoList.jsx
'use client';
import { useReducer } from 'react';
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
export default function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text) {
dispatch({ type: 'ADD_TODO', text });
setText('');
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}Context API + useReducer(轻量级全局状态管理)
对于跨组件的状态管理,可以使用 Context API 结合 useReducer。
示例:
jsx
// app/contexts/AppContext.jsx
'use client';
import { createContext, useContext, useReducer } from 'react';
const AppContext = createContext();
function appReducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.user };
case 'SET_LOADING':
return { ...state, loading: action.loading };
case 'SET_ERROR':
return { ...state, error: action.error };
default:
return state;
}
}
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, {
user: null,
loading: false,
error: null,
});
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppContext must be used within an AppProvider');
}
return context;
}
// app/layout.js
import { AppProvider } from './contexts/AppContext';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<AppProvider>{children}</AppProvider>
</body>
</html>
);
}
// app/components/Login.jsx
'use client';
import { useState } from 'react';
import { useAppContext } from '@/app/contexts/AppContext';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { dispatch } = useAppContext();
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SET_LOADING', loading: true });
try {
// 模拟登录请求
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: 'SET_USER', user: { email } });
dispatch({ type: 'SET_LOADING', loading: false });
} catch (error) {
dispatch({ type: 'SET_ERROR', error: 'Login failed' });
dispatch({ type: 'SET_LOADING', loading: false });
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
// app/components/UserProfile.jsx
'use client';
import { useAppContext } from '@/app/contexts/AppContext';
export default function UserProfile() {
const { state } = useAppContext();
const { user, loading, error } = state;
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return <p>Please login</p>;
return <p>Welcome, {user.email}!</p>;
}Redux Toolkit(RTK)集成 Next.js(复杂项目适用)
对于大型项目,可以使用 Redux Toolkit 进行状态管理。
安装
bash
npm install @reduxjs/toolkit react-redux配置
javascript
// app/store.js
'use client';
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';
import todoReducer from './slices/todoSlice';
export const store = configureStore({
reducer: {
user: userReducer,
todo: todoReducer,
},
});
// app/slices/userSlice.js
import { createSlice } from '@reduxjs/toolkit';
export const userSlice = createSlice({
name: 'user',
initialState: {
user: null,
loading: false,
error: null,
},
reducers: {
setUser: (state, action) => {
state.user = action.payload;
},
setLoading: (state, action) => {
state.loading = action.payload;
},
setError: (state, action) => {
state.error = action.payload;
},
},
});
export const { setUser, setLoading, setError } = userSlice.actions;
export default userSlice.reducer;
// app/slices/todoSlice.js
import { createSlice } from '@reduxjs/toolkit';
export const todoSlice = createSlice({
name: 'todo',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push({ id: Date.now(), text: action.payload, completed: false });
},
toggleTodo: (state, action) => {
const todo = state.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
return state.filter(todo => todo.id !== action.payload);
},
},
});
export const { addTodo, toggleTodo, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
// app/providers.jsx
'use client';
import { Provider } from 'react-redux';
import { store } from './store';
export default function Providers({ children }) {
return <Provider store={store}>{children}</Provider>;
}
// app/layout.js
import Providers from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}使用
jsx
// app/components/Login.jsx
'use client';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { setUser, setLoading, setError } from '@/app/slices/userSlice';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const dispatch = useDispatch();
const handleSubmit = async (e) => {
e.preventDefault();
dispatch(setLoading(true));
try {
// 模拟登录请求
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch(setUser({ email }));
dispatch(setLoading(false));
} catch (error) {
dispatch(setError('Login failed'));
dispatch(setLoading(false));
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
// app/components/UserProfile.jsx
'use client';
import { useSelector } from 'react-redux';
export default function UserProfile() {
const { user, loading, error } = useSelector(state => state.user);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return <p>Please login</p>;
return <p>Welcome, {user.email}!</p>;
}Zustand(轻量简洁,新手易上手,备选方案)
Zustand 是一个轻量级的状态管理库,比 Redux 更简单易用。
安装
bash
npm install zustand配置
javascript
// app/store/useUserStore.js
import { create } from 'zustand';
export const useUserStore = create((set) => ({
user: null,
loading: false,
error: null,
setUser: (user) => set({ user }),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
login: async (email, password) => {
set({ loading: true, error: null });
try {
// 模拟登录请求
await new Promise(resolve => setTimeout(resolve, 1000));
set({ user: { email }, loading: false });
} catch (error) {
set({ error: 'Login failed', loading: false });
}
},
logout: () => set({ user: null }),
}));
// app/store/useTodoStore.js
import { create } from 'zustand';
export const useTodoStore = create((set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }],
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id),
})),
}));使用
jsx
// app/components/Login.jsx
'use client';
import { useState } from 'react';
import { useUserStore } from '@/app/store/useUserStore';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login, loading, error } = useUserStore();
const handleSubmit = async (e) => {
e.preventDefault();
await login(email, password);
};
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
// app/components/UserProfile.jsx
'use client';
import { useUserStore } from '@/app/store/useUserStore';
export default function UserProfile() {
const { user, logout } = useUserStore();
if (!user) return <p>Please login</p>;
return (
<div>
<p>Welcome, {user.email}!</p>
<button onClick={logout}>Logout</button>
</div>
);
}
// app/components/TodoList.jsx
'use client';
import { useState } from 'react';
import { useTodoStore } from '@/app/store/useTodoStore';
export default function TodoList() {
const [text, setText] = useState('');
const { todos, addTodo, toggleTodo, deleteTodo } = useTodoStore();
const handleSubmit = (e) => {
e.preventDefault();
if (text) {
addTodo(text);
setText('');
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}8.2 服务端状态管理(SWR/React Query 应用)
对于服务端数据,可以使用 SWR 或 React Query 进行状态管理。
SWR
示例:
jsx
// app/components/Posts.jsx
'use client';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function Posts() {
const { data, error, isLoading, mutate } = useSWR('https://api.example.com/posts', fetcher);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
const handleRefresh = () => {
mutate();
};
return (
<div>
<h1>Posts</h1>
<button onClick={handleRefresh}>Refresh</button>
<ul>
{data.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}React Query
示例:
jsx
// app/components/Posts.jsx
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
const fetchPosts = async () => {
const response = await fetch('https://api.example.com/posts');
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
return response.json();
};
const createPost = async (post) => {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(post),
});
if (!response.ok) {
throw new Error('Failed to create post');
}
return response.json();
};
export default function Posts() {
const queryClient = useQueryClient();
const { data: posts, error, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// 重新获取 posts
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleCreatePost = () => {
mutation.mutate({ title: 'New Post', excerpt: 'This is a new post' });
};
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>Posts</h1>
<button onClick={handleCreatePost} disabled={mutation.isLoading}>
{mutation.isLoading ? 'Creating...' : 'Create Post'}
</button>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}8.3 本地存储持久化(localStorage、cookies 使用,next/headers 读取)
localStorage
对于客户端数据,可以使用 localStorage 进行持久化。
示例:
jsx
// app/components/TodoList.jsx
'use client';
import { useState, useEffect } from 'react';
export default function TodoList() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState('');
// 从 localStorage 加载数据
useEffect(() => {
const storedTodos = localStorage.getItem('todos');
if (storedTodos) {
setTodos(JSON.parse(storedTodos));
}
}, []);
// 保存数据到 localStorage
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const handleAddTodo = (e) => {
e.preventDefault();
if (text) {
setTodos([...todos, { id: Date.now(), text, completed: false }]);
setText('');
}
};
const handleToggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const handleDeleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<form onSubmit={handleAddTodo}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => handleDeleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}Cookies
对于需要在服务端访问的数据,可以使用 cookies。
客户端设置 cookies
jsx
// app/components/Login.jsx
'use client';
import { useState } from 'react';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
// 模拟登录请求
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (response.ok) {
alert('Login successful');
} else {
alert('Login failed');
}
} catch (error) {
alert('Error: ' + error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}服务端读取 cookies
javascript
// app/api/login/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const { email, password } = await request.json();
// 模拟验证
if (email && password) {
// 设置 cookie
const response = NextResponse.json({ message: 'Login successful' });
response.cookies.set('auth-token', 'your-auth-token', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
return response;
} else {
return NextResponse.json({ message: 'Login failed' }, { status: 401 });
}
}
// app/api/me/route.js
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function GET() {
const cookieStore = cookies();
const token = cookieStore.get('auth-token');
if (token) {
// 验证 token
return NextResponse.json({ message: 'Authenticated' });
} else {
return NextResponse.json({ message: 'Not authenticated' }, { status: 401 });
}
}小结
本章介绍了 Next.js 中的状态管理方法,包括客户端状态管理、服务端状态管理和本地存储持久化。通过本章的学习,你应该已经掌握了:
客户端状态管理:
- 使用 useState 和 useReducer 进行基础状态管理
- 使用 Context API + useReducer 进行轻量级全局状态管理
- 使用 Redux Toolkit 进行复杂项目的状态管理
- 使用 Zustand 进行轻量级状态管理
服务端状态管理:
- 使用 SWR 进行服务端数据的状态管理
- 使用 React Query 进行服务端数据的状态管理
本地存储持久化:
- 使用 localStorage 进行客户端数据的持久化
- 使用 cookies 进行需要在服务端访问的数据的持久化
选择合适的状态管理方案对于构建可维护的 Next.js 应用至关重要。在实际开发中,建议:
- 对于简单的状态管理,使用 useState 和 useReducer
- 对于跨组件的状态管理,使用 Context API + useReducer 或 Zustand
- 对于大型项目,使用 Redux Toolkit
- 对于服务端数据,使用 SWR 或 React Query
- 对于需要持久化的数据,使用 localStorage 或 cookies
接下来,我们将学习 Next.js 的样式解决方案,包括基础样式、CSS 模块化、CSS 预处理器和第三方样式库集成。
