React如何动态设置body id实现适配弹窗的全局暗色模式
问题原因
Reactstrap 提供的 Modal 组件默认通过 Portal 能力直接挂载到 <body> 节点下,不在自定义的应用根div内部,因此之前绑定在根div上的id="dark"标识无法被Modal继承,导致暗色样式选择器命中失败。给每个Modal单独加id的方案重复工作量大,后期维护成本极高,不推荐使用。
推荐方案
方案1:动态同步暗色状态到body标签(改造成本最低,完全兼容现有CSS代码)
直接操作body标签不属于违反React开发规范的行为:React仅接管挂载到应用根节点内部的DOM渲染,<html>、<body>这类根节点外的全局DOM元素,本身就不在虚拟DOM的管控范围内,通过副作用钩子同步状态到这类全局DOM,是React官方认可的标准处理方式,不会产生渲染冲突。
针对现有的类组件写法,只需要加一个同步方法,在状态变化时更新body的id即可:
// 根组件代码 componentDidMount() { // 初始化同步暗色状态 this.syncDarkModeToBody(); this.manageDarkMode(); } componentDidUpdate(prevProps, prevState) { // 暗色状态变更时同步更新body属性 if (prevState.should_enable_dark_mode !== this.state.should_enable_dark_mode) { this.syncDarkModeToBody(); } } componentWillUnmount() { // 移除事件监听避免内存泄漏 window.removeEventListener("storage", this.handleStorageChange); } // 同步暗色状态到body syncDarkModeToBody() { document.body.id = this.state.should_enable_dark_mode ? "dark" : ""; } // 抽离storage事件回调方便卸载时移除 handleStorageChange = () => { const should_enable_dark_mode = localStorage.darkMode == "true"; this.setState({ should_enable_dark_mode }); } manageDarkMode() { window.addEventListener("storage", this.handleStorageChange); } render() { // 根div不需要再绑定dark id,body上的id可以覆盖所有内部元素包括Modal return ( <div className="app-container"> {/* 原有应用内容 */} </div> ) }
如果是函数组件写法,用useEffect实现更简洁:
const [shouldEnableDarkMode, setShouldEnableDarkMode] = useState(localStorage.darkMode === "true"); // 状态变化时同步到body useEffect(() => { document.body.id = shouldEnableDarkMode ? "dark" : ""; }, [shouldEnableDarkMode]); // 监听localStorage变更 useEffect(() => { const handleStorageChange = () => { setShouldEnableDarkMode(localStorage.darkMode === "true"); }; window.addEventListener("storage", handleStorageChange); return () => window.removeEventListener("storage", handleStorageChange); }, []);
这个方案完全不需要修改之前写的所有暗色模式CSS代码,不管是普通页面组件还是动态挂载的Modal,只要是body下的元素都能被#dark .xxx的选择器命中,改造成本极低。
方案2:修改Modal默认挂载节点(适合不想修改全局body属性的场景)
如果不想改动body标签,可以利用Reactstrap Modal提供的container属性,指定Modal挂载到自己的应用根节点内部,这样Modal就能继承根div上的dark id。
为了避免每个Modal单独加配置,直接封装一个通用的AppModal组件替换原生Modal即可:
// 封装全局通用Modal组件 const AppModal = (props) => { // 指向你的应用根节点,一般项目默认根节点id为root const appRoot = document.getElementById('root'); return <Modal container={appRoot} {...props} /> }
后续项目中所有使用Modal的地方,统一替换为封装的AppModal,不需要给每个Modal单独加id,就能自动继承根节点的暗色样式。
优化提示
- 当前CSS中大量使用的
!important是为了覆盖第三方组件的默认样式,等暗色模式逻辑跑通后,可以通过提高选择器优先级逐步移除冗余的!important,降低样式维护成本。 - 如果后续需要跟随系统暗色模式自动切换,只需要在状态初始化时加上
window.matchMedia('(prefers-color-scheme: dark)').matches的判断即可,不需要改动核心同步逻辑。
内容的提问来源于stack exchange,提问作者AG_HIHI

