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