YO Y
YO Y

Reputation: 131

How to wait for setState in useEffect until render?

  let [item, setItem] = useState({});
  let [comments, setComments] = useState([]);
  useEffect(async () => {
    await axios
      .all([
        axios.get(`https://dummyapi.io/data/v1/post/${id}`, {
          headers: { "app-id": process.env.REACT_APP_API_KEY }
        }),
        axios.get(`https://dummyapi.io/data/v1/post/${id}/comment`, {
          headers: { "app-id": process.env.REACT_APP_API_KEY }
        })
      ])
      .then(
        axios.spread((detail, comment) => {
          setItem({ ...detail.data })
          setComments([...comment.data.data])
        })
      )
      .catch((detail_err, comment_err) => {
        console.error(detail_err);
        console.error(comment_err);
      });
  }, []);

i setStated like above. and I was trying to use the State in return(), but it seems it didn't wait for the data set.

return (
          <div>
            {item.tags.map((tag, index) => {
              return <Chip label={tag} key={index} />
            })}
          </div>
)

because i got an error message like this : Uncaught TypeError: Cannot read properties of undefined (reading 'map'). Since i initialized 'item' just empty {object}, so it can't read 'item.tags', which is set by setState in useEffect.

How can i wait for the data set?

Upvotes: 1

Views: 3237

Answers (3)

Amila Senadheera
Amila Senadheera

Reputation: 13245

Initially, item is an empty JSON ({}). You should be using the optional chaining operator(?.) to easily get rid of the null or undefined exceptions.

return (
          <div>
            {item?.tags?.map((tag, index) => {
              return <Chip label={tag} key={index} />
            })}
          </div>
)

Upvotes: 2

bcjohn
bcjohn

Reputation: 2523

In generic, it would set a state isFetched to determine if the data from api is ready or not. And when the isFetched equal to true, it means the item.tags have value.

const [isFetched, setIsFetched] = useState(false);
useEffect(async () => {
  await axios.all(...).then(() => {
    ...
    ...
    setIsFetched(true);
  })
}, [])

// You could return null or an Loader component meaning the api is not ready
if (!isFetched) return null;
return (
      <div>
        {item.tags.map((tag, index) => {
          return <Chip label={tag} key={index} />
        })}
      </div>
)

On the other hand, you could use optional chaining to avoid using map from an undefined value (that is item.tags), the right way is replace item.tags.map to item.tags?.map.

Upvotes: 2

Nicholas Tower
Nicholas Tower

Reputation: 85012

let [item, setItem] = useState({});

Your initial state is an empty object, and there will always be at least one render that uses this initial state. Your code thus needs to be able to work correctly when it has this state. For example, you could check if item.tags exists before you try to use it:

if (item.tags) {
  return (
    <div>
      {item.tags.map((tag, index) => {
        return <Chip label={tag} key={index] />
      })}
    </div>
  );
} else {
  return <div>Loading...</div>
}

Alternatively, you could change your initial state so it has the same shape that it will have once loading has finished:

let [item, setItem] = useState({ tags: [] });

Upvotes: 2

Related Questions