Search code examples
javascriptreduxreact-reduxredux-toolkit

useDispatch is giving an error and I'm unable to add to my array


To try and understand Redux I've setup a simple todo project. Unfortunately I'm unable to get useDispatch to work.

I'm able to get all todo's I have stored with useSelect but I'm not able to add a todo because I think useDispatch is causing an error.

The error says: "e.preventDefault() is not a function" but when I remove useDispatch it doesn't display this error so I think the error is coming from useDispatch.

Here is the slice:

import { createSlice } from "@reduxjs/toolkit";

const slice = createSlice({
  name: "list",
  initialState: {
    value: [
      {
        id: 0,
        label: "Boodschappen",
      },
      {
        id: 1,
        label: "Scheren",
      },
      {
        id: 2,
        label: "Stoep vegen",
      },
    ],
  },
  reducers: {
    addTodo: (state, action) => {
      state.list = action.payload;
    },
  },
});

export const { addTodo } = slice.actions;

export default slice.reducer;

Here is the form component:

import { useDispatch, useSelector } from "react-redux";
import { addTodo } from "../../slices/todoSlice";

function Form() {
  const list = useSelector((state) => state.list.value);
  const [todo, setTodo] = useState("");

  const dispatch = useDispatch();

  const addTodo = (e, addTodo) => {
    e.preventDefault();
    dispatch(addTodo([...list, { id: list.length + 1, label: todo }]));

    setTodo("");
  };

  return (
    <div>
      <FormWrapper>
        <form action="" onSubmit={(e) => addTodo(e, addTodo)}>
          <input
            type="text"
            name="todo"
            id="todo"
            value={todo}
            onChange={(e) => setTodo(e.target.value)}
          />
          <input type="submit" value="Add todo" />
        </form>
      </FormWrapper>
    </div>
  );
}

Here is the store I have setup. I did wrap it around my app component:

import { configureStore } from "@reduxjs/toolkit";
import listReducer from "../slices/todoSlice";

// config the store
const store = configureStore({
  reducer: {
    list: listReducer,
  },
});

// export default the store
export default store;

Solution

  • The code isn't dispatching the addTodo you think it is. Instead of the imported addTodo action being passed to the local addTodo submit handler function, it's the local addTodo submit handler that is passed to itself and called in dispatch and it's subsequent call that is passed an argument that isn't an event object to call preventDefault on.

    ...
    import { addTodo } from "./list.slice"; // <-- never referenced
    
    function Form() {
      const list = useSelector((state) => state.list.value);
      const [todo, setTodo] = useState("");
    
      const dispatch = useDispatch();
    
      const addTodo = (e, addTodo) => { // <-- (1) this addTodo is in scope
        e.preventDefault();
        dispatch(addTodo( // <-- (3) calls local addTodo without event object!
          [...list, { id: list.length + 1, label: todo }]
        ));
    
        setTodo("");
      };
    
      return (
        <div>
          <FormWrapper>
            <form action="" onSubmit={(e) => addTodo(e, addTodo)}> // <-- (2) addTodo passed to itself!
              <input
                type="text"
                name="todo"
                id="todo"
                value={todo}
                onChange={(e) => setTodo(e.target.value)}
              />
              <input type="submit" value="Add todo" />
            </form>
          </FormWrapper>
        </div>
      );
    }
    

    The fix is trivial, rename the submit handler to anything but the same identifier as the action you want to dispatch. You also don't need to pass the action through the submit handler callback's arguments since the addTodo action already has file scope.

    ...
    import { addTodo } from "./list.slice";
    
    function Form() {
      const list = useSelector((state) => state.list.value);
      const [todo, setTodo] = useState("");
    
      const dispatch = useDispatch();
    
      const submitHandler = (e) => {
        e.preventDefault();
        dispatch(addTodo( // <-- imported addTodo action :)
          [...list, { id: list.length + 1, label: todo }]
        ));
    
        setTodo("");
      };
    
      return (
        <div>
          <FormWrapper>
            <form action="" onSubmit={submitHandler}>
              <input
                type="text"
                name="todo"
                id="todo"
                value={todo}
                onChange={(e) => setTodo(e.target.value)}
              />
              <input type="submit" value="Add todo" />
            </form>
          </FormWrapper>
        </div>
      );
    }
    

    There is also a typo in the list slice addTodo reducer. The list state is an object with a value property, so when updating the list's value you need to update state.value instead of state.list.

    const slice = createSlice({
      name: "list",
      initialState: {
        value: [ // <-- state.value
          ...
        ]
      },
      reducers: {
        addTodo: (state, action) => {
          state.value = action.payload; // <-- change
        }
      }
    });
    

    That said, it's uncommon to update the state the way you have. Instead of computing the next state value in the UI and passing the entire new value to the addTodo action, you should really dispatch only what you want to add and let the addTodo reducer function compute the next state. In other words, instead of duplicating the "addTodo" logic anywhere in the UI that wants to add a todo (which is also prone to error), you write the logic once in the reducer.

    Example:

    import { createSlice, nanoid } from "@reduxjs/toolkit";
    
    const slice = createSlice({
      name: "list",
      initialState: {
        value: [
          ...
        ]
      },
      reducers: {
        addTodo: {
          reducer: (state, action) => {
            state.value.push(action.payload); // <-- push todo into list
          },
          prepare: (label) => ({
            payload: { id: nanoid(), label }, // <-- create payload
          }),
        },
      }
    });
    

    Form

    function Form() {
      const [todo, setTodo] = useState("");
    
      const dispatch = useDispatch();
    
      const submitHandler = (e) => {
        e.preventDefault();
        dispatch(addTodo(todo)); // <-- dispatch just todo value
        setTodo("");
      };
    
      return (
        <div>
          <FormWrapper>
            <form action="" onSubmit={submitHandler}>
              <input
                type="text"
                name="todo"
                id="todo"
                value={todo}
                onChange={(e) => setTodo(e.target.value)}
              />
              <input type="submit" value="Add todo" />
            </form>
          </FormWrapper>
        </div>
      );
    }
    

    Demo

    Edit usedispatch-is-giving-an-error-and-im-unable-to-add-to-my-array