Skip to content

第8章:状态管理

8.1 客户端状态管理(新手循序渐进)

useState、useReducer(基础状态管理)

useStateuseReducer 是 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 中的状态管理方法,包括客户端状态管理、服务端状态管理和本地存储持久化。通过本章的学习,你应该已经掌握了:

  1. 客户端状态管理:

    • 使用 useState 和 useReducer 进行基础状态管理
    • 使用 Context API + useReducer 进行轻量级全局状态管理
    • 使用 Redux Toolkit 进行复杂项目的状态管理
    • 使用 Zustand 进行轻量级状态管理
  2. 服务端状态管理:

    • 使用 SWR 进行服务端数据的状态管理
    • 使用 React Query 进行服务端数据的状态管理
  3. 本地存储持久化:

    • 使用 localStorage 进行客户端数据的持久化
    • 使用 cookies 进行需要在服务端访问的数据的持久化

选择合适的状态管理方案对于构建可维护的 Next.js 应用至关重要。在实际开发中,建议:

  • 对于简单的状态管理,使用 useState 和 useReducer
  • 对于跨组件的状态管理,使用 Context API + useReducer 或 Zustand
  • 对于大型项目,使用 Redux Toolkit
  • 对于服务端数据,使用 SWR 或 React Query
  • 对于需要持久化的数据,使用 localStorage 或 cookies

接下来,我们将学习 Next.js 的样式解决方案,包括基础样式、CSS 模块化、CSS 预处理器和第三方样式库集成。

© 2026 编程马·菜鸟教程 版权所有