React Context+Hooks状态无法填充及测试报错问题求助
React 18 Context 初始化与测试问题
问题背景
我编写了一段React 18的Context Provider代码,用于异步加载配置并更新Context,但测试时遇到两个核心问题:
- Context中的
internalConfig始终是空对象{},无论在Provider还是Consumer中都无法获取到正确的配置值 - 测试过程中出现组件更新冲突警告,以及错误边界提示
Provider 核心代码
import { Dispatch, ReactNode, SetStateAction, Suspense, createContext, useEffect, useState } from "react"; import { InternalConfig } from "../types/config"; import config from "../src/config"; import React from "react"; export const EtechUIContext = createContext<[InternalConfig, Dispatch<SetStateAction<InternalConfig>>] | []>([]); export default function EtechUIProvider({ children, configPath, options }: { children: ReactNode; configPath: string; options?: { filter: string; }; }) { const [internalConfig, setInternalConfig] = useState<InternalConfig>({} as InternalConfig); const [calledTimes, setCalledTimes] = useState(0); useEffect(() => { const init = async (configPath: string, options?: { filter: string }) => { try { const newConfig = await config().init(configPath, options); setInternalConfig(newConfig); setCalledTimes(0); } catch (err: any) { setCalledTimes((prev) => prev++); if (calledTimes >= 3) { throw new Error("Failed to initialize eTech UI. This is likely a bug, please report it :)"); } } }; if (Object.keys(internalConfig).length === 0) { init(configPath, options); } }, [internalConfig, calledTimes]); return ( <Suspense fallback={<p>Loading...</p>}> <EtechUIContext.Provider value={[internalConfig, setInternalConfig]}>{children}.</EtechUIContext.Provider> </Suspense> ); }
测试时的错误信息
The above error occurred in the <TestConsumer> component: at TestConsumer (/Users/ekrich/git/etech-ui/packages/etech-ui-utils/contexts/tests/EtechUI.spec.tsx:17:58) at Suspense at EtechUIProvider (/Users/ekrich/git/etech-ui/packages/etech-ui-utils/contexts/EtechUI.tsx:10:3) Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries. Warning: Cannot update a component (`EtechUIProvider`) while rendering a different component (`TestConsumer`). To locate the bad setState() call inside `TestConsumer`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render at TestConsumer (/Users/ekrich/git/etech-ui/packages/etech-ui-utils/contexts/tests/EtechUI.spec.tsx:26:73) at Suspense at EtechUIProvider (/Users/ekrich/git/etech-ui/packages/etech-ui-utils/contexts/EtechUI.tsx:10:3)
注:实际未提供上述错误的具体详情
我已经尝试按照提示使用Suspense组件,但问题仍未解决。
测试代码(Vitest + React Testing Library)
import EtechUiProvider, { EtechUIContext } from "../EtechUI"; import { describe, it, expect } from "vitest"; import { render } from "@testing-library/react"; import React, { useContext } from "react"; describe("EtechUIContext", () => { it("should accept children into the provider", () => { render( <EtechUiProvider configPath="./etech.config.ts" options={{ filter: "etech-ui-utils-tests" }}> Testing </EtechUiProvider> ); }); it("should contain an InternalConfig in context", () => { function TestConsumer() { expect(useContext(EtechUIContext)[0]?.configName).toBe("etech-ui-utils-tests"); return null; } render( <EtechUiProvider configPath="./etech.config.ts" options={{ filter: "etech-ui-utils-tests" }}> <TestConsumer /> </EtechUiProvider> ); }); it("should have a setInternalConfig function that updates the InternalConfig", () => { function TestConsumer() { const [internalConfig, setInternalConfig] = useContext(EtechUIContext); console.log("internal", internalConfig); expect(setInternalConfig).toBeDefined(); // @ts-ignore // Ignore error: cannot invoke null object. // The above expect statement ensures that setInternalConfig is defined setInternalConfig({ name: "etech-ui-utils", configName: "etech-ui-utils-tests", uiDir: "packages/etech-ui-utils/ui", theme: { colors: { primary: "red", secondary: "purple", success: "green", warning: "orange", error: "red" }, colorMode: "browser" } }); expect(internalConfig?.theme.colors.primary).toBe("red"); return null; } render( <EtechUiProvider configPath="./etech.config.ts" options={{ filter: "etech-ui-utils-tests" }}> <TestConsumer /> </EtechUiProvider> ); }); });
目前只有should accept children into the provider这个测试用例能够通过。
尝试的修改(使用act回调)
我尝试用act包裹测试逻辑,但Context中的InternalConfig仍然是空对象{}:
import EtechUiProvider, { EtechUIContext } from "../EtechUI"; import { describe, it, expect } from "vitest"; import { act, render, renderHook } from "@testing-library/react"; import React, { Context, useContext, useEffect } from "react"; const usePopulated = (context: Context<any[]>, expected: string) => { const [internalConfig] = useContext(context); if (internalConfig.configName === expected) { return true; } return false; }; describe("EtechUIContext", () => { it("should accept children into the provider", () => { render( <EtechUiProvider configPath="./etech.config.ts" options={{ filter: "etech-ui-utils-tests" }}> Testing </EtechUiProvider> ); }); it("should have a setInternalConfig function that updates the InternalConfig", async () => { function TestConsumer() { const [internalConfig, setInternalConfig] = useContext(EtechUIContext); useEffect(() => { if (setInternalConfig) { // @ts-ignore setInternalConfig({ name: "etech-ui-utils", configName: "etech-ui-utils-tests", uiDir: "packages/etech-ui-utils/ui", theme: { colors: { primary: "red", secondary: "purple", success: "green", warning: "orange", error: "red" }, colorMode: "browser" } }); } }, []); return null; } act(() => { const { result } = renderHook(() => usePopulated(EtechUIContext, "etech-ui-utils-tests"), { wrapper: ({ children }) => ( <EtechUiProvider configPath="./etech.config.ts" options={{ filter: "etech-ui-utils-tests" }}> {children} </EtechUiProvider> ) }); expect(result.current).toBe(true); }); }); });
特此求助解决上述问题。
内容的提问来源于stack exchange,提问作者Ethan
相关产品推荐
相关产品推荐

