24

This is a component that render data from firebase storage and make it listed. What the function has to do is set the videos extracted from firebase storage to the useState. That way I can call videos and map into a new component, which happens to be a list of buttons. It works relatively well, the problem is that the component renders twice, the first time it doesn't save the videos in the state, and the second time it does. In other words, the component does not wait for the videos to be saved in the state, and simply renders itself, resulting in the list of buttons with the title of the videos not being displayed.

// 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 capitalize from "../../../utils/capitalize";
import { PlayIcon } from "@heroicons/react/outline";

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

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

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

  // Check if path is empty

  function getVideos() {
    let items = [];
    listAll(reference).then((res) => {
      res.items.forEach(async (item) => {
        getDownloadURL(ref(storage, `training/${id}/${item.name}`)).then(
          (url) => {
            items.push({
              name: item.name.replace("-", " "),
              href: item.name,
              url,
            });
          }
        );
      });
    });
    setVideos(items);
  }

  useEffect(() => {
    getVideos();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [id]);

  console.log(videos);

  return (
    <>
      <Seo
        title={`${capitalize(id)} Training - Dashboard`}
        description={`${capitalize(
          id
        )} training for all Every Benefits Agents.`}
      />
      <DashboardLayout>
        <h2>{capitalize(reference.name)}</h2>
        <section>
          <video controls controlsList="nodownload">
            {videoActive && <source src={videoActive} type="video/mp4" />}
          </video>
          <ul role="list" className="divide-y divide-gray-200 my-4">
            {videos.map((video, index) => (
              <button key={index} className="py-4 flex">
                <div className="w-full ml-3 flex flex-row justify-start items-center space-x-3">
                  <PlayIcon className="w-6 h-6 text-gray-600" />
                  <p className="text-sm font-medium text-gray-900">
                    {video.name}
                  </p>
                </div>
              </button>
            ))}

            {console.log("Component rendered")}
          </ul>
        </section>
      </DashboardLayout>
    </>
  );
}

export default withProtected(Video);

This is an example of how the component is begin rendering:

enter image description here

Anyone has an idea of why this is happening?

Diesan Romero
  • 1,292
  • 4
  • 20
  • 45
  • Are you ensure that rerender is called only in Video? Maybe parent component force rerender. – CubeStorm Apr 12 '22 at 19:00
  • @CubeStorm There is a parent component which is the dashboard layout. That one is also rendered but together with the child component. I notice that because they both render twice. The reason why I put the logout function in the console log is to check that precisely. Since the logout function is only rendered in the layout. – Diesan Romero Apr 12 '22 at 19:04
  • 1
    `useEffect` calls `getVideos` that sets state - state changes cause rerenders. – Sean W Apr 12 '22 at 19:25
  • @SeanW Something like that I suspect. This happens when I call getDownloadUrl inside listAll. I assume that each time each is called the state is re-rendered. The problem is that I need information from both. listAll to list each of the videos and display them in the list, and getDownloadUrl to access the firebase url and then be able to render the video in the editor. There is any way to solve it? – Diesan Romero Apr 12 '22 at 19:41
  • I think that setVideos(items): is called twice because you are not awaiting for the async API calls. What happens if you move the setVideos(items); immediately after the res.items.forEach? Could that help? Or you could try to await all the async API calls and then use setVideos(items):. – Mircea Matei Apr 15 '22 at 14:07
  • You'll always re-render the component because you're updating state inside `useEffect`. If you want to avoid that, fetch the videos data on the server-side instead. – juliomalves May 11 '22 at 20:32

2 Answers2

70

I found the answer in this thread. https://github.com/vercel/next.js/issues/35822

In shortly, This issue is about React 18 Strict Mode. You can read about what's new in the React 18 Strict Mode.

If you are not using strict mode it should not happen. If it is not very important, you can turn off React Strict Mode in the next.config.js. file as shown below.

const nextConfig = {
  reactStrictMode: false
}

module.exports = nextConfig
Ferruh
  • 701
  • 2
  • 3
4

This is because strict mode in react is used to find out common bugs in component level and to do so, react renders twice a single component. First render as we expect and other render is for react strict mode. In development phase you will face the issue although this is not an issue. It's for better dev-experience at development phase. In production phase there will be no issue like this by default without any fault.

We can read more from : https://react.dev/reference/react/StrictMode

Moreover, if this is annoying to you you can just disable the strict mode (I personally don't prefer to disable strict mode) in next.config.js:

const nextConfig = {
  reactStrictMode: false
}

module.exports = nextConfig

Thank you!