Jibin Thomas
Jibin Thomas

Reputation: 864

State returning empty array

I'm trying to access state from useState hook but it is giving me the initial state even after I have modified it.

const quotesURL = "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";


function QuoteGenerator() {
  const [quotes, setQuotes] = useState([]);
  const [currentQuote, setCurrentQuote] = useState({ quote: "", author: "" });

  useEffect(() => {
    axios(quotesURL)
      .then(result => {
        console.log(result);
        setQuotes(result.data);
      })
      .then(() => {

        console.log(quotes);
      });
    }, []);

console.log(quotes) is returning empty array instead of array of objects

Upvotes: 7

Views: 23338

Answers (3)

Daniel
Daniel

Reputation: 15413

Another way you could refactor your code to work:

const quotesURL = "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";


function QuoteGenerator = ({ quote }) => {
  const [quotes, setQuotes] = useState([]);
  const [currentQuote, setCurrentQuote] = useState({ quote: "", author: "" });

  const fetchQuote = async quote => {
    const result = await axios.get(quotesURL);
    setQuotes(result.data);
  };

    useEffect(() => {
      fetchQuote(quote);
    }, [quote]);
};

So now you have a function inside of your QuoteGenerator functional component called fetchQuote. The useEffect hook allows us to use something like lifecycle methods, kind of like combining the componentDidMount and componentDidUpdate lifecycle methods. In this case I called useEffect with a function to be ran everytime this component initially gets rendered to the screen and any time the component update as well.

You see in the other answers, that a second argument is passed as an empty array. I put quote as the first element inside of that empty array as it was passed as a prop in my example, but in others' example it was not, therefore they have an empty array.

If you want to understand why we use an empty array as the second argument, I think the best way to explain it is to quote the Hooks API:

If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.

If you pass an empty array ([]), the props and state as inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model...

In place of setState we call setQuotes and this is used to update the list of quotes and I passed in the new array of quotes which is result.data.

So I passed in fetchQuote and then passed it the prop that was provided to the component of quote.

That second argument of the empty array in useEffect is pretty powerful and not easy to explain and/or understand for everybody right away. For example, if you do something like this useEffect(() => {}) with no empty array as a second argument, that useEffect function will be making non-stop requests to the JSON server endpoint or whatever.

If you use useEffect(() => {}, []) with an empty array, it will only be invoked one time which is identical to using a componentDidMount in a class-based component.

In the example, I gave above, I am instituting a check to limit how often useEffect gets called, I passed in the value of the props.

The reason I did not put the async function inside of useEffect is because it's my understanding that we cannot use useEffect if we are passing an async function or a function that returns a Promise, at least according to the errors I have seen in the past.

With that said, there is a workaround to that limitation like so:

useEffect(
  () => {
    (async quote => {
       const result = await axios.get(quotesURL);
       setQuotes(result.data);
     })(quote);
  },
  [quote]
);

This is a more confusing syntax but it supposedly works because we are defining a function and immediately invoking it. Similar to something like this:

(() => console.log('howdy'))()

Upvotes: 1

Zerium
Zerium

Reputation: 17333

This is expected. Here's how your code works:

  1. quotes and setQuotes are returned from the useState function.
  2. useEffect runs for the first time once your component is mounted. quotes (empty array) and setQuotes are available within this function.
  3. When your axios request completes, you setQuotes. However, two things: 1 - this doesn't immediately update the value of the state. 2 - within the context of useEffect, quotes is still an empty array - when you do setQuotes(result.data) you're creating a new array, and that will not be directly accessible within this context.
  4. As such, console.log(quotes); will give an empty array.

Depends on what you're trying to use quotes for. Why not just directly work with result.data?

Update: I'm thinking of maybe something like this:

function QuoteGenerator() {
  const [quotes, setQuotes] = useState([]);
  const [currentQuote, setCurrentQuote] = useState({ quote: "", author: "" });

  useEffect(() => {
    axios(quotesURL).then(result => {
      console.log(result);
      setQuotes(result.data);
      setSomeOtherState(); // why not do it here?
    });
  }, []);
}

This way you maintain closer control of the data, without giving it over to lifecycle methods.

Upvotes: 1

cbdeveloper
cbdeveloper

Reputation: 31385

Here's how you should do it:

const quotesURL = "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";


function QuoteGenerator() {
  const [quotes, setQuotes] = useState([]);
  const [currentQuote, setCurrentQuote] = useState({ quote: "", author: "" });

  useEffect(() => {         // THIS WILL RUN ONLY AFTER YOUR 1ST RENDER
    axios(quotesURL)
      .then(result => {
        console.log(result);
        setQuotes(result.data);  // HERE YOU SET quotes AND IT WILL TRIGGER A NEW RENDER
      })
    }, []);                 // BECAUSE YOU'VE SET IT WITH '[]'

  useEffect(() => {         // THIS WILL RUN WHEN THERE'S A CHANGE IN 'quotes'
     if (quotes.length) {
       setSomeOtherState();   // YOU CAN USE IT TO SET SOME OTHER STATE
     }
  },[quotes]);

}

How this code works:

  • 1st render: You just the the initial states. useEffects are not run yet.
  • After 1st render: Both effects will run (in that order). The first one will fire the axios request. The second one will do nothing, because quotes has no length yet.
  • Axios request completes: the thenclause will run and setQuotes will be called to set the new quotes value. This will trigger a re-render.
  • 2nd render: Now the quotes state has beens updated with the new value.
  • After 2nd render: Only the second useEffect will run, because it's "listening" for changes in the quotes variable that just changes. Then you can use it to set some state like you said.

Upvotes: 11

Related Questions