React主题切换时用useState更新Image组件的问题求助
React新手搭建作品集网站,需求是点击Switcher组件的主题切换按钮时,Home组件中的图片同步切换。使用Tailwind CSS,切换按钮时会给html元素添加/移除dark类。当前实现能切换图片,但控制台抛出「Maximum update depth exceeded」警告,提示Home.js中Hook使用不规范,需要优化。
控制台警告信息:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
at Home (http://localhost:3000/main.428ce48dfed5fa0fec8a.hot-update.js:38:74)
at div
at App
核心问题
Home组件里的useEffect未设置依赖数组,导致每次组件渲染都会触发该effect,而effect内调用setImgg又会触发组件重新渲染,形成无限循环,进而触发更新深度超限警告。此外,直接将JSX元素存入state并非React最佳实践,应仅存储数据,渲染时再生成JSX。
具体优化方案
方案一:状态提升(推荐)
将主题状态(isDark)放到父组件(如App)中,通过props传递给Home和Switcher,实现状态统一管理,避免直接操作DOM,彻底解决无限循环问题。
修改后的App.js(父组件示例)
import React, { useState, useEffect } from 'react'; import Home from './Home'; import Switcher from './Switcher'; function App() { const [isDark, setIsDark] = useState(true); // 同步html的dark类 useEffect(() => { const html = document.querySelector('html'); isDark ? html.classList.add('dark') : html.classList.remove('dark'); }, [isDark]); const toggleTheme = () => setIsDark(prev => !prev); return ( <div className="App"> <Switcher isDark={isDark} onToggle={toggleTheme} /> <Home isDark={isDark} /> </div> ); } export default App;
修改后的Home.js
import React from 'react'; import { AiFillGithub } from "react-icons/ai"; import { SiFacebook, SiCodepen, SiLinkedin } from "react-icons/si"; import darkImg from "../images/bg.jpg"; import lightImg from "../images/bg-light.jpg"; function Home({ isDark }) { // 根据状态直接选择图片,无需存储JSX到state const currentImg = isDark ? darkImg : lightImg; return ( <div className="h-full mt-[2px] flex-row"> <img className="ml-14" src={currentImg} alt="home" /> <div className="font-arvo tracking-wider text-center text-7xl">PORTFOLIO</div> <div className="text-center text-4xl">Software Engineer</div> <div className="flex flex-row items-center justify-center mt-5 space-x-7"> <a href="#"><AiFillGithub className="social-icons" size="30"/></a> <a href="#"><SiFacebook className="social-icons" size="30"/></a> <a href="#"><SiCodepen className="social-icons" size="30"/></a> <a href="#"><SiLinkedin className="social-icons" size="30"/></a> </div> </div> ); } export default Home;
修改后的Switcher.js
import React from 'react'; import { HiLightBulb } from "react-icons/hi"; import { MdDarkMode } from "react-icons/md"; function Switcher({ isDark, onToggle }) { // 根据状态动态计算样式和图标,无需存储到state const decorStyle = isDark ? "flex flex-row w-6 h-6 md:w-7 md:h-7 relative rounded-full transition duration-500 transform bg-two -translate-x-2 pt-1 pl-0.5 text-black dark:bg-nine dark:text-seven" : "flex flex-row w-6 h-6 md:w-7 md:h-7 relative rounded-full transition duration-500 transform bg-two translate-x-full pt-1 pl-0.5 text-black dark:bg-nine dark:text-seven"; const currentIcon = isDark ? <MdDarkMode size="20"/> : <HiLightBulb size="20"/>; return ( <Toggler decor={decorStyle} icon={currentIcon} onToggle={onToggle}/> ); } const Toggler = (props) => { return ( <button onClick={props.onToggle} className="absolute top-9 right-10 w-14 h-5 rounded-2xl flex items-center transition duration-300 focus:outline-none shadow bg-four dark:bg-white"> <div id="switch-toggle" className={props.decor}> {props.icon} </div> </button> ); }; export default Switcher;
方案二:不提升状态,修复Home组件无限循环
若不想修改组件结构,可给Home的useEffect添加空依赖数组,同时用MutationObserver监听html的class变化,仅在组件挂载时初始化一次,之后仅在class变化时更新图片。
修改后的Home.js
import React, { useEffect, useState } from 'react'; import { AiFillGithub } from "react-icons/ai"; import { SiFacebook, SiCodepen, SiLinkedin } from "react-icons/si"; import darkImg from "../images/bg.jpg"; import lightImg from "../images/bg-light.jpg"; function Home(props) { const html = document.querySelector("html"); const [isDark, setIsDark] = useState(html.classList.contains("dark")); useEffect(() => { // 更新图片的逻辑 const updateImage = () => { setIsDark(html.classList.contains("dark")); }; // 监听html的class属性变化 const observer = new MutationObserver(updateImage); observer.observe(html, { attributes: true, attributeFilter: ['class'] }); // 组件卸载时清理监听 return () => observer.disconnect(); }, []); // 空依赖数组,仅在组件挂载时执行一次 return ( <div className="h-full mt-[2px] flex-row"> <img className="ml-14" src={isDark ? darkImg : lightImg} alt="home" /> <div className="font-arvo tracking-wider text-center text-7xl">PORTFOLIO</div> <div className="text-center text-4xl">Software Engineer</div> <div className="flex flex-row items-center justify-center mt-5 space-x-7"> <a href="#"><AiFillGithub className="social-icons" size="30"/></a> <a href="#"><SiFacebook className="social-icons" size="30"/></a> <a href="#"><SiCodepen className="social-icons" size="30"/></a> <a href="#"><SiLinkedin className="social-icons" size="30"/></a> </div> </div> ); } export default Home;
内容的提问来源于stack exchange,提问作者tsukingo

