Logo
Section 1 / 7

本地状态与上下文

通过在 Next.js 服务器组件中嵌入客户端组件,管理状态、副作用和上下文。

导入

本地状态与上下文

通过在 Next.js 服务器组件中嵌入客户端组件,管理状态、副作用和上下文。

No configuration found for ID: eddcd3f3c564c51136c1055b72acd02ae5a8393788bb1cb141af0e36e73e38bb

No configuration found for ID: eb4873d41db95a8172ed6e0e94454dbd24dd135e76703138fb5858db46c2f9ef

A client-side provider wrapping server-rendered children

1// context/CartProvider.js 2"use client"; 3 4import { createContext, useState, useContext } from 'react'; 5 6const CartContext = createContext(); 7 8export const useCart = () => useContext(CartContext); 9 10export const CartProvider = ({ children }) => { 11 const [cartCount, setCartCount] = useState(0); 12 13 const addToCart = () => setCartCount(prev => prev + 1); 14 15 return ( 16 <CartContext.Provider value={{ cartCount, addToCart }}> 17 {children} 18 </CartContext.Provider> 19 ); 20};

No configuration found for ID: 398741c4dd969d6442cf693bf94b9a99f9ed4759c72d97aa45719061c211b442

No configuration found for ID: df696680416b0b85d2547a817932860e4fd0ca615ca3acb34b83f6376ba5892e

Show solution

Let’s walk through the complete solution step by step.

1. The SettingsPanel.js file

This file defines a simple client-side React settings panel that lets users update their name and toggle the app’s theme.

Managing local and global state in a Client Component

1// components/SettingsPanel.js 2"use client"; 3 4import { useState } from 'react'; 5import { useTheme } from '../context/ThemeProvider'; 6 7export default function SettingsPanel() { 8 const [name, setName] = useState('Jane Doe'); 9 const [message, setMessage] = useState(''); 10 const { theme, toggleTheme } = useTheme(); 11 12 const handleSubmit = (e) => { 13 e.preventDefault(); 14 setMessage(`Settings saved for ${name}`); 15 }; 16 17 const panelStyle = { 18 border: '2px solid blue', 19 padding: '1rem', 20 marginTop: '1rem', 21 backgroundColor: theme === 'dark' ? '#222' : '#fff', 22 color: theme === 'dark' ? '#fff' : '#000', 23 }; 24 25 return ( 26 <div style={panelStyle}> 27 <form onSubmit={handleSubmit}> 28 <label> 29 User Name: 30 <input 31 type="text" 32 value={name} 33 onChange={(e) => setName(e.target.value)} 34 /> 35 </label> 36 <button type="submit">Save</button> 37 </form> 38 39 {/* Show success message if present */} 40 {message && <p>{message}</p>} 41 42 <hr style={{ margin: '1rem 0' }} /> 43 <button onClick={toggleTheme}> 44 Toggle Theme 45 </button> 46 <p>Current Theme: {theme}</p> 47 </div> 48 ); 49}

Explanation:

  • Line 5: We import the custom useTheme to consume our global context.

  • Lines 8–9: We use useState to create local state variables: name for the form input and message for the submission status.

  • Line 10: We call useTheme() to retrieve the current theme and the toggleTheme function from our global context.

  • Lines 12–15: When the form is submitted, handleSubmit prevents a page reload and updates the message state to show feedback to the user.

  • Lines 17–23: We define a panelStyle object that dynamically changes background and text color based on whether the theme is light or dark.

  • Lines 30–34: The <input> field is a controlled component with its value and onChange handler tied to the local name state.

  • Lines 43–45: The “Toggle Theme” button is wired to toggleTheme, causing the global theme to switch between light and dark.

  • Line 46: The current theme is displayed and automatically updates whenever the context value changes.

2. The ThemeProvider.js file

This file provides a ThemeProvider context that manages light/dark mode, persists it to localStorage, and exposes a toggle function to the rest of the app.

Creating a persistent theme with context and useEffect

1// context/ThemeProvider.js 2"use client"; 3 4import { createContext, useState, useContext, useEffect } from 'react'; 5 6const ThemeContext = createContext(); 7 8export const useTheme = () => useContext(ThemeContext); 9 10export const ThemeProvider = ({ children }) => { 11 const [theme, setTheme] = useState('light'); // same on server & client 12 13 useEffect(() => { 14 if (typeof window === 'undefined') return; 15 const stored = window.localStorage.getItem('theme'); 16 if (stored) { 17 setTheme(stored); 18 } 19 }, []); 20 21 useEffect(() => { 22 if (typeof window === 'undefined') return; 23 window.localStorage.setItem('theme', theme); 24 document.documentElement.setAttribute('data-theme', theme); 25 }, [theme]); 26 27 const toggleTheme = () => { 28 setTheme(prev => (prev === 'light' ? 'dark' : 'light')); 29 }; 30 31 return ( 32 <ThemeContext.Provider value={{ theme, toggleTheme }}> 33 {children} 34 </ThemeContext.Provider> 35 ); 36};

Explanation:

  • Line 11: The initial state for theme is set to a fixed default value ("light"), ensuring consistent server-client rendering and preventing hydration errors.

  • Lines 13–19: This useEffect runs after the component mounts on the client and loads any previously saved theme from localStorage.

  • Lines 21–25: This useEffect runs whenever theme changes, updating both localStorage and the <html> element’s data-theme attribute to enable CSS-based theme styling.

3. The app/layout.js file

This file sets up the RootLayout by wrapping all app pages in the ThemeProvider so theme state is available globally.

Applying the global provider in the root layout

1// app/layout.js 2import './globals.css'; 3import { ThemeProvider } from './context/ThemeProvider'; 4 5export default function RootLayout({ children }) { 6 return ( 7 <html lang="en"> 8 <body> 9 <ThemeProvider> 10 {children} 11 </ThemeProvider> 12 </body> 13 </html> 14 ); 15}

Explanation:

  • Line 3: The ThemeProvider is imported and wrapped around the children. This makes the theme context available to all components throughout the entire application.

样式化与图片优化

使用 Zustand 管理全局状态