useReducer 和 useContext Dispatch 在 onClick 函数中不起作用

useReducer and useContext Dispatch doesn't work in onClick function

我会为您提供更广泛的背景,因为它非常简单。使用 React hooks,这里的第一个调度会自动触发并且工作正常,但第二个不会。 Context 是从另一个文件导入的,所以我认为这是一个(小写)上下文问题,但我不知道如何解决它?

const Component = props => {
  const [, dispatch] = useContext(Context);

  useEffect(() => {
    document.title = state.title;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  });

  // this works
  dispatch({ type: "UPDATE_TITLE", payload: "Not Click!" });

  function onAddClick() {
    // this doesn't work
    dispatch({ type: "UPDATE_TITLE", payload: "CLICKED!" });
  }

  return (
    <div>
      <AddButton onClick={onAddClick} />
    </div>
  );
};

这是父级。

const Reducer = (state, action) => {
  switch (action.type) {
    case "UPDATE_TITLE":
      state["title"] = action.payload;
      return state;
    default:
      return state;
  }
};

const initialState = {
  title: "My Title"
};

export const Context = createContext(initialState);

const App = () => {
  const [state, dispatch] = useReducer(Reducer, initialState);

  return (
    <Context.Provider value={[state, dispatch]}>
      <Component />
    </Context.Provider>
  );
};

export default App;

控制台日志在两种情况下都在正确的 reducer 情况下触发,但只有标记为 'this works' 的一个会正确更新状态,另一个会静默失败。


固定:https://codesandbox.io/s/cranky-wescoff-9epf9?file=/src/App.js

您似乎正试图直接改变状态。相反,请尝试 return 一个新对象,该对象是应用于旧状态的操作更改的结果。

const Reducer = (state, action) => {
  switch (action.type) {
    case "UPDATE_TITLE":
      return {
        ...state,
        page: {
          ...state.page,
          title: action.payload
        }
      };
    default:
      return state;
  }
};


或者,使用 produce from immerjs 让您能够以这种 mutable 风格编写 reducer。

import produce from "immer";

const Reducer = produce((state, action) => {
  switch (action.type) {
    case "UPDATE_TITLE":
      state["title"] = action.payload;
      break;
    default:
      return;
  }
});

我不知道你想通过将 dispatch 放在 controlled 环境之外来实现什么(比如 eventuseEffect):-

// this works
dispatch({ type: "UPDATE_TITLE", payload: "Not Click!" });
// but it will run in infinite loop tho (no changes can be made then)

所以 fixes 应该是:-

  1. Reducer 中,确保不要完全 mutate 你的 state:-
const Reducer = (state, action) => {
  switch (action.type) {
    case "UPDATE_TITLE":
      // Don't do this
      // let newState = state;
      // newState["page"]["title"] = action.payload;
      // console.log("updating", newState, newState.page.title);
      // return newState;

      // Do this instead
      return {
        ...state,
        page: {
          ...state.page,
          title: action.payload
        }
      };
    default:
      return state;
  }
};
  1. dispatch for not Click! 放在 eventfunction 中,或者在这种情况下更好,useEffect 因为你想应用它 once component 渲染。
const Demo = props => {
  const [state, dispatch] = useContext(Context);

  useEffect(() => {
    document.title = state.title;
    // eslint-disable-next-line react-hooks/exhaustive-deps

    // this works (should be here)
    dispatch({ type: "UPDATE_TITLE", payload: "Not Click!" });
  }, []); // run it once when render

  // this works but, (should not be here tho - cause it will run non-stop)
  // dispatch({ type: "UPDATE_TITLE", payload: "Not Click!" });

  function onAddClick() {
    // this will work now
    dispatch({ type: "UPDATE_TITLE", payload: "CLICKED!" });
  }

  return (
    <div>
      <button onClick={onAddClick}>Click Me!</button>
      <p>State now: {state.title}</p>
    </div>
  );
};

您可以尝试参考这个 sandbox 看看它是如何工作的。

已编辑和更新sandbox