You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);

问题分析

  1. 清理函数引用不匹配:第一个useEffect的清理函数中,removeEventListener传入的是匿名函数,和添加监听时的handleResize不是同一个引用,导致resize监听无法被正确移除,可能引发内存泄漏,同时影响后续width更新逻辑。
  2. values更新逻辑低效且有风险:第二个useEffect中通过forEach循环多次调用setValues,每次更新依赖前一次状态,容易导致状态覆盖;且breakpoints未加入依赖数组,当breakpoints变化时不会重新计算values。
  3. 初始状态不合理:values初始化为undefined,第一次更新时...prev会抛出类型错误,同时影响Context初始值的有效性。
  4. 冗余的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 05:01:02