2

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;
Robert
  • 10,126
  • 19
  • 78
  • 130

1 Answers1

3

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)

Sinan Yaman
  • 5,714
  • 2
  • 15
  • 35
  • Thanks! I feel like you almost have to replace the whole state. I tried something rather simple: ```const previous_val_reversed = !patch[action.payload.index].complete; patch[action.payload.index].complete = previous_val_reversed;``` Which I thought would be the same as ```patch[action.payload.index].complete = true``` but i guess not, this still doesn't work. The direct assignment of the bool x = true never fails, which to me is the confusing paty. – Robert Oct 19 '21 at 13:38
  • Yeah, you have to replace the state with a whole new reference. Assigning like `patch[action.payload.index].complete = true` does work, but mutates the state, since `patch` is not a deep copy. – Sinan Yaman Oct 19 '21 at 13:40
  • This worked ```patch.splice(action.payload.index, 1, {...patch[action.payload.index], ...{complete: previous_val_reversed}});``` – Robert Oct 19 '21 at 13:46
  • Okay but `state.splice(action.payload.index, 1, {...state[action.payload.index],...{ complete: previous_val_reversed } });` will also work. So I believe `patch.splice()` is also mutating the state which can produce side effects. – Sinan Yaman Oct 19 '21 at 13:50
  • Gotcha, gotcha, so in essence the splice way, since its not a complete copy is like mutating state directly. I think im getting the idea. – Robert Oct 19 '21 at 13:51
  • Yeah, exactly. That is one thing I don't really love about react, avoiding state mutation is not trivial. You have to keep in mind that you need a deep copy, or a total replacement. The way I suggested with `.map` is really common in my experience. – Sinan Yaman Oct 19 '21 at 13:53