How to prevent set state before useEffect? - javascript

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.

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

How to use data of an Async function inside a functional component to render HTML in React

I've been trying to use the data I get from an Async function inside of another function I use to display HTML on a react project. I have made several attempts but nothing seems to work for me. Hope any of you could help me. Please correct me if I did anything wrong.
I've tried it with a useEffect as well:
import React, { useState, useEffect } from 'react';
import { getGenres } from './api/functions';
const ParentThatFetches = () => {
const [data, updateData] = useState();
useEffect(() => {
const getData = async () => {
const genres = await getGenres('tv');
updateData(genres);
}
getData();
}, []);
return data && <Screen data={data} />
}
const Screen = ({data}) => {
console.log({data}); //logs 'data: undefined' to the console
return (
<div>
<h1 className="text-3xl font-bold underline">H1</h1>
</div>
);
}
export default Screen;
The Error I get from this is: {data: undefined}.
The getGenres function that makes the HTTP Request:
const apiKey = 'key';
const baseUrl = 'https://api.themoviedb.org/3';
export const getGenres = async (type) => {
const requestEndpoint = `/genre/${type}/list`;
const requestParams = `?api_key=${apiKey}`;
const urlToFetch = baseUrl + requestEndpoint + requestParams;
try {
const response = await fetch(urlToFetch);
if(response.ok) {
const jsonResponse = await response.json();
const genres = jsonResponse.genres;
return genres;
}
} catch(e) {
console.log(e);
}
}
I want to use the data inside my HTML, so the H1 for example.
Once again, haven't been doing this for a long time so correct me if I'm wrong.
There are a few conceptual misunderstandings that I want to tackle in your code.
In modern React, your components should typically render some type of jsx, which is how React renders html. In your first example, you are using App to return your genres to your Screen component, which you don't need to do.
If your goal is to fetch some genres and then ultimately print them out onto the screen, you only need one component. Inside that component, you will useEffect to call an asynchronous function that will then await the api data and set it to a react state. That state will then be what you can iterate through.
When genres is first rendered by react on line 6, it will be undefined. Then, once the api data is retrieved, React will update the value of genre to be your array of genres which will cause the component to be re-rendered.
{genres && genres.map((genre) ... on line 20 checks to see if genres is defined, and only if it is, will it map (like looping) through the genres. At first, since genres is undefined, nothing will print and no errors will be thrown. After the genres are set in our useEffect hook, genres will now be an array and we can therefore loop through them.
Here is a working example of your code.
import React, { useState, useEffect } from "react";
import { getGenres } from "./api/functions";
function App() {
const [genres, setGenres] = useState();
useEffect(() => {
async function apiCall() {
const apiResponse = await getGenres("tv");
console.log(apiResponse);
setGenres(apiResponse);
}
apiCall();
}, []);
return (
<div>
<h1 className="text-3xl font-bold underline">H1</h1>
{genres && genres.map((genre) => <div key={genre}>{genre}</div>)}
</div>
);
}
export default App;
You should use a combination or useEffect and useState
You should use useEffect to launch the async get, without launching it each rerendering. If a async function is used to get some data from outside the component, this is called 'side effect' so use useEffect.
You should use useState to react to changes on theses side effects, re-rendering the component to get the data in the dom.
In the next example, Im using a dummy async function getGenres which returns an array of genres.
Here is an example and a WORKING EXAMPLE :
const {useState, useEffect} = React;
async function getGenres() {
var promise = new Promise(function(resolve, reject) {
window.setTimeout(function() {
resolve( ['genre1', 'genre2']);
});
});
return promise;
}
const Screen = () => {
const [genres, setGenres] = useState([])
useEffect(
() => {
getGenres().then(
res => setGenres(res)
)
}, [getGenres]
)
return (
<ul>
{
genres.map(
i => <li>{i}</li>
)
}
</ul>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<Screen/>)

Custom Hook to Load Firebase Image URL

I'm trying to create a custom hook that will load the URL for an image stored in Firebase.
So far I've arrived at the following:
useLoadImageURL.js
export default async function useLoadImageURL(projectId, fileName) {
const [url, setURL] = useState()
useEffect(() => {
return url
}, [url])
const storage = getStorage(app);
setURL(await getDownloadURL(ref(storage, `images/projects/${projectId}/${fileName}`)))
}
However, when I use it in my component, I only get promises, and therefore using the returned promise as URL does not work.
I'm calling the hook via the following:
ProjectCard.js
export default function ProjectCard({ project }) {
const coverImageURL = useLoadImageURL(project.id, project.coverImagePath)
return <img src={coverImageURL} />
}
How can I change the hook or how I'm calling it to retrieve the actual URL?
You can remove async and await from your code, and instead of calling setUrl at the bottom of your hook -- which would initiate the call every time the callee component rerenders -- wrap it in a useEffect, and update the state in a then callback.
edit: Full code:
const storage = getStorage(app); // as app seems to be external to the hook, storage can likely be too
export default function useLoadImageURL(projectId, fileName) {
const [url, setUrl] = useState();
useEffect(() => {
getDownloadURL(ref(storage, `images/projects/${projectId}/${fileName}`))
.then((value) => {
setUrl(value); // should work provided getDownloadURL actually is a promise that gives a string
});
}, [projectId,fileName]);
return url;
}
This will make the url state only change when projectId or fileName changes.
You have to do your side effect logic (awaiting async response in your case) inside the useEffect hook.
Following your intent to do it with promises and async await, your snippet hook should be something like this.
import { useState, useEffect } from "react";
// hook
function useLoadImageURL(filename) {
const [url, setURL] = useState("some-image-spinner-url");
// simulating your async api call to firebase service
const setTimeoutAsync = (cb, delay) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(cb());
}, delay);
});
useEffect(() => {
async function asyncFn() {
const url = await setTimeoutAsync(() => filename, 3000);
setURL(url);
}
asyncFn();
}, [filename]);
return [url];
}
// main component
function App() {
const [url] = useLoadImageURL("firebase-file-name-f1");
return <div> {url} </div>;
}
export default App;

Await result of API call and update list component

I am calling a REST API and return chat rooms. Those get returned fine, as I can see in the payload of the object via the console.
Now I want to display this in a list.
I was able to display a list and download the chat rooms, but not combine both. This is what I did:
import * as React from 'react';
export default function ChatRoomList({param1}) {
var x = getChatRoom(param1)
console.log(x)
return (
<div>
<li>{param1}</li>
<li> asd </li>
<li> asd </li>
</div>
);
}
async function getChatRoom(status) {
// status could be either 'open' or 'room'
var dict = {
chatType: status,
};
var adminChats = await callKumulosAPIFunction(dict, 'getAdminChat')
return adminChats
}
Now I did try to simply await the first getChatRoom(param1) call. But I can only await inside an async function. I then tried to add the async keyword to the export function, but this would crash the whole app.
How would the workflow be here? And how would I map the result from getChatRoom onto a listview?
The result of the adminChats (console.log(adminChats)):
You need to use useEffect hook to get remote data:
export default function ChatRoomList({param1}) {
React.useEffect(()=>{
(async () => {
var x = await getChatRoom(param1)
console.log(x)
})()
},[])
return (
<div>
<li>{param1}</li>
<li> asd </li>
<li> asd </li>
</div>
);
}
If the data returned from getChatRoom is an array and you want to show it, you need to save the response in a state, or the component will not re-render:
export default function ChatRoomList({param1}) {
const [chatRooms, setChatRooms] = React.useState([]);
React.useEffect(()=>{
(async () => {
var x = await getChatRoom(param1)
setChatRooms(x)
})()
},[])
return (
<div>
<li>{param1}</li>
{chatRooms.map((chatRoom , index) => {
return <li key={index}>{JSON.stringify(chatRoom)}</li>
})}
</div>
);
}
async function getChatRoom(status) {
// status could be either 'open' or 'room'
var dict = {
chatType: status,
};
var adminChats = await callKumulosAPIFunction(dict, 'getAdminChat')
return adminChats.payload
}
I suggest you to read the documentation about React hooks and lifecycle management:
https://reactjs.org/docs/hooks-intro.html
https://reactjs.org/docs/state-and-lifecycle.html
You need to use useEffect hook to call the API.
Read more about hooks
import * as React from 'react';
function getChatRoom(status) {
const [chats, setChats] = React.useState([]);
const getChart = async (status) => {
// status could be either 'open' or 'room'
const dict = {
chatType: status,
};
const adminChats = await callKumulosAPIFunction(dict, 'getAdminChat');
setChats(adminChats);
};
React.useEffect(() => {
getChart(status)
}, [status])
return { chats };
};
export default function ChatRoomList({param1}) {
const { chats } = getChatRoom(param1)
console.log(chats)
return (
<div>
<li>{param1}</li>
{chats?.map((chatRoom , index) => {
return <li key={index}>{JSON.stringify(chatRoom)}</li>
})}
</div>
);
}
Dependencies argument of useEffect is useEffect(callback, dependencies)
Let's explore side effects and runs:
Not provided: the side-effect runs after every rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after EVERY rendering
});
}
An empty array []: the side-effect runs once after the initial rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
}
Has props or state values [prop1, prop2, ..., state1, state2]: the side-effect runs only when any dependency value changes.
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state]);
}

logging the data but not rendering p tag , why?

I am using firebase firestore and i fetched the data , everything is working fine but when i am passing it to some component only one item gets passed but log shows all the elements correctly.
I have just started learning react , any help is appreciated.
import React, { useEffect, useState } from 'react'
import { auth, provider, db } from './firebase';
import DataCard from './DataCard'
function Explore() {
const [equipmentList, setEquipments] = useState([]);
const fetchData = async () => {
const res = db.collection('Available');
const data = await res.get();
data.docs.forEach(item => {
setEquipments([...equipmentList, item.data()]);
})
}
useEffect(() => {
fetchData();
}, [])
equipmentList.forEach(item => {
//console.log(item.description);
})
const dataJSX =
<>
{
equipmentList.map(eq => (
<div key={eq.uid}>
{console.log(eq.equipment)}
<p>{eq.equipment}</p>
</div>
))
}
</>
return (
<>
{dataJSX}
</>
)
}
export default Explore
You have problems with setting fetched data into the state.
You need to call setEquipments once when data is prepared because you always erase it with an initial array plus an item from forEach.
The right code for setting equipment is
const fetchData = async () => {
const res = db.collection('Available');
const data = await res.get();
setEquipments(data.docs.map(item => item.data()))
}

simple fetching api with hooks and printing?

I'm trying to import a card API using react hooks and print the cards to the browser but I can't figure out how?
This is the structure of the api
Im trying to use the .map function to access the "image"
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const App = () => {
const [deck, setDeck] = useState ([]);
useEffect(async () => {
const response = await fetch('https://deckofcardsapi.com/api/deck/new/draw/?count=5')
const data = await response.json();
setDeck(data);
console.log(data)
}, []);
return (
<ul>
{deck.map(a => (<li> {a.cards.image}</li>))} // how to access api's "image" ??
</ul>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Vicente, have a look at this CodeSandbox to see an example of how you could implement it. And here is the App component:
const App = () => {
const [deck, setDeck] = React.useState({ cards: [] });
React.useEffect(() => {
async function getData() {
const response = await fetch(
"https://deckofcardsapi.com/api/deck/new/draw/?count=5"
);
const data = await response.json();
setDeck(data);
console.log(data);
}
getData();
}, []);
return (
<div className="App">
{deck.cards.map((card, index) => (
<div key={index}>
<img src={card.image} alt={card.code} />
</div>
))}
</div>
);
};
An important thing to take into account is that React.useEffect should run synchronously. Instead of passing an async function into it, define an async function inside the useEffect callback, and run it synchronously. Inside this new function you can await for promises to resolve, and you can set the state inside this function. Please read "React's useEffect" docs for more information
Another problem you had was that you were instantiating the deck state with an empty array ([]). But, the response is an object, so when you updated your state you got an error. I solve that by initializing the deck state with an object that defined a cards list, following the result of your endpoint.

Categories

Resources