Custom Hook to Load Firebase Image URL - javascript

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;

Related

Is it possible to fetch API without useEffect in React JS?

Hello i'm newbie here...
I found my friend's code when he using useState instead of using useEffect to fetch the API.
I tried it and it worked, the code didn't cause an error and infinite loops.
here is for the code
import { useState } from "react";
import { IN_THEATER, POSTER } from "../../../constant/movies";
import { GlobalGet } from "../../../utilities/fetch";
const Service = () => {
const [movieData, setMovieData] = useState({ data: null, poster: null });
const fetchMovieData = async () => {
try {
let movieRes = await GlobalGet({ url: `${IN_THEATER}` });
return movieRes;
} catch (error) {
console.log(error);
}
};
const fetchPoster = async () => {
try {
let posterRes = await GlobalGet({ url: `${POSTER}` });
return posterRes;
} catch (error) {
console.log(error);
}
};
const fetchData = async () => {
setMovieData({
data: await fetchMovieData(),
poster: await fetchPoster(),
});
};
useState(() => { //<=here it is
fetchData();
}, []);
return {
movieData,
};
};
export default Service;
And my question is, why it could be happen ? why using useState there doesn't cause an infinite loops ?
The useState() function can accept an initializer function as its first argument:
const [state, setState] = useState(initializerFunction)
When a function is passed to useState(), that function is only called once before the component initially mounts. In your case below, the initializer function is an anonymous arrow function:
useState(() => { // <-- Initializer function invoked once
fetchData();
}, []);
Here, the initializer function is () => { fetchData(); }, which is invoked once before the initial mount, so the fetchData() method is only called once. The array that is passed as the second argument [] is useless and doesn't do anything in this case as it's ignored by useState(). The above useState would behave the same if you did useState(fetchData);. Because fetchData() is only called once on the initial mount, any state updates of your component don't cause the fetchData() function to execute again as it's within the initializer function.
With that said, useState() shouldn't be used for fetching data on mount of your component, that's what useEffect() should be used for instead.
Generally it's possible to fetch data from outside of the useEffect hook.
Somewhere in the body of your component...
const [fetchedData, setFetchedData] = useState(false)
const someFetchFunc = asyunc (url) => {
setFetchedData(!fetchedData)
const res = await fetch(url)
const data = await res.json()
setMovieData(data)
}
!fetchedData && someFetchFunc()
But this is an antipattern. In this case developer lacks a whole toolset of dealing with possible issues. What if fetching fails?
So, it's generally a good idea to handle all the side effects like fetching in a place that was intended for that. It's useEffect hook)

How to prevent set state before useEffect?

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.

How to use a custom-hook that returns a value, inside another custom Hook?

I am using React-native and in it, I have a custom Hook called useUser that gets the user's information from AWS Amplify using the Auth.getUserInfro method, and then gets part of the returned object and sets a state variable with it. I also have another Hook called useData hook that fetches some data based on the userId and sets it to a state variable.
useUser custom-Hook:
import React, { useState, useEffect } from "react";
import { Auth } from "aws-amplify";
const getUserInfo = async () => {
try {
const userInfo = await Auth.currentUserInfo();
const userId = userInfo?.attributes?.sub;
return userId;
} catch (e) {
console.log("Failed to get the AuthUserId", e);
}
};
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
getUserInfo().then((userId) => {
setId(userId);
});
}, []);
return id;
};
export default useUserId;
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) = > { // code to fetch data from GraphQl}
const useData = () => {
const [data, setData] = useState();
useEffect(() => {
const userId = useUser();
fetchData(userId).then( // the rest of the code to set the state variable data.)
},[])
return data
}
When I try to do this I get an error telling me
*Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.*
I think the problem is that I am calling the Hook useUser inside of the use effect, but using it inside the function will cause the problem described here, and I can't use it outside the body of the fetchData since the useData itself is a hook, and it can be only used inside a functional component's or Hook's body. So I don't know how to find a way around this problem.
Correct, React hooks can only be called from React function components and other React hooks. The useEffect hook's callback isn't a React hook, it's a callback. According to the Rules of Hooks, don't call hooks inside loops, conditions, or nested functions.
I suggest refactoring the useData hook to consume the userId as an argument, to be used in the dependency array of the useEffect.
const fetchData = async (userId) => {
// code to fetch data from GraphQl
};
const useData = (userId) => {
const [data, setData] = useState();
useEffect(() => {
fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
});
}, [userId]);
return data;
};
Usage in Function component:
const userId = useUser();
const data = useData(userId);
If this is something that is commonly paired, abstract into a single hook:
const useGetUserData = () => {
const userId = useUser();
const data = useData(userId);
return data;
};
...
const data = useGetUserData();
Though you should probably just implement as a single hook as follows:
const useGetUserData = () => {
const [data, setData] = useState();
useEffect(() => {
getUserInfo()
.then(fetchData) // shortened (userId) => fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
setData(....);
});
}, []);
return data;
};
You can't call hook inside useEffect, Hook should be always inside componet body not inside inner function/hook body.
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) => {
// code to fetch data from GraphQl}
};
const useData = () => {
const [data, setData] = useState();
const userId = useUser();
useEffect(() => {
if (userId) {
fetchData(userId).then(setData);
}
}, [userId]);
return data;
};

React: ES2020 dynamic import in useEffect blocks rendering

