React useReducer not updating object in array

Viewed 82

Todos Component:

import React, { useReducer, useState } from 'react';
import Todo2 from './Todo2';

export const ACTIONS = {
    ADD_TODO : 'add-todo',
    TOGGLE_TODO : 'toggle-todo'
};

function reducer(state, action) {
    switch(action.type) {
        case ACTIONS.ADD_TODO:
            return [...state, {id: Date.now(), name: action.payload.name, complete: false}];
        case ACTIONS.TOGGLE_TODO:
            const patch = [...state];

            console.log('The index is:', action.payload.index);
            console.log('The current state is:', patch[action.payload.index].complete);

            // update state
            patch[action.payload.index].complete = !patch[action.payload.index].complete;

            console.log('The updated state is:', patch[action.payload.index].complete);
            console.log('The patch is:', patch);
            return patch;
    }
}
export default function Todos() {
    const [todos, dispatch] = useReducer(reducer, []);
    const [name, setName] = useState('');

    function handleSubmit(e) {
        e.preventDefault();

        dispatch({type: ACTIONS.ADD_TODO, payload: {name: name}});
        setName('');
    }
    return (
        <div>
            <form onSubmit={handleSubmit}>
                <input type="text" value={name} onChange={e => setName(e.target.value)}/>
            </form>
            {todos.map((todo, i) => <Todo2 key={todo.id} todo={todo} index={i} dispatch={dispatch} />)}
        </div>
    );
}

Todo Component:

import React from 'react';
import { ACTIONS } from './Todos2';

export default function Todo2({ todo, dispatch, index }) {
    return (
        <div>
            <span style={{color: todo.complete ? 'green':'red' }}>{todo.name}</span>
            <button onClick={e => dispatch({type: ACTIONS.TOGGLE_TODO, payload : {index: index}})}>Toggle</button>
        </div>
    )
}

I am trying to update an object inside an array, setting its "complete" property to either true or false depending on its current value. I console.logged the results but in the end the patch never gets it's updated data, it always retains it's original value.enter image description here

If I update the state like this, it works and I don't know why this works but the index way of updating does not.

// update state
patch[action.payload.index].complete = true;
1 Answers

I created a codesandbox example and reproduced the same issue, then I changed const patch = [...state] to:

import _ from 'lodash'
...
...
const patch = _.cloneDeep(state)

And, everything else staying the same, it worked like a charm. Here is the code Now I know that, spread operator ..., does create a shallow copy rather than a deep copy. Therefore, I think your !patch[action.payload.index].complete is updated in the same line, creating a paradoxical assignment (like a double update). Couldn't find a technical reference to explain this better, but the issue is for sure not deep copying the object.

Suggestion

case ACTIONS.TOGGLE_TODO:
   return state.map((el, idx) => idx === action.payload.index ? {...el, complete: !el.complete} : el)

Related