lee
lee

Reputation: 195

Problem on handling array of object as state

I'm trying to set state as array of objects, but it fails.

I created project using CRA, and using react-hooks for states. I get data from graphql server using react-apollo-hooks. I just declared data object in codesandbox, but it doesn't affect my problem.

For every click, set state(array of object) with data(array of object).

const data = {
  lists: [
    {
      id: "1"
    },
    {
      id: "2"
     },
    {
      id: "3"
    }
   ]
};

const Sample = () => {
  const [sample, setSample] = useState([]);

  const Updator = async () => {
    try {
      await data.lists.map(list => {
        setSample([
          ...sample,
          {
            label: list.id,
            value: list.id
          }
        ]);
        return true;
      });
      console.log(sample);
    } catch (err) {
      throw err;
    }
  };
  return (
    <div>
      <React.Fragment>
        <button
          onClick={e => {
            Updator();
          }}
        >
          Click me
        </button>
        <p>
          <strong>
            {sample.map(single => {
              return <div>{single.label}</div>;
            })}
          </strong>
        </p>
      </React.Fragment>
    </div>
  );
};

I attached all test code on below.

Here is codesandbox link. https://codesandbox.io/s/zr50rv7qjp

I expect result of

123

by click, but result is

3

Also, for additional click, expected result is

123
123

And I get

3
3
.

When I use setSample(), I expect function something like Array.push(). But it ignores all the previous data expect the last one.

Any helps will be thankful!

Upvotes: 2

Views: 131

Answers (2)

Shubham Khatri
Shubham Khatri

Reputation: 282000

state updater does batching and since you are calling the setSample method in map, only your last value is being written in state.

The best solution here is to return data from map and then update the state once like below.

import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const data = {
  lists: [
    {
      id: "1"
    },
    {
      id: "2"
    },
    {
      id: "3"
    }
  ]
};

const Sample = () => {
  const [sample, setSample] = useState([]);

  const Updator = async () => {
    try {
      const newData = data.lists.map(list => {
        return {
          label: list.id,
          value: list.id
        };
      });
      setSample([...sample, ...newData]);
    } catch (err) {
      throw err;
    }
  };
  return (
    <div>
      <React.Fragment>
        <button
          onClick={e => {
            Updator();
          }}
        >
          Click me
        </button>
        <p>
          <strong>
            {sample.map((single, index) => {
              return <div key={index}>{single.label}</div>;
            })}
          </strong>
        </p>
      </React.Fragment>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<Sample />, rootElement);

Working Demo

Another solution is to use the callback method to update state but you must avoid calling state updates multiple times.

Upvotes: 2

ApplePearPerson
ApplePearPerson

Reputation: 4439

You're destructing sample which will not have the latest version of itself when you're looping and calling setSample. This is why it only puts 3 in the list of samples, because the last iteration of the map will destruct an empty sample list and add 3.

To make sure you have the newest value of sample you should pass a function to setSample. This function will get the latest version of your state var from react.

setSample((latest) => {
    return [
      ...latest,
      {
        label: list.id,
        value: list.id
      }
    ]
});

Upvotes: 1

Related Questions