Home > OS >  How to prevent set state before useEffect?
How to prevent set state before useEffect?

Time:04-17

I have a React/Next component. This component download data from firebase storage based on a route. For example, for route http://localhost:3000/training/javascript the component with get data from /training/javascript router in firebase storage.

// ReactJS
import { useState, useEffect } from "react";

// NextJS
import { useRouter } from "next/router";

// Seo
import Seo from "../../../components/Seo";

// Hooks
import { withProtected } from "../../../hook/route";

// Components
import DashboardLayout from "../../../layouts/Dashboard";

// Firebase
import { getDownloadURL, getMetadata, listAll, ref } from "firebase/storage";
import { storage } from "../../../config/firebase";

// Utils
import prettysize from "prettysize";
import capitalize from "../../../utils/capitalize";
import { PlayIcon } from "@heroicons/react/outline";
import { async } from "@firebase/util";

function Video() {
  // States
  const [videos, setVideos] = useState([]);

  // Routing
  const router = useRouter();
  const { id } = router.query;

  // Reference
  const reference = ref(storage, `training/${id}`);

  useEffect(() => {
    const fetchData = async () => {
      let tempVideos = [];
      let completeVideos = [];
      const videos = await listAll(reference);

      videos.items.forEach((video) => {
        tempVideos.push(video);
      });

      tempVideos.forEach((video) => {
        getMetadata(ref(storage, video.fullPath)).then((metadata) => {
          completeVideos.push({
            name: metadata.name,
            size: prettysize(metadata.size),
          });
        });
      });

      tempVideos.forEach((video) => {
        getDownloadURL(ref(storage, video.fullPath)).then((url) => {
          completeVideos.forEach((completeVideo) => {
            if (completeVideo.name === video.name) {
              completeVideo.url = url;
            }
          });
        });
      });

      setVideos(completeVideos);
    };

    fetchData();
  }, [id]);

  console.log("Render", videos)
  return (
    <>
      <Seo
        title={`${capitalize(id)} Training - Dashboard`}
        description={`${capitalize(
          id
        )} training for all Every Benefits Agents.`}
      />
      <DashboardLayout>
        <h2>{capitalize(reference.name)}</h2>
        <ul>
          {videos.map((video) => {
            return (
              <li key={video.name}>
                <a href={video.url} target="_blank" rel="noopener noreferrer">
                  <PlayIcon />
                  {video.name}
                </a>
                <span>{video.size}</span>
              </li>
            );
          })}
        </ul>
      </DashboardLayout>
    </>
  );
}

export default withProtected(Video);

I have an useState that should be the array of videos from firebase. I use an useEffect to get the data from firebase and extract the needed information. Some medatada and the url.

Everything's fine. The information is extracted, and is updated to the state correctly. But when the state is updated, it's no showings on the screen.

This is a console.log of the videos state updated, so you can see it's correctly updated. enter image description here

CodePudding user response:

You messed up a bit with asynchronous code and loops, this should work for you:

 useEffect(() => {
    const fetchData = async () => {
      try {
        const videos = await listAll(reference);
        const completeVideos = await Promise.all(
          videos.items.map(async (video) => {
            const metadata = await getMetadata(ref(storage, video.fullPath));
            const url = await getDownloadURL(ref(storage, video.fullPath));
            return {
              name: metadata.name,
              size: prettysize(metadata.size),
              url,
            };
          })
        );
        setVideos(completeVideos);
      } catch (e) {
        console.log(e);
      }
    };
    fetchData();
  }, []);

Promise.all takes an array of promises, and returns a promise that resolves with an array of all the resolved values once all the promises are in the fulfilled state. This is useful when you want to perform asynchronous operations like your getMetaData and getDownloadURL, on multiple elements of an array. You will use .map instead of .forEach since map returns an array, while forEach does not. By passing an async function to .map, since an async function always returns a Promise, you are basically creating an array of promises. and that's what you can feed Promise.all with. That's it, now it just waits that all the async calls are done and you can just await for Promise.all to resolve.

  • Related