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