本地状态与上下文
通过在 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
useThemeto consume our global context.Lines 8–9: We use
useStateto create local state variables:namefor the form input andmessagefor the submission status.Line 10: We call
useTheme()to retrieve the currentthemeand thetoggleThemefunction from our global context.Lines 12–15: When the form is submitted,
handleSubmitprevents a page reload and updates themessagestate to show feedback to the user.Lines 17–23: We define a
panelStyleobject that dynamically changes background and text color based on whether the theme islightordark.Lines 30–34: The
<input>field is a controlled component with itsvalueandonChangehandler tied to the localnamestate.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
themeis set to a fixed default value ("light"), ensuring consistent server-client rendering and preventing hydration errors.Lines 13–19: This
useEffectruns after the component mounts on the client and loads any previously saved theme fromlocalStorage.Lines 21–25: This
useEffectruns wheneverthemechanges, updating bothlocalStorageand the<html>element’sdata-themeattribute 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
ThemeProvideris imported and wrapped around thechildren. This makes the theme context available to all components throughout the entire application.