useReducer and useContext Dispatch doesn't work in onClick function

5.4k Views Asked by At

I'll spare you the broader context as it's pretty simple. Using React hooks, the first dispatch here fires automatically and works just fine, but the second one doesn't. Context is imported from another file, so I assume it's a (lowercase) context issue, but I don't know how to fix it?

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>
  );
};

Here's the parent.

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;

Console logs fire in the correct reducer case in both cases, but only the one marked 'this works' will actually update the state properly, the other one fails silently.


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

2

There are 2 best solutions below

3
On BEST ANSWER

It looks like you are attempting to mutate state directly. Instead try to return a new object that is the result of the changes from the action applied to the old state.

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

Edit mystifying-hofstadter-xqwy6


Alternatively, use produce from immerjs to give you the ability to write your reducer in this mutable style.

import produce from "immer";

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

Edit reducer with immerjs produce

4
On

I don't know what you trying to achieve by placing the dispatch outside controlled env (like an event or useEffect):-

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

So the fixes should be:-

  1. in Reducer, make sure not to completely mutate your 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. Place your dispatch for not Click! inside an event or function or better yet in this case, useEffect since you wanna apply it once the component rendered.
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>
  );
};

You can try and refer to this sandbox and see how it works.

EDITED & UPDATED sandbox