1

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

Diesan Romero
  • 1,292
  • 4
  • 20
  • 45
  • What you're describing is expected and correct behavior. What exactly is the issue? In what way is this behavior a problem? – David Apr 16 '22 at 19:17
  • @David The problem is that the component sticks with the first render. The second render is the one that updates the array with the videos. Therefore, I can't see the information on the screen, because the component only paints it once. – Diesan Romero Apr 16 '22 at 19:19
  • 2
    What do you mean by *"the component sticks with the first render"*? Do you mean that the component is not re-rendering when state is updated? Because the console output certainly implies that it does, though we can only guess where the `console.log` statement is because it's not in the code shown. – David Apr 16 '22 at 19:21
  • You're not awaiting `getMetadata` and `getDownloadURL` promises before setting the state, and when they finally resolve, they're mutating the current state array (another issue). – Emile Bergeron Apr 16 '22 at 19:24
  • @David I have added the console log in the code for you to see. Just before the return. The idea with that console log was to see if the state was updating. Indeed, as you say, the state is updated. So, I don't understand why he doesn't paint it on screen. – Diesan Romero Apr 16 '22 at 19:24
  • Does this answer your question? [Use async await with Array.map](https://stackoverflow.com/questions/40140149/use-async-await-with-array-map) – Emile Bergeron Apr 16 '22 at 19:25
  • @EmileBergeron no. I try to update the question for better understanding. Please, feel free to ask me for clarification if need it. – Diesan Romero Apr 16 '22 at 19:29
  • There's 2 things: the log only shows the videos because it gets mutated in-between the render and the time you end up looking at it ([lazy evaluation in the console](https://stackoverflow.com/q/23429203/1218980)). The second thing, to fix the issue, convert your `forEach` loops into `await Promise.all(tempVideos.map(video => getMetadata(/*...*/)))` so that the state is only set when it is ready. – Emile Bergeron Apr 16 '22 at 19:32
  • 1
    @EmileBergeron It's work perfectly. Thanks. So, to fully understand, if I want to render one time the result of more than one promise, I should use Promise all instead of do it one by one? – Diesan Romero Apr 16 '22 at 19:37

1 Answers1

2

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.

Cesare Polonara
  • 3,473
  • 1
  • 9
  • 19