Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React hooks state not using latest

I have the below code where I want to create a list of tags. In this example I'm fetching a list of tags in setAllTags() and then a number of available tags in setAvailableTags().

Then problem that I have is that when setAvailableTags() is run it will remove the tags that was fetched in setAllTags(). It seems like the state that I set in setAllTags() is not used when setAvailableTags() is settings it's state.

Any idea what I can do to fix this?

https://codesandbox.io/s/rj40lz6554

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

const Search = () => {
  const [tags, setTags] = useState({
    all: [],
    available: []
  });

  const setAllTags = () => {
    const all = ["tag 1", "tag 2", "tag 3"];
    const newValue = {
      ...tags,
      all
    };
    console.log("setting all tags to ", newValue);
    setTags(newValue);
  };

  const setAvailableTags = () => {
    const available = ["tag 1", "tag 2"];
    const newValue = {
      ...tags,
      available
    };
    console.log("setting available tags to", newValue);
    setTags(newValue);
  };

  useEffect(setAllTags, []);
  useEffect(setAvailableTags, []);

  return (
    <div>
      <div>
        <select placeholder="Tags">
          {tags.all.map((tag, i) => (
            <option key={tag + i} value={tag}>
              {tag}
            </option>
          ))}
        </select>
      </div>
    </div>
  );
};

const App = () => {
  return (
    <div>
      <h1>Hello React!</h1>
      <Search />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("app"));

The console is logged with

setting all tags to Object {all: Array[3], available: Array[0]}
setting available tags to Object {all: Array[0], available: Array[2]}
like image 704
nbon Avatar asked Mar 18 '19 12:03

nbon


1 Answers

In the react docs, you can see how useEffect works (https://reactjs.org/docs/hooks-effect.html)

Experienced JavaScript developers might notice that the function passed to useEffect is going to be different on every render. This is intentional. In fact, this is what lets us read the count value from inside the effect without worrying about it getting stale. Every time we re-render, we schedule a different effect, replacing the previous one. In a way, this makes the effects behave more like a part of the render result — each effect “belongs” to a particular render. We will see more clearly why this is useful later on this page.

What this means, is that each side effect within your render function will only have access to the initial result of useState. Remember that react states are immutable, so it doesn't make sense for you to update the state, and then try and use the updated version within the same render cycle.

You can see this by simply calling:

setTags({ all: ['test'] })
console.log(tags)

You should notice that tags does not change at all.

Personally I would use hook conventions here, and separate your useState out into two separate variables:

const [allTags, setAllTags] = useState([])
const [availableTags, setAvailableTags] = useState([])

This makes your effects more explicit (as they only need to update the one variable), and improves readability.

like image 190
Matt Way Avatar answered Oct 07 '22 22:10

Matt Way