Reputation: 43
I am learning about Redux and ran into the problem below. My component doesn't re-render when dispatch action changeStatus
, but works when dispatch actionRemove
. Let me know why? Thanks.
code here
reducer
const HandleTaskReducer = (state = initializeState, action) => {
switch (action.type) {
case addTask: {
let list = state.tasks;
list.push(action.payload);
localStorage.setItem('todos', JSON.stringify(list));
return {
...state,
tasks: list
};
}
case completeTask: {
let list = state.tasks;
let index = list.findIndex((value => value.id === action.payload));
if (index > -1) {
list[index].status = true;
localStorage.setItem('todos', JSON.stringify(list));
}
return {
...state,
tasks: list
};
}
case removeTask: {
let list = state.tasks;
let index = list.findIndex((value => value.id === action.payload));
if (index > -1) {
list.splice(index, 1);
localStorage.setItem('todos', JSON.stringify(list));
}
return {
...state,
tasks: list
};
}
default: {
return {...state};
}
}
}
this is code in component
const TaskItemComponent = props => {
const className = !props.task.status ? 'item-task bg-grey' : 'item-task bg-grey success';
const removeTask = () => {
props.remove(props.task.id);
}
const changeStatus = () => {
props.completeTask(props.task.id);
}
return (
<div className={className}>
<div className="content-task">
<h3>{props.task.name}</h3>
<p>{props.task.description}</p>
</div>
<div className="action-task">
{!props.task.status && <button onClick={changeStatus} className="btn">Complete</button>}
<button className="btn" onClick={removeTask}>Delete</button>
</div>
</div>
)
}
const mapDispatchToProps = dispatch => {
return {
remove: id => {
dispatch(actionRemove(id))
},
completeTask: id => {
dispatch(actionComplete(id));
console.log(id)
}
}
}
export default connect(null, mapDispatchToProps)(TaskItemComponent);
this is ListTaskComponent
import React, {useState, useEffect} from 'react';
import TaskItemComponent from "./TaskItemComponent";
import {connect} from "react-redux";
const ListTaskComponent = props => {
const [tasks, setTasks] = useState([]);
useEffect(() => {
setTasks(props.taskState.tasks);
}, []);
const list = tasks.map(value =>
<TaskItemComponent
key={value.id.toString()} task={value}/>
);
return (
<section id="wrap-task" className="mt-3">
{list}
</section>
)
}
const mapStateToProps = state => {
return {
taskState: state.taskReducer,
}
}
export default connect(mapStateToProps, null)(ListTaskComponent)
Upvotes: 0
Views: 1978
Reputation: 21130
The problem is most likely that you're violate the "single source of truth" principal.
You do this with the following lines of code:
const ListTaskComponent = props => { const [tasks, setTasks] = useState([]); useEffect(() => { setTasks(props.taskState.tasks); }, []);
So, what is the plan here? You receive a list of taskState.tasks
from the Redux store. Which is is copied into the components own state. What happens if taskState.tasks
changes? The ListTaskComponent
component will re-render, but since tasks
is only set on component mount the value will never update.
The problem is easily fixed by removing the local state.
const ListTaskComponent = props => {
const tasks = props.taskState.tasks;
The above should fix the "single source of truth" principal. However if the first thing we do is map the state to a value you might as well move this change into mapStateToProps
to simplify the actual component.
const ListTaskComponent = ({ tasks }) => {
const list = tasks.map(value =>
<TaskItemComponent key={value.id.toString()} task={value} />
);
return (
<section id="wrap-task" className="mt-3">{list}</section>
)
}
const mapStateToProps = state => ({ tasks: state.taskReducer.tasks });
export default connect(mapStateToProps, null)(ListTaskComponent);
Another issue I can see has to do with the reducer itself. Reducers should not mutate the previous state, yet in your code I can spot mutations to the original state:
let list = state.tasks; list.push(action.payload);
let list = state.tasks; let index = list.findIndex((value => value.id === action.payload)); if (index > -1) { list[index].status = true;
let list = state.tasks; let index = list.findIndex((value => value.id === action.payload)); if (index > -1) { list.splice(index, 1);
I suggest reading Writing Reducers which explains the rules that reducers should follow and also gives some examples.
Coming back to your code you could write the above code blocks in the following manner:
let list = [...state.tasks];
list.push(action.payload);
// or
let list = [...state.tasks, action.payload];
let list = [...state.tasks];
let index = list.findIndex((value => value.id === action.payload));
if (index > -1) {
list[index] = { ...list[index], status: true };
// or
let list = state.tasks.map(value => {
if (value.id !== action.payload) return value;
return { ...value, status: true };
});
let list = [...state.tasks];
let index = list.findIndex((value => value.id === action.payload));
if (index > -1) {
list.splice(index, 1);
// or
let list = state.tasks.filter(value => value.id !== action.payload);
Upvotes: 1
Reputation: 4578
Try change your ListTaskComponent
to below should solve the issue, not need useState
just use them directly.
const ListTaskComponent = props => (
<section id="wrap-task" className="mt-3">
{props.taskState.tasks.map(value =>
<TaskItemComponent key={value.id} task={value}/>
)}
</section>
)
Upvotes: 0