Free Me
Free Me

Reputation: 195

Does useState hook change the value of the state

I just started React, and in this Item list tutorial I have some question about updating the states of the item. Also, I'm using functional component .So in app.js

const [items, setItems] = useState([
  {
    id: 1,
    title: 'Banana',
    bought: false
  },
 ...
])

Then I have a function in app.js to update the bought to true or false when I check a box

// The id is passed in from Item.js down below
const markBought = (id) => {
  setItems(
    items.map(
     item => {
      if (item.id === id) {
        /// If bought is false, checking it will make it true and vice versa
        item.bought = !item.bought; // (1)
      }
      return item; // (2)
    })
  );
};
return (
  <div className="App">
    <Items items={items} markBought={markBought}></Items>
  </div>
);

The teacher said we are using something called Component Drilling. So in Items.js, we map through every item to display them one by one, but I don't think it is neccessary to show.

Finally in Item.js

<input type="checkbox" onChange={() => props.markBought(props.item.id)} />
{props.item.title}

The application worked perfectly, but it's a little bit confusing for me. So:

  1. In app.js, after we change the bought status, shouldn't we also need to return item, the same way we return the item if the condition is false? Why only return the item when if is wrong, but when it is right we only change it without a return?
  2. I read that map will not modify the array, so markBought function should create a new items array, with the bought modified already, but what happens to this array, how do React know to "props" this to item.js, rather than the ones I hard coded?

Sorry if this is a little bit long, any help will be really appreciated. Thanks for reading

Upvotes: 2

Views: 2776

Answers (3)

HMR
HMR

Reputation: 39250

You are mutating an item in your map, if you optimized your Item component to be a pure component then that component won't re render because of the mutation. Try the following instead:

//use useCallback so marBought doesn't change and cause
//  needless DOM re renders
const markBought = useCallback(id => {
  setItems((
    items //pass callback to the setter from useState
  ) =>
    items.map(
      item =>
        item.id === id
          ? { ...item, bought: !item.bought } //copy item with changed value
          : item //not this item, just return the item
    )
  );
}, []);

Here is a full example:

const { useCallback, useState, useRef, memo } = React;
function Items() {
  const [items, setItems] = useState([
    {
      id: 1,
      title: 'Banana',
      bought: false,
    },
    {
      id: 2,
      title: 'Peach',
      bought: false,
    },
  ]);
  const toggleBought = useCallback(id => {
    setItems((
      items //pass callback to the setter from useState
    ) =>
      items.map(
        item =>
          item.id === id
            ? { ...item, bought: !item.bought } //copy item with changed value
            : item //not this item, just return the item
      )
    );
  }, []);
  return (
    <div>
      {items.map(item => (
        <Item
          key={item.id}
          item={item}
          toggleBought={toggleBought}
        />
      ))}
    </div>
  );
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
  const renderedRef = useRef(0);
  renderedRef.current++;
  return (
    <div>
      <div>{item.title}</div>
      <div>bought: {item.bought ? 'yes' : 'no'}</div>
      <button onClick={() => toggleBought(item.id)}>
        toggle bought
      </button>
      <div>Rendered: {renderedRef.current} times</div>
    </div>
  );
});

//render the application
ReactDOM.render(<Items />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Here is a broken example where you mutate the item and won't see the re render even though the state did change:

const { useCallback, useState, useRef, memo } = React;
function Items() {
  const [items, setItems] = useState([
    {
      id: 1,
      title: 'Banana',
      bought: false,
    },
    {
      id: 2,
      title: 'Peach',
      bought: false,
    },
  ]);
  const toggleBought = useCallback(id => {
    setItems((
      items //pass callback to the setter from useState
    ) =>
      items.map(
        item =>
          item.id === id
            ? ((item.bought = !item.bought),item) //mutate item
            : item //not this item, just return the item
      )
    );
  }, []);
  return (
    <div>
      <div>
        {items.map(item => (
          <Item
            key={item.id}
            item={item}
            toggleBought={toggleBought}
          />
        ))}
      </div>
      <div>{JSON.stringify(items)}</div>
    </div>
  );
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
  const renderedRef = useRef(0);
  renderedRef.current++;
  return (
    <div>
      <div>{item.title}</div>
      <div>bought: {item.bought ? 'yes' : 'no'}</div>
      <button onClick={() => toggleBought(item.id)}>
        toggle bought
      </button>
      <div>Rendered: {renderedRef.current} times</div>
    </div>
  );
});

//render the application
ReactDOM.render(<Items />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Upvotes: 2

Chris Heald
Chris Heald

Reputation: 62648

Your map function always returns an item. It just modifies the item first if the item you're modifying matches the id of the item currently being mapped. Map returns a new array of items (even if it doesn't change anything), which causes useState to see a new value. By default, in React, the check for updates isn't very clever - it's just checking if oldValue === newValue.

For primitives like strings, object equality tests return true for two separate objects, as long as their values match.

"foo" === "foo"  // => true

However, this isn't true for object or arrays. Two different arrays containing the same values will not compare as equal (because Javascript isn't comparing their contents, but rather their object IDs):

["foo"] === ["foo"]  // => false

So, when you map your items, you get a new array object (because recall: map collects the return values of the callback function into a new array), which will never match the previous value of items, so every call to setItems will cause React to say "hm, my previous items isn't the same object as my new items, I must re-render this component".

Upvotes: 0

Trisma
Trisma

Reputation: 765

Hi there and welcome to Stackoverflow.

  1. You are always returning the item. You just have an if statement that will change the bought state and item will get returned even if the condition above was false, which is the correct way of doing.

  2. Map will indeed not modify the array but return a new one. If you want to get that returning array you could simply do :

const myNewArray = items.map(...)

The way this new array is getting to your other component is because this new array is given to your useState(). You see setItems() ? This will set your state and Item.js will automatically be updated. That is what is so great about react. All components that are served from state will be updated once this state is updated.

Upvotes: 0

Related Questions