Khả Nam
Khả Nam

Reputation: 43

Component not re-render when dispatch action in Redux

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

Answers (3)

3limin4t0r
3limin4t0r

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

b3hr4d
b3hr4d

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

Steve Phan
Steve Phan

Reputation: 69

Just check reducer, u did not anything with changeStatus !

Upvotes: 0

Related Questions