2

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

Michael Heilemann
  • 2,627
  • 4
  • 23
  • 28
  • Can you give me more details? Did you try to put some logging inside `onAddClick` to make sure that it's running? – kunquan Nov 21 '20 at 01:21
  • Yeah, it's running alright. The dispatched case in the reducer switch gets called as well. But the state changes don't happen when it's called from the click, whereas they DO happen when it's called in the LayoutSystem initializer. I added some more code for context. – Michael Heilemann Nov 21 '20 at 01:33
  • `LayoutSystem` → `Component` (I updated the code example). – Michael Heilemann Nov 21 '20 at 01:43

2 Answers2

2

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

lala
  • 1,309
  • 9
  • 21
  • This is great, thank you. Will this work if the state has to go to another file though? That's my actual setup, that's why there's no expression of state in the file registering the click (I'm working on a hosted example). – Michael Heilemann Nov 23 '20 at 15:53
  • Here's the example code. https://stackblitz.com/edit/dispatch-test?file=src/App.js – Michael Heilemann Nov 23 '20 at 16:36
  • Follow the **Fixes no.1**. Since you got `nested` `obj` of `initialState`, then you should do like that. – lala Nov 23 '20 at 17:02
  • you can see the updated [sandbox](https://stackblitz.com/edit/dispatch-test-8raqxh?file=src%2FButton.js) to see it working – lala Nov 23 '20 at 17:04
0

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

ksav
  • 20,015
  • 6
  • 46
  • 66