7

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

3 Answers 3

11

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.
Sign up to request clarification or add additional context in comments.

3 Comments

I agree that this is semantically correct; however, this is not what I would recommend be done. There is a huge difference between setting some state when quotes is initially loaded, and every single time quotes is set as is shown in the above. Simple example of this would be if quotes is cleared, for example.
But OP said in the comments of your answer that he wants to "use the quotes array to set another state to be displayed in the page". What would you suggest to accomplish that? I think that if quotes gets cleared, you can also handle that case inside the useEffect, right? What I've posted is my usual approach, but I would like to learn other ways of doing it. Thanks!
Well, if the quotes array is only being used here as an intermediary (for the sake of setting the other state, anyways), I think perhaps it's better to do all the state setting in a single useEffect as opposed to two. I'll update my answer below with what I think should be done in this instance. :)
1

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.

2 Comments

I want to use quotes array to set a another state that will be displayed in the page
@JibinThomas in that case, contrary to the accepted answer, I would suggest that you directly work with the state inside the first .then callback. Adding a second useEffect hook increases complexity without much added bonus.
1

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'))()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.