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

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有关?


问题原因及解决方案

原因分析

  1. 事件监听捕获旧函数引用:SVGEditor中的useEffect依赖为空数组,仅在组件挂载时执行一次,此时绑定的handleKeyDown函数捕获了当时的itemUp和right引用。当Context中状态更新后,itemUp会重新创建(因为它依赖editorState),但旧的handleKeyDown依然持有挂载时的itemUp引用,所以调用时获取的是旧状态。
  2. 错误的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 20:00:00