I am learning React Reducer now. I want to build a toggle button that changes a boolean completed
value to its opposite each time I click the button.
What I have is an array of states
, each state
is an object with an id
and a completed
value set to be true
or false
. Then I loop through states
, setting each state
as an Item
component and display it on screen.
// App.js file import React, { useReducer } from "react"; import { AppReducer } from "./AppReducer"; import Item from "./Item"; function App() { const initialStates = [ { id: 1, completed: false, }, { id: 2, completed: false, }, ]; const [states, dispatch] = useReducer(AppReducer, initialStates); return ( <div> {states.map((state) => ( <Item item={state} key={state.id} dispatch={dispatch} /> ))} </div> ); } export default App;
In the Item
component, I display whether this item is completed or not (true
or false
). I set up a toggle
function on the button to change the completed
state of the Item
.
// Item.js import React from "react"; const Item = ({ item, dispatch }) => { function setButtonText(isCompleted) { return isCompleted ? "True" : "False"; } let text = setButtonText(item.completed); function toggle(id){ dispatch({ type: 'toggle', payload: id }) text = setButtonText(item.completed); } return ( <div> <button type="button" onClick={() => toggle(item.id)}>Toggle</button> <span>{text}</span> </div> ); }; export default Item;
Here is my reducer function. Basically what I am doing is just loop through the states
array and locate the state
by id
, then set the completed
value to its opposite one.
// AppReducer.js export const AppReducer = (states, action) => { switch (action.type) { case "toggle": { const newStates = states; for (const state of newStates) { if (state.id === action.payload) { const next = !state.completed; state.completed = next; break; } } return [...newStates]; } default: return states; } };
So my problem is that the toggle button only works once. I checked my AppReducer
function, it did change completed
to its opposite value, however, every time we return [...newStates]
, it turned back to its previous value. I am not sure why is that. I appreciate it if you can give it a look and help me.
The code is available here.
Advertisement
Answer
Here is the working version forked from your codesandbox https://codesandbox.io/s/toggle-button-forked-jy6jd?file=/src/Item.js
The store value updated successfully. The problem is the way of listening the new item change. dispatch is a async event, there is no guarantee the updated item will be available right after dispatch()
So the 1st thing to do is to monitor item.completed
change:
useEffect(() => { setText(setButtonText(item.completed)); }, [item.completed]);
The 2nd thing is text = setButtonText(item.completed);
, it will not trigger re-render. Therefore, convert the text
to state and set it when item.completed
to allow latest value to be displayed on screen
const [text, setText] = useState(setButtonText(item.completed));