React(Next)中Reducer状态已设置,但Context函数无法获取最新值
React(Next.js)中Context/Reducer状态更新后函数获取旧值问题
我在React(Next.js)的Context/Reducer使用中遇到问题:Reducer的状态已在useEffect中正确设置并打印,但按下按键调用Context内的函数时,获取的状态却是旧值。
相关代码
pages/svgTest.tsx
export default function SvgTest() { return ( <> <EditorContextProvider> <SVGEditor /> </EditorContextProvider> </> ) }
components/svgTest/SVGEditor.tsx
export default function SVGEditor() { const {brightness} = useLocalStorage() const {itemUp, right} = useEditorContext() useEffect(() => { function handleKeyDown(event: KeyboardEvent) { switch (event.key) { case 'ArrowUp': itemUp() break case 'ArrowRight': right() break } } document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('keydown', handleKeyDown) } }, []) return ( <div style={{ display: 'grid', gridTemplateColumns: '2fr 5fr', background: globals.brightness[brightness].background, height: 500, }}> </div> ) }
components/svgTest/context.tsx
const focuses = { tree: 'tree', form: 'form', input: 'input', } type EditorAction = { name: string, payload?: any } type EditorState = { focus: string } const initialTreeState: EditorState = { focus: focuses.tree, } export function reducer(state: EditorState, action: EditorAction): EditorState { const {name, payload} = action switch (name) { case 'focus': { return {...state, focus: payload} } } return state } export type EditorContext = { editorState: EditorState itemUp: () => void right: () => void } const EditorContext = createContext<EditorContext>({} as EditorContext) export function EditorContextProvider(props: { children: ReactNode }) { const [editorState, dispatch] = useReducer(reducer, initialTreeState) useEffect(() => { console.log("logsntr", "editorState.effect", editorState) }, [editorState]) function itemUp() { console.log("logsntr", "editorState.up", editorState) } function right() { dispatch({name: 'focus', payload: focuses.form}) } return ( <EditorContext.Provider value={{ editorState, itemUp, right }}> {props.children} </EditorContext.Provider> ) } export function useEditorContext() { return useContext(EditorContext) }
测试输出
页面刷新后:
tree tree
按下上箭头:
tree
按下右箭头:
form
再次按下上箭头:
tree
补充尝试
曾尝试用useCallback包裹right函数,但结果相同:
const right = useCallback(() => { dispatch({name: 'focus', payload: focuses.form}) }, [editorState])
此前使用useState时也遇到类似异常,曾用useRef解决,但认为Reducer不应存在此问题,怀疑是否与Next.js有关?
问题原因及解决方案
原因分析
- 事件监听捕获旧函数引用:
SVGEditor中的useEffect依赖为空数组,仅在组件挂载时执行一次,此时绑定的handleKeyDown函数捕获了当时的itemUp和right引用。当Context中状态更新后,itemUp会重新创建(因为它依赖editorState),但旧的handleKeyDown依然持有挂载时的itemUp引用,所以调用时获取的是旧状态。 - 错误的
useCallback依赖:之前尝试的useCallback给right添加了editorState依赖,这完全没必要——dispatch是useReducer返回的稳定引用,不需要依赖editorState。同时,SVGEditor的useEffect未将itemUp和right加入依赖,即使Context中的函数更新了,监听事件依然使用旧的函数。
解决方案
方案1:更新SVGEditor的useEffect依赖
将itemUp和right加入useEffect的依赖数组,确保函数更新时重新绑定事件监听:
useEffect(() => { function handleKeyDown(event: KeyboardEvent) { switch (event.key) { case 'ArrowUp': itemUp() break case 'ArrowRight': right() break } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [itemUp, right]) // 加入依赖
方案2:优化Context中的函数,用useCallback包裹并设置正确依赖
在EditorContextProvider中,用useCallback包裹itemUp和right,避免不必要的函数重建:
export function EditorContextProvider(props: { children: ReactNode }) { const [editorState, dispatch] = useReducer(reducer, initialTreeState) useEffect(() => { console.log("logsntr", "editorState.effect", editorState) }, [editorState]) // itemUp依赖editorState,所以加入依赖 const itemUp = useCallback(() => { console.log("logsntr", "editorState.up", editorState) }, [editorState]) // right仅依赖dispatch,而dispatch是稳定引用,所以依赖数组只放dispatch const right = useCallback(() => { dispatch({name: 'focus', payload: focuses.form}) }, [dispatch]) return ( <EditorContext.Provider value={{ editorState, itemUp, right }}> {props.children} </EditorContext.Provider> ) }
方案3:通过useRef保存最新状态(可选)
如果不想让itemUp因editorState更新而频繁重建,可以用useRef保存最新状态:
export function EditorContextProvider(props: { children: ReactNode }) { const [editorState, dispatch] = useReducer(reducer, initialTreeState) const stateRef = useRef(editorState) // 每次状态更新时同步ref useEffect(() => { stateRef.current = editorState console.log("logsntr", "editorState.effect", editorState) }, [editorState]) // itemUp不再依赖editorState,直接取ref的current值 const itemUp = useCallback(() => { console.log("logsntr", "editorState.up", stateRef.current) }, []) const right = useCallback(() => { dispatch({name: 'focus', payload: focuses.form}) }, [dispatch]) return ( <EditorContext.Provider value={{ editorState, itemUp, right }}> {props.children} </EditorContext.Provider> ) }
额外说明
这个问题和Next.js无关,是React闭包特性导致的常见问题——当函数在某个渲染周期被创建时,会捕获当时的状态值,后续状态更新后,如果没有重新获取函数的最新引用,就会拿到旧状态。
内容的提问来源于stack exchange,提问作者sntrcode
相关产品推荐
相关产品推荐

