Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using useEffect() Hook and Async/Await to Fetch Data from Firebase/Firestore

1. My Intention:

Using useEffect() hook to initialize component's state variable from the props, whose data is extracted from firebase using mapStateToProps().


2. Problem: unable to initialize component's state variable

  • The idea is that the useEffect will run only once when the component mounts. In this initialization window, I want to load data from firebase to this component's local state variable.

  • It works fine when running the example in this tutorial. In it, he fetches data with axios() in a async/await function. See below section 3 for his code.

  • But when I do it, trying to fetch data from the props, which is synced with or initialized by Firestore, I kept getting null, unable to retrieve data from the result returned from my async/await function.

  • I think it is because my lack of understanding of async/await. I wasn't able to create a valid async/await function to retrieve data from props.

Question:

I suspect that's where the problem lies. So, what's the correct way to write an async/await function which properly retrieve data from the props that was synced with firebase?

My Problem Code:

    import React, { useState, useEffect} from "react";
    import { connect } from "react-redux";
    import { firestoreConnect } from "react-redux-firebase";
    import { compose } from "redux";

    const app = props => {
      const { order } = props;  
      const [data, setData] = useState({});

      useEffect(() => {
        const fetchData = async () => {
          const result = await (() => {
            return new Promise(resolve => {
              if (order) { resolve(order); }
            });
          })();
          setData(result);
        };
        fetchData();
      }, []);
      /** if I leave useEffect's second argument empty, which is intended to run only once at the beginning, then i'm getting null  */

    ...

    const mapStateToProps = state => {
      const id = "DKA77MC2w3g0ACEy630g"; // docId for testing purpose.
      const orders = state.firestore.data.orders;
      const order = orders ? orders[id] : null;        
      return {
        order: order
      };
    };

    export default compose(
      connect(mapStateToProps),
      firestoreConnect([{collection: "orders"}])
    )(app);

What I've Tried:

  • If I add order into the second argument, then It will correctly load the data into component's state variable. But it will re-load the props' data into component's state variable and erase any changes in every re-render cycle.
 useEffect(() => {
        ...
      }, [order]);
  • Is mapStateToProps() happens after the initial component mount? Because that'll explain why in the initial effect, props is always empty.

3. Tutorial I was following: fetch data using hooks

I was able to successfully fetch data using techniques from this tutorial:

  const [data, setData] = useState({ hits: [] });
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        "https://hn.algolia.com/api/v1/search?query=redux"
      );
      setData(result.data);
    };
    fetchData();
  }, []);
like image 761
Victor Avatar asked Oct 03 '19 21:10

Victor


People also ask

Can we use async await with the useEffect hook?

Either way, we're now safe to use async functions inside useEffect hooks. Now if/when you want to return a cleanup function, it will get called and we also keep useEffect nice and clean and free from race conditions. Enjoy using async functions with React's useEffect from here on out!

How do I use fetch in useEffect?

Put the fetchData function above in the useEffect hook and call it, like so: useEffect(() => { const url = "https://api.adviceslip.com/advice"; const fetchData = async () => { try { const response = await fetch(url); const json = await response. json(); console. log(json); } catch (error) { console.

Can I use hook in useEffect?

Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That's what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

Can we call API in useEffect?

But you can use the API documentation. You can access it all in the README file. Call it in useEffect to get the questions. If you look at the documentation, you'll see that the route that matches the questions ( http://localhost:8000/survey ) is a GET route, which does not require a parameter.


1 Answers

It sounds like order may be an object that the parent creates every time. Hook dependencies check for exact matches -- previous === next -- so even if the values inside an object don't change, if the object itself changes, the effect will run again.

The most common way this happens is by passing normal object literals:

const order = { id: 'foo', SKU: 'bar', cost: 19.95, quantity: 1 };
return <App order={order} />;

This will repopulate order every time with a new object literal. A more insidious re-creation of a literal occurs when you spread one object inside of another:

const order = { ...oldOrder, quantity: 2 };

This still creates a new object.

Don't try to mutate existing objects. React will start doing really strange things to you. Once you understand how hook dependencies work, mutating objects will throw you under the bus.

You have three alternatives:

Use primitives as dependencies

If you can identify what should force a refresh from the server, pull that into its own variable, and set your dependency based on that. This only works well if you only need to send a limited number of fields to the server.

If, in your example, you only need the order's id to make your call, you can extract that into its own variable:

const orderId = order.id;

useEffect(() => {
  const fetchData = async () => {
    const response = await axios(`url-with-id?orderId=${orderId}`);
    setData(response.data);
  };
  fetchData();
}, [orderID]);

Note that if you try to access the whole order object from within the effect, even though you don't include it in the dependency array, React will complain, with good reason. Unless you can absolutely guarantee that it will never change unless orderId will change too, it will probably result in runtime bugs that will hit you later. Only use what's inside the dependency array within an effect.

Memoize your objects

If your order object is pretty simple, you can memoize it, based upon its contents. useMemo takes a dependency array, the same way useEffect does, but instead of running an effect whenever the dependency changes, useMemo returns a new object whenever a dependency changes.

As long as none of the dependencies change, useMemo returns the same object. And this is the key to why this works.

If order has a few simple values, such as id, SKU, price, and quantity, you could change your object literal creation to look like this:

const order = useMemo(
  () => ({
    id,
    SKU,
    price,
    quantity
  }),
  [id, SKU, price, quantity]
);

Now, as long as none of the numbers or strings in order change, you'll get the same object every render. Since it's the same object, useEffect(..., [order]) will only run once. If one of those values change, then a new order object will be provided, useEffect's dependency array gets changed, and the effect is run for the new order.

Check to see why you keep getting new order objects

In your case, it looks like you're getting your order object from a Redux store. Redux relies on its data only changing when it must. Every time the data changes in Redux, it triggers changes in every component that needs it. If the data changes unnecessarily, this could cause you to get objects that keep updating.

You'll want to look to see if there's another component that dispatches unnecessary actions to Redux, or if your reducer is accidentally creating all new objects when it isn't required. Is a parent making updates without checking if they're required? Do you have asynchronous actions (thunks or sagas) that are doing mass updates of your data store? Are you checking for deltas between your incoming and existing data before simply writing the new data on top of the old?

Redux state management is a whole topic of its own. It's probably easier to use Redux Toolkit than roll your own. It takes care of what needs to be recreated in a store without you having to manage immutability issues. If FireBase data is overwriting your data willy-nilly, Redux Toolkit may make it a lot easier to make sure only the data that changes between calls is updated, instead of changing everything all the time.

like image 132
Michael Landis Avatar answered Sep 17 '22 19:09

Michael Landis