Beware of these traps on state and props management using react-hooks
Tags: react, hooks
Published: 2020-02-08 02:22:19

## The setup

We have two components. A parent component (Main), where some tag list resides. And a child component (TagList) that receives a tag list, each tag is rendered as a removable item.

A first approach could be something like the following.

### Main Component

```jsx
const Main = () => {
  const tags = ['one', 'two', 'three']

  return <TagList tags={tags} />
}
```

### TagList Component

```jsx
const TagList = (props = {}) => {
  const [tags, setTags] = useState(props.tags ?? [])

  const handleDeleteTag = index => {
    tags.splice(index, 1)
    setTags(tags)
  }

  const handleReset = () => setTags(props.tags)

  return (
    <div>
      {props.tags.map((tag, i) => (
        <div key={i}>
          <span>{tag}</span>
          <input type="button" value="x" onClick={() => handleDeleteTag(i)} />
        </div>
      ))}
      <input type="button" value="Reset" onClick={handleReset} />
    </div>
  )
}
```

## Expectations

- When the user clicks on an 'x' marked button, the corresponding tag on that line is removed.

- When the user clicks on the reset button, after having made some changes to any item. The initial list should be displayed.

## Results

If we run that code, we will notice that no matter which button is pressed, nothing seems to be happening.

But behind the scenes, if we open the "Components" tab of Google Chrome devtools, (it may be needed to unselect and reselect components) we notice something pretty interesting.

The TagList component state data have been updated, but props data have been modified too on both Components.

### State changed but no update was triggered

Because the updated state object passed to the `setTags` is the variable provided by `useState(props.tags)`, the same reference is detected, thus re-render operation is not triggered.

### Components props data are modified

The useState and setTags methods passes its argument reference to the variable. In our case, it causes props.tags to mutate since it is passed as an argument on useState and setTags methods.

```jsx
const [tags, setTags] = useState(props.tags ?? [])
```

```jsx
const handleReset = () => setTags(props.tags)
```

## Fix the issues

### Reflect state updates

As we noticed, a state updated with a parameter that carries the same reference, won't cause a component to re-render. To fix the issue, we need to pass an argument with a different reference.

We'll make use of the ES6 spread operator to create a new array from updated tags.

```jsx
const handleDeleteTag = index => {
  tags.splice(index, 1)
  setTags([...tags])
}
```

### Prevent props to be changed

Since we know that `useState` and `setTags` mutates its passed parameter. We need to pass data in a way that doesn't lead props object to change.

```jsx
const tagsInitialState = [...(props?.tags ?? [])]
const [tags, setTags] = useState(tagsInitialState)
```

```jsx
const handleReset = () => setTags(tagsInitialState)
```

If you haven't been following on the latest ECMAScript specifications, that line may seem a bit tricky.

```jsx
const tagsInitialState = [...(props?.tags ?? [])]
```

That line can be converted to.

```jsx
const hasTags = props && props.tags && props.tags.length
const tagsInitialState = hasTags ? [...props.tags] : []
```

## Final code

Our final TagList component code now looks like this

```jsx
const TagList = (props = {}) => {
  const tagsInitialState = [...(props?.tags ?? [])]
  const [tags, setTags] = useState(tagsInitialState)

  const handleDeleteTag = index => {
    tags.splice(index, 1)
    setTags([...tags])
  }

  const handleReset = () => setTags(tagsInitialState)

  return (
    <div>
      {tags.map((t, i) => (
        <div key={i}>
          <span>{t}</span>
          <input type="button" value="x" onClick={() => handleDeleteTag(i)} />
        </div>
      ))}
      <input type="button" value="Reset" onClick={handleReset} />
    </div>
  )
}
```

I hope that helps!
Feel free to share thoughts in the comment section!