3

I've created a React component with an array state state.colors that contains three hex values (initial state is ['#aaaaaa', '#aaaaaa', '#aaaaaa']). Each value in the state.colors array can be updated using color pickers. As a test, I've updated the first color to red, second to green, and third to blue. This works as expected in a class component (as shown in the screenshot below).


class EditArtworkClass extends React.Component {
  constructor(props) {
    this.state = {
      colors: initialPalette
    };
  }

  setColor(index, hex) {
    const clonedColors = _.clone(this.state.colors);
    clonedColors[index] = hex;

    this.setState(
      {
        colors: clonedColors
      },
      () => {
        console.log("updated");
      }
    );
  }

  // other code here
}

Class Component - Before choosing colors:

Class component before updates

Class Component - After choosing colors (state.colors have the correct hex values):

Class component after updates

However, I am facing an issue when using a functional component. Every time I update a color using the color picker, all other values in the colors state array are reset to the initial value (#aaaaaa). If I set the first color to red, second to blue, and third to green, only the last value in the colors array will have the correct hex since the other two values are reset to #aaaaaa when updating the third color.

export default function EditArtworkFunctional() {
  const [colors, setColors] = useState(initialPalette);

  const setColor = (index, hex) => {
    const clonedColors = _.clone(colors);
    clonedColors[index] = hex;

    return clonedColors;
  };

  // Other code here
}

Functional Component - Before choosing colors:

Functional component before updates

Functional Component - After choosing colors (only the last color I pick has a correct hex in the state):

Functional component after updates

Note that the color picker is an uncontrolled component and will display the colors you pick, not the color in the colors state array.

I have created a reproducible mini-app in the Codesandbox Link below.

App.js

Condesandbox Link: https://codesandbox.io/s/class-vs-functional-component-array-state-8lnzd

I have zero ideas as to why this is happening, so any help or guidance would be greatly appreciated .

UPDATE: I've fixed the problem using @TalOrlanczyk's answer. The trick was to retrieve the previous state using the optional parameter when updating. You can view the CodeSandbox repo of the working version here.

subwaymatch
  • 1,020
  • 10
  • 11
  • 1
    Add your code to the question (only the relevant parts), or it will get useless when the link dies. **Tip:** You can add JS code snippets directly here in StackOverflow. – emi Nov 29 '20 at 13:04
  • @emi I was concerned that the question would get too lengthy . But you're right - I will edit the question to include relevant code in case the codesandbox link dies. – subwaymatch Nov 29 '20 at 13:33

2 Answers2

4

This is because of two reasons

  1. const clonedColors = _.clone(colors) using this deep clones the array which is not what we want, we have to make a shallow copy of the array . So, const clonedColors = colors will be the right way.
  2. setColors(clonedColors) does not work correctly, but if we setState using spread syntax setColors([...clonedColors]) it will. This is because the array is just a reference, and React setState won't rerender if the state doesn't get a new reference. That's why we send a new array reference by using the spread syntax [...clonedColors]. Thanks to @TalOrlanczyk for the reason.
const setColor = (index, color) => {
    const clonedColors = colors;
    clonedColors[index] = color;
    setColors([...clonedColors]);
    console.log(colors);
};

Here is the Updated Codesandbox Link

theWellHopeErr
  • 1,856
  • 7
  • 22
  • 2
    it's happend because array is reference and setState will not rerender if the state don't get a new reference. so if we do a spread operator we get a new reference. – TalOrlanczyk Nov 29 '20 at 13:10
  • 1
    you know this is an anti pattern because you mutate the current state and this is anti pattern as far as I know. – TalOrlanczyk Nov 29 '20 at 13:21
  • Thanks both @theWellHopeErr and @TalOrlanczyk! Based on my understanding, I thought I always had to make a clone of the original state objects to treat them as immutable objects. I'm still a bit confused how using the spread operator is different from creating a clone with `_.clone()`. But I'll take time to read the docs and test them out. Again, thank you both! – subwaymatch Nov 29 '20 at 13:29
  • 1
    _.clone() and spread operator are pretty much the same lodash lodash solution for clone deep is ._cloneDeep() – TalOrlanczyk Nov 29 '20 at 14:00
4

I think I solve the problem

const setColor = (index, hex) => {
    setColors((prev) => {
      console.log(prev);
      const clonedColors = [...prev];
      clonedColors[index] = hex;
      return clonedColors;
    });
  };

like @theWellHopeErr says it cause because it happend because you didn;t send it as a spread operator it's happend because array is reference if you change the same reference it's maybe change the value but the use state don't catch it because it's not a new reference.

Another thing you should know that is much better to use the like this (with the prev) because like that you can be sure you get the real last input you insert into this state

UPDATE

The spread operator is good only for shallow only when it's deep one like objects with a property with object this will not work and change the original state also.

don't mutates the current state, this is an anti-pattern in React this is a thread that explain it perfectly

Clone array to do deep clone you should use

a good artical in medium that explain about shallow and deep clone

TalOrlanczyk
  • 1,205
  • 7
  • 21
  • This seems to be the perfect solution I've been looking for. I wasn't even aware there was a way to retrieve the previous state in the lambda function when using `useState()`. Using your suggested fix worked like a charm. Thank you @TalOrlanczyk! – subwaymatch Nov 29 '20 at 13:35
  • thanks @subwaymatch look at the cloone array article I think it will help you alot – TalOrlanczyk Nov 29 '20 at 13:55