React自定义withSnackbar高阶组件触发不必要全页重渲染问题求助
问题根因
- HOC内部状态联动触发全量重渲染:你当前实现的
withSnackbar将业务组件WrappedComponent和Snackbar组件放在同一渲染层级,只要HOC内部的open、messageData状态发生变化(包括打开/关闭Toast、更新消息内容),整个HOC返回的组件树都会触发重渲染,业务组件必然跟着重新渲染。 - 传入业务组件的回调引用不稳定:每次HOC重渲染时,
showMessage函数都会生成新的函数引用,即使你给WrappedComponent套了React.memo,浅比较props时会检测到showToastMessage属性发生变化,直接导致memo优化失效,业务组件仍然会重渲染。 - 全局状态变更的叠加影响:当接口请求完成触发dispatch操作更新全局状态时,会触发订阅了该状态的组件树重渲染,叠加上面两个问题,就会出现全局状态一更新就触发模板页面全量重渲染的情况。
解决方案
方案1:原地优化现有HOC(改动最小)
核心是稳定回调引用、隔离业务组件和Toast状态的渲染影响,修改后的代码如下:
import { memo, useCallback } from 'react'; const withSnackbar = (WrappedComponent: React.FunctionComponent<any>): React.FunctionComponent<any> => { // 先给业务组件包memo,保证只有props变化时才重渲染 const MemoizedWrapped = memo(WrappedComponent); const LoadedWrappedComponent = (props: any): ReactElement<any> => { const classes = useStyles(); const theme = useTheme(); const [open, setOpen] = useState(false); const [messageData, setMessageData] = useState<IMessageData>({ message: '', severityType: 'info', duration: 2000, vertical: 'bottom', horizontal: 'right', }); // 用useCallback稳定showMessage的引用,依赖只包含会变化的外部变量 const showMessage = useCallback(({ message, severityType, vertical = 'bottom', horizontal = 'right', duration = 2000, }: IMessageData): void => { setMessageData({ message, severityType, duration: horizontal === 'left' ? 5000 : duration, vertical, horizontal: theme.direction === 'ltr' ? 'right' : 'left', }); setOpen(true); }, [theme, setMessageData, setOpen]); const handleClose = useCallback((): void => { setOpen(false); }, [setOpen]); const { message, severityType, duration, horizontal, vertical } = messageData; return ( <> {/* 传入的showToastMessage引用已经稳定,memo会生效 */} <MemoizedWrapped {...props} showToastMessage={showMessage} /> <Snackbar anchorOrigin={{ vertical, horizontal, }} autoHideDuration={duration} open={open} onClose={handleClose} TransitionComponent={Slide} > <Alert classes={{ root: classes.root, }} variant='filled' severity={severityType} action={ horizontal === 'right' ? ( <IconButton> <Icon src='CloseOutlined' onClick={handleClose} /> </IconButton> ) : ( <></> ) } > {message} </Alert> </Snackbar> </> ); }; return LoadedWrappedComponent; }; export default withSnackbar;
方案2:架构优化(彻底解决重渲染问题)
如果有调整架构的空间,建议将全局Toast能力抽离为全局Context+Portal的实现:
- 在应用根节点外层包裹ToastProvider,统一管理Toast的状态和渲染逻辑,通过Portal将Snackbar渲染到根节点下独立的DOM节点
- 业务组件通过
useToast()hook获取showToastMessage方法,不会和Toast的状态产生渲染绑定,Toast状态变化完全不会影响业务组件的渲染。
内容的提问来源于stack exchange,提问作者RTM
相关产品推荐
相关产品推荐