From what I understand, async code inside useEffect runs without blocking the rendering process. So if I have a component like this:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
const data = await response.json();
console.log("end");
};
getData();
}, []);
return null;
};
The console output is (in order):
begin
window loaded
end
However if I use ES2020 dynamic import inside the useEffect:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const data = await import("./data.json");
console.log("end");
};
getData();
}, []);
return null;
};
The output is (in order):
begin
end
window loaded
This is a problem because when data.json is very large, the browser hangs while importing the large file before React renders anything.
All the necessary and useful information is in Terry's comments. here is the implementation of what you want according to the comments:
First goal:
I would like to import the data after window has loaded for SEO reasons.
Second goal:
In my case the file I'm trying to dynamically import is actually a function that requires a large dataset. And I want to run this function whenever some state has changed so that's why I put it in a useEffect hook.
Let's do it
You can create a function to get the data as you created it as getData with useCallback hook to use it anywhere without issue.
import React, {useEffect, useState, useCallback} from 'react';
function App() {
const [data, setData] = useState({});
const [counter, setCounter] = useState(0);
const getData = useCallback(async () => {
try {
const result = await import('./data.json');
setData(result);
} catch (error) {
// do a proper action for failure case
console.log(error);
}
}, []);
useEffect(() => {
window.addEventListener('load', () => {
getData().then(() => console.log('data loaded successfully'));
});
return () => {
window.removeEventListener('load', () => {
console.log('page unmounted');
});
};
}, [getData]);
useEffect(() => {
if (counter) {
getData().then(() => console.log('re load data after the counter state get change'));
}
}, [getData, counter]);
return (
<div>
<button onClick={() => setCounter((prevState) => prevState + 1)}>Increase Counter</button>
</div>
);
}
export default App;
Explanation:
With component did mount, the event listener will load the data.json on 'load' event. So far, the first goal has been met.
I use a sample and simple counter to demonstrate change in the state and reload data.json scenario. Now, with every change on counter value, the getData function will call because the second useEffect has the counter in its array of dependencies. Now the second goal has been met.
Note: The if block in the second useEffect prevent the getData calling after the component did mount and first invoking of useEffect
Note: Don't forget to use the catch block for failure cases.
Note: I set the result of getData to the data state, you might do a different approach for this result, but the other logics are the same.

How do deal with React hooks returning stale event handlers

I'm trying to figure out how to deal with stale event handlers returned by a hook. First when the component is first rendered, it makes a an asynchronous request to api to fetch credentials. Then these credentials are used when pressing a submit button in a dialog to create a resource. The problem is the credentials for the dialog submit button click event handler are undefined even after the credentials have been fetched.
credentials.js
import { useEffect } from 'react';
import { api } from './api';
export const useCredentials = (setCredentials) => {
useEffect(() => {
const asyncGetCredentials = async () => {
const result = await api.getCredentials();
if (result) {
setCredentials(result);
}
};
asyncGetCredentials().then();
}, []);
return credentials;
}
useComponent.js
import { useEffect, useRef, useCallback, useState } from 'react';
import { useCredentials } from './credentials';
import { createResource } from './resources';
import { useDialog } from './useDialog';
export const useComponent = () => {
const { closeDialog } = useDialog();
const [credentials, setCredentials] = useState();
useCredentials(setCredentials);
const credentialsRef = useRef(credentials);
useEffect(() => {
// logs credentials properly after they have been fetched
console.log(credentials)
credentialsRef.current = credentials;
}, [credentials]);
const createResourceUsingCredentials = useCallback(
async function () {
// credentials and credentialsRef.current are both undefined
// even when the function is called after the credentials
// have already been fetched.
console.log(credentials);
console.log(credentialsRef.current);
createResource(credentialsRef.current);
}, [credentials, credentialsRef, credentialsRef.current]
);
const onDialogSubmit = useCallback(
async function () {
await createResourceUsingCredentials();
closeDialog();
}, [
credentials,
credentialsRef,
credentialsRef.current,
createResourceUsingCredentials,
],
);
return {
onDialogSubmit,
}
}
Try this way
export const useCredentials = (setCredentials) => {
useEffect(() => {
const asyncGetCredentials = async () => {
const result = await api.getCredentials();
if (result) {
setCredentials(result);
}
};
asyncGetCredentials().then();
}, []);
}
export const useComponent = () => {
const { closeDialog } = useDialog();
const [credentials, setCredentials] = useState(); // new add
useCredentials(setCredentials);
....
}
Why are you adding complexity, always return function and check inside the function for credentials
export const useComponent = () => {
const { closeDialog } = useDialog();
const credentials = useCredentials();
// correctly logs undefined at first and updated credentials
// when they are asynchronously received from the api.
console.log(credentials);
async function createResourceUsingCredentials() {
createResource(credentials);
}
let onClickDialogSubmit = async () => {
if (credentials) {
await createResourceUsingCredentials();
closeDialog();
}
};
return {
onClickDialogSubmit,
}
}
I found the problem was in the useCredentials hook implementation. It blocks any further requests for credentials to the api if a request is already in flight. Due to poor implementation of this functionality, if more than 1 component using that hook was rendered, only the component that was rendered first got updated credentials. I changed the useCredentials hooks so that it subscribes to the global state (that has the credentials) so that no matter which component starts the request, all components will get the credentials when the request finishes. https://simbathesailor007.medium.com/debug-your-reactjs-hooks-with-ease-159691843c3a helped a lot with debugging this issue.

Categories

Resources