如何避免React中Context Provider辅助函数引发的重复渲染?
问题
优化React Toast组件渲染性能时遇到以下问题:
- 新增Toast时,所有现有Toast都会重新渲染
- 删除某个Toast时,剩余Toast也会重新渲染
相关代码片段:
ToastProvider.jsx
function ToastProvider({ children }) { const [toasts, setToasts] = useState([]) const memoisedClearToast = React.useCallback(() => setToasts([]), []) useKeyDown("Escape", memoisedClearToast) const addToast = (toast) => { setToasts([...toasts, toast]); }; const dismissToast = (toastId) => { const nextToasts = [...toasts] const filtered = nextToasts.filter((toast) => toast.id !== toastId) setToasts(filtered) } const value = React.useCallback({ toasts, addToast, dismissToast }, [toasts]) return ( <ToastContext.Provider value={value}> {children} </ToastContext.Provider> ) }
ToastShelf.jsx
function ToastShelf() { const {toasts} = useContext(ToastContext) console.log(`toast shelf render`) return ( <ol> {toasts.map((toast) => { return ( <li key={toast.id}> <Toast id={toast.id} variant={toast.variant} message={toast.message} /> </li> ) })} </ol> ); } export default React.memo(ToastShelf);
Toast.jsx
function Toast({ id, variant, message }) { const { dismissToast } = useContext(ToastContext) console.log(`toast render`) return ( <div> <button onClick={() => dismissToast(id)}> <X size={24} /> </button> </div> ); } export default React.memo(Toast);
问题根源:addToast和dismissToast函数依赖toasts状态,每次toasts更新时这两个函数会生成新引用,导致Context的value对象更新,进而触发所有消费Context的Toast组件重渲染。
解决方案
1. 用函数式更新稳定函数引用
修改ToastProvider.jsx中的状态更新逻辑,使用React的函数式更新获取最新状态,让addToast和dismissToast不再依赖toasts:
const addToast = React.useCallback((toast) => { // 通过函数式更新获取最新的toasts数组 setToasts(prevToasts => [...prevToasts, toast]); }, []); // 空依赖数组,确保函数引用永久稳定 const dismissToast = React.useCallback((toastId) => { setToasts(prevToasts => prevToasts.filter(toast => toast.id !== toastId)); }, []); // 空依赖数组,函数引用稳定
2. 优化Context的value对象
使用useMemo创建Context的value对象,仅在toasts变化时更新(addToast和dismissToast引用已稳定,不会触发更新):
const value = React.useMemo(() => ({ toasts, addToast, dismissToast }), [toasts, addToast, dismissToast]);
修改后的完整ToastProvider.jsx
function ToastProvider({ children }) { const [toasts, setToasts] = useState([]) const memoisedClearToast = React.useCallback(() => setToasts([]), []) useKeyDown("Escape", memoisedClearToast) const addToast = React.useCallback((toast) => { setToasts(prevToasts => [...prevToasts, toast]); }, []); const dismissToast = React.useCallback((toastId) => { setToasts(prevToasts => prevToasts.filter(toast => toast.id !== toastId)); }, []); const value = React.useMemo(() => ({ toasts, addToast, dismissToast }), [toasts, addToast, dismissToast]); return ( <ToastContext.Provider value={value}> {children} </ToastContext.Provider> ) }
效果说明
ToastShelf仍会在toasts变化时重渲染(正常逻辑,因为需要渲染新的Toast列表)- 现有Toast组件的props(id、variant、message)未变化,且依赖的
dismissToast引用稳定,React.memo会阻止不必要的重渲染 - 新增或删除Toast时,仅目标Toast会触发渲染,其余Toast保持不变
内容的提问来源于stack exchange,提问作者xaovnumwsercy
相关产品推荐
相关产品推荐

