Redux Toolkit中解决DOM元素引用作为Action Payload的非序列化问题
问题描述
我当前使用Redux Toolkit,依赖版本为:react-redux ^8.0.2、redux ^4.2.0。我希望在dispatch一个action时,将HTML元素引用作为payload传递,具体代码如下:
<Stack aria-describedby={id} variant="contained" onClick={(e) => dispatch(open({ele: e.currentTarget}))} //... </Stack>
对应的slice代码:
const initialState = { anchorEl: null, } export const loginPopupSlice = createSlice({ name: 'loginPopup', initialState, reducers: { open: (state,action) => { state.anchorEl = action.payload.ele }, close: (state) => { state.anchorEl = null } } })
但此时出现报错:
react_devtools_backend.js:4026 A non-serializable value was detected in the state, in the path: `loginPopup.anchorEl`. Value:
我曾考虑将HTML DOM转换为字符串再转换回来,但担心这种方式会影响性能。另外需要注意的是,Redux DevTools在存在非序列化值时无法正常工作,因此我希望通过解决该问题来正常使用Redux DevTools。
解决方案
Redux的核心原则之一是状态必须可序列化,DOM元素是复杂的浏览器对象,既不可序列化,也不应该被存到全局状态里(容易引发内存泄漏、DevTools异常等问题)。下面是几种更合理的解决方式:
1. 使用组件局部状态+Ref管理DOM元素(推荐)
弹窗的锚点元素通常只在触发它的组件附近使用,完全不需要放到Redux全局状态中。可以用React的useRef和useState来局部管理:
import { useRef, useState } from 'react'; import { Stack } from '@mui/material'; // 假设你用的是MUI的Stack const YourComponent = () => { const anchorElRef = useRef(null); const [isLoginPopupOpen, setIsLoginPopupOpen] = useState(false); const handleOpenPopup = (e) => { anchorElRef.current = e.currentTarget; setIsLoginPopupOpen(true); }; const handleClosePopup = () => { setIsLoginPopupOpen(false); anchorElRef.current = null; }; return ( <> <Stack aria-describedby={id} variant="contained" onClick={handleOpenPopup} > 点击打开登录弹窗 </Stack> {/* 假设你的登录弹窗组件接收anchorEl和onClose属性 */} {isLoginPopupOpen && ( <LoginPopup anchorEl={anchorElRef.current} onClose={handleClosePopup} /> )} </> ); };
这种方式既避免了序列化问题,又符合React组件局部状态的使用场景,性能也完全没问题。
2. 存储可序列化的元素标识(适合必须用Redux的场景)
如果因为业务需求必须在Redux中管理弹窗的触发源,可以给目标元素设置唯一ID,传递ID而非DOM元素本身,需要使用时再通过document.getElementById获取元素:
修改组件代码:
<Stack id="login-trigger-element" aria-describedby={id} variant="contained" onClick={() => dispatch(open({ elementId: 'login-trigger-element' }))} > 点击打开登录弹窗 </Stack>
修改slice代码:
const initialState = { anchorElementId: null }; export const loginPopupSlice = createSlice({ name: 'loginPopup', initialState, reducers: { open: (state, action) => { state.anchorElementId = action.payload.elementId; }, close: (state) => { state.anchorElementId = null; } } });
在需要获取锚点元素的组件中:
import { useSelector } from 'react-redux'; const SomeComponent = () => { const anchorElementId = useSelector(state => state.loginPopup.anchorElementId); const anchorEl = anchorElementId ? document.getElementById(anchorElementId) : null; return ( <LoginPopup anchorEl={anchorEl} /> ); };
这种方式完全符合Redux的序列化要求,DevTools也能正常工作。
3. 临时禁用特定路径的序列化检查(不推荐)
如果以上两种方式都不适用,可以在配置Store时忽略该路径的序列化检查,但这只是绕过问题,不是根本解决,可能依然会导致DevTools功能异常:
import { configureStore } from '@reduxjs/toolkit'; import loginPopupReducer from './loginPopupSlice'; export const store = configureStore({ reducer: { loginPopup: loginPopupReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredPaths: ['loginPopup.anchorEl'], }, }), });
总结
优先推荐第一种方案,用组件局部状态管理DOM元素,这是最符合React和Redux设计理念的做法。如果必须用Redux,就选择第二种方式存储元素ID。尽量避免第三种绕过检查的方式,长期来看会带来潜在问题。
内容的提问来源于stack exchange,提问作者Ali Shefaee

