React Context值未更新求助:自定义断点Provider状态失效
自定义Context Provider无法更新数据的问题排查与解决
问题概述
开发自定义Breakpoints Context Provider时,发现Context始终保持默认值,无法通过内部函数更新数据。使用NextJS框架,相关代码及调用方式如下:
相关代码
useBreakpoints.tsx
// Functions import { createContext } from 'react'; // Hooks import { useContext, useEffect, useState } from 'react'; // Types import type { ReactNode } from 'react'; type Width = number | undefined; interface Breakpoints { [key: number | string]: number; } interface Values { [key: number | string]: boolean; } interface IProps { children: ReactNode; breakpoints: Breakpoints; } // Context export const BreakpointsContext = createContext({}); export const BreakpointsProvider = ({ children, breakpoints }: IProps) => { const [width, setWidth] = useState<Width>(undefined); const [values, setValues] = useState<Values | undefined>(undefined); useEffect(() => { if (typeof window !== 'undefined') { const handleResize = () => { setWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); handleResize(); return () => window.removeEventListener('resize', () => { handleResize(); }); } }, []); useEffect(() => { if (width !== undefined) { const handleValues = () => { Object.keys(breakpoints).forEach((breakpoint, index) => { setValues((prev) => ({ ...prev, [breakpoint]: width >= Object.values(breakpoints)[index], })); }); }; handleValues(); return () => window.removeEventListener('resize', handleValues); } }, [width]); const exposed = { width, values, }; return <BreakpointsContext.Provider value={exposed}>{children}</BreakpointsContext.Provider>; }; export const useBreakpoints = () => useContext(BreakpointsContext); export default BreakpointsProvider;
_app.tsx
// Providers import { ThemeProvider } from 'next-themes'; import BreakpointsProvider from '../hooks/useBreakpoints'; // Variables import { Style } from '@master/css'; // Styles import '@master/normal.css'; import '../styles/master.css'; // Types import { ReactElement, ReactNode } from 'react'; import type { NextPage } from 'next'; import type { AppProps } from 'next/app'; export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & { getLayout?: (page: ReactElement) => ReactNode; }; interface AppPropsWithLayout extends AppProps { Component: NextPageWithLayout; } function MyApp({ Component, pageProps }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => page); return getLayout( <ThemeProvider themes={Style.colorSchemes} attribute="class"> <BreakpointsProvider breakpoints={Style.breakpoints}> <Component {...pageProps} /> </BreakpointsProvider> </ThemeProvider> ); } export default MyApp;
组件调用
const test = useBreakpoints(); console.log(test);
问题分析
- 清理函数引用不匹配:第一个
useEffect的清理函数中,removeEventListener传入的是匿名函数,和添加监听时的handleResize不是同一个引用,导致resize监听无法被正确移除,可能引发内存泄漏,同时影响后续width更新逻辑。 - values更新逻辑低效且有风险:第二个
useEffect中通过forEach循环多次调用setValues,每次更新依赖前一次状态,容易导致状态覆盖;且breakpoints未加入依赖数组,当breakpoints变化时不会重新计算values。 - 初始状态不合理:
values初始化为undefined,第一次更新时...prev会抛出类型错误,同时影响Context初始值的有效性。 - 冗余的resize监听:第二个
useEffect中添加了不必要的resize监听,第一个useEffect已经监听resize并更新width,只需依赖width变化即可更新values。
解决方案
修改useBreakpoints.tsx,修复上述问题:
// Functions import { createContext } from 'react'; // Hooks import { useContext, useEffect, useState } from 'react'; // Types import type { ReactNode } from 'react'; type Width = number | undefined; interface Breakpoints { [key: number | string]: number; } interface Values { [key: number | string]: boolean; } interface BreakpointsContextType { width: Width; values: Values; } interface IProps { children: ReactNode; breakpoints: Breakpoints; } // Context:设置正确的默认值和类型 export const BreakpointsContext = createContext<BreakpointsContextType>({ width: undefined, values: {} }); export const BreakpointsProvider = ({ children, breakpoints }: IProps) => { const [width, setWidth] = useState<Width>(undefined); const [values, setValues] = useState<Values>({}); // 初始化为空对象 useEffect(() => { if (typeof window !== 'undefined') { const handleResize = () => { setWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); handleResize(); // 初始化width // 清理函数直接传入handleResize,保证引用一致 return () => window.removeEventListener('resize', handleResize); } }, []); useEffect(() => { if (width !== undefined && breakpoints) { // 一次性计算所有breakpoint状态,避免多次setValues const newValues: Values = {}; Object.entries(breakpoints).forEach(([key, breakpointValue]) => { newValues[key] = width >= breakpointValue; }); setValues(newValues); } }, [width, breakpoints]); // 添加breakpoints到依赖数组 const exposed = { width, values, }; return <BreakpointsContext.Provider value={exposed}>{children}</BreakpointsContext.Provider>; }; export const useBreakpoints = () => useContext(BreakpointsContext); export default BreakpointsProvider;
关键修改点说明
- 为
BreakpointsContext定义明确的类型BreakpointsContextType,并设置合理的默认值,避免类型不匹配问题。 - 修复第一个
useEffect的清理函数,确保移除监听时使用和添加时相同的函数引用。 - 重构values计算逻辑,通过
Object.entries遍历breakpoints,一次性生成完整的values对象后再调用setValues,避免多次状态更新。 - 将
breakpoints加入第二个useEffect的依赖数组,确保breakpoints变化时重新计算values。 - 将
values初始值设为空对象{},避免undefined导致的展开错误。
内容的提问来源于stack exchange,提问作者Kozak
相关产品推荐
相关产品推荐

