自定义Hook无法获取Context更新值,原因是什么?
我尝试在自定义Hook里调用一个能在多个组件和Context中复用的函数,但这个Hook始终拿不到更新后的Context值,一直是空数组。但Context里的状态明明已经更新了,组件也能正常接收到更新后的Context值,这是怎么回事?看起来Context的值没传递给Hook,但我找不到原因。
代码示例
Context
import { createContext, useState, useEffect } from "react"; import useTest from "./useTest"; const TestContext = createContext({ test: [], testContextFn: () => {} }); export const TestProvider = ({ children }) => { const [testState, setTestState] = useState([]); const { testFn } = useTest(); const testHandlerHandler = () => { const test = 1; setTestState((current) => [...current, test]); testFn(); }; const contextValue = { test: testState, testContextFn: testHandlerHandler }; useEffect(() => { console.log("state: ", testState); // => 能拿到更新后的值 }, [testState]); return ( <TestContext.Provider value={contextValue}>{children}</TestContext.Provider> ); };
自定义Hook
import { useContext } from "react"; import { TestContext } from "./hooks/textContext"; const useTest = () => { const testCtx = useContext(TestContext); const testFn = () => { console.log("testCtx: ", testCtx.test); // => 始终是空数组 }; return { testFn }; }; export default useTest;
组件
const App = () => { return ( <TestProvider> <ExampleComponent /> </TestProvider> ); }; const ExampleComponent = () => { const testCtx = useContext(TestContext); console.log("testCtx: ", testCtx.test); // => 能拿到更新后的值 return <div onClick={testCtx.testContextFn}>CLICK HERE</div>; };
核心问题
Provider自身无法获取自己提供的Context值
你的TestProvider是TestContext.Provider的定义组件,但它自己并没有被包裹在这个Provider内部。所以当你在TestProvider里调用useTest时,useTest通过useContext拿到的是Context的默认空数组,根本不是TestProvider动态生成的最新值。这就是testFn始终打印空数组的直接原因。闭包捕获旧值
退一步说,就算Provider能拿到自己的Context值,testFn在创建时会捕获当时的testCtx引用。后续Context更新时,旧的testFn依然会指向旧的testCtx,除非testFn重新创建并关联最新值。
解决方法
方案一:调用testFn时直接传入最新状态
这种方法简单直接,绕过Context的闭包问题:
修改TestProvider中的testHandlerHandler,计算新状态后直接传给testFn:
const testHandlerHandler = () => { const test = 1; const newTestState = [...testState, test]; setTestState(newTestState); // 把新状态作为参数传给testFn testFn(newTestState); };
修改useTest,接收传入的状态值:
const useTest = () => { const testFn = (updatedTest) => { console.log("testCtx: ", updatedTest); }; return { testFn }; };
方案二:在子组件中使用useTest(推荐)
既然useTest需要访问最新的Context值,就应该把它放到能获取到Context的子组件里,而不是在Provider内部调用:
首先修改TestProvider,移除内部的useTest调用:
export const TestProvider = ({ children }) => { const [testState, setTestState] = useState([]); const testHandlerHandler = () => { const test = 1; setTestState((current) => [...current, test]); }; const contextValue = { test: testState, testContextFn: testHandlerHandler }; useEffect(() => { console.log("state: ", testState); }, [testState]); return ( <TestContext.Provider value={contextValue}>{children}</TestContext.Provider> ); };
然后修改ExampleComponent,在组件内部调用useTest并执行testFn:
const ExampleComponent = () => { const testCtx = useContext(TestContext); const { testFn } = useTest(); console.log("testCtx: ", testCtx.test); const handleClick = () => { testCtx.testContextFn(); // 这里调用testFn,能拿到最新的Context值 testFn(); }; return <div onClick={handleClick}>CLICK HERE</div>; };
这样useTest在子组件中执行,自然能获取到Provider提供的最新Context值,testFn里打印的就是更新后的数组了。
方案三:让Provider内部能访问自己的Context(进阶)
如果确实需要在Provider内部使用自己的Context,可以把Provider的逻辑拆分为内部子组件,让内部组件被Provider包裹:
export const TestProvider = ({ children }) => { const [testState, setTestState] = useState([]); // 内部组件,被Provider包裹,能访问Context const InnerProvider = () => { const { testFn } = useTest(); const testHandlerHandler = () => { const test = 1; setTestState((current) => [...current, test]); testFn(); }; const contextValue = { test: testState, testContextFn: testHandlerHandler }; return ( <TestContext.Provider value={contextValue}>{children}</TestContext.Provider> ); }; // 先渲染一层Provider,让InnerProvider能访问到Context return ( <TestContext.Provider value={{ test: testState, testContextFn: () => {} }}> <InnerProvider /> </TestContext.Provider> ); };
不过这种写法比较绕,除非有特殊需求,否则不推荐。
总结
- React Context的Provider仅为它的子组件提供Context值,Provider自身无法直接获取自己提供的Context值,必须通过内部子组件间接获取。
- 依赖Context的自定义Hook,必须在Provider的子组件中使用,才能拿到最新的Context值。
- 异步状态更新时,直接调用依赖状态的函数会捕获旧值,要么传入最新值,要么在组件渲染时获取最新的Context引用。
内容的提问来源于stack exchange,提问作者Berg_Durden

