Javascript, then method not waiting for async function in React - javascript

I'm trying to set the background of my React App to the NASA APOD (Astronomy Picture of the Day). Here is my code for that:
import './App.css';
import React, { useState, useEffect } from "react";
const apiKey = process.env.REACT_APP_NASA_KEY;
function App() {
const [bg, setBg] = useState(null);
async function fetchPhoto() {
const dat = await fetch(`https://api.nasa.gov/planetary/apod?api_key=${apiKey}`);
const data = await dat.json();
setBg(data);
}
useEffect(() => {
fetchPhoto().then(() => {
document.body.style.backgroundImage = `url('${bg.url}')`
})
}, [])
return null;
}
However, in this code then does not wait for the state to be set and I get the error:
App.js:22 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'url')at App.js:22:1
What am I doing wrong? Thanks!

1. Solution
A simpler way to achieve what you want is by doing like below, this way you don't have to menage asynchronous tasks like you are doing:
import './App.css';
import React, { useState, useEffect } from "react";
const apiKey = process.env.REACT_APP_NASA_KEY;
function App() {
const [bg, setBg] = useState(null);
async function fetchPhoto() {
const dat = await fetch(`https://api.nasa.gov/planetary/apod?api_key=${apiKey}`);
const data = await dat.json();
setBg(data);
}
useEffect(() => {
fetchPhoto()
}, [])
useEffect(() => {
if(bg){
document.body.style.backgroundImage = `url('${bg.url}')`;
}
}, [bg])
return null;
}
2. Explanation
Your code is not working because just after fetchPhoto() is executed and resolved, React did not yet re-render the components (update the state), therefore bg is still null.

Do you need the state for something else? If not, you could avoid it:
function App() {
useEffect(() => {
const fetchPhoto = async () => {
const dat = await fetch(`https://api.nasa.gov/planetary/apod?api_key=${apiKey}`);
const data = await dat.json();
if (data) {
document.body.style.backgroundImage = `url('${data.url}')`;
}
}
fetchPhoto();
}, [])
return null;
}

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/>)

React - Can console log object, but not specific property of said object

I'm doing an exercise to learn React in which I have set up a page with a list of clickable pokemon names which are linking to the pokemons specific detail page. Below is the code of the details page
import { useState, useEffect } from "react";
import axios from "axios";
import { useParams } from "react-router-dom";
export default function DetailsPage() {
const pokeName = useParams();
console.log(pokeName);
const [pokeList, setPokeList] = useState([]);
useEffect(() => {
const fetchData = async () => {
const response = await axios.get(
"https://pokeapi.co/api/v2/pokemon?limit=151"
);
console.log(response.data);
setPokeList(response.data.results);
};
fetchData();
}, []);
const specificPokemon = pokeList.find((pokemon) => {
return pokemon.name === pokeName.pokemon_name;
});
console.log(specificPokemon);
console.log(specificPokemon.name);
return <div><p>{specificPokemon.name}</p></div>;
}
This code has an error I fail to understand
The console.log(specificPokemon) works fine, but the console.log(specificPokemon.name) gives me the following error
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
The correct code is the following, but I wonder why my method doesn't work
const [pokeList2, setPokeList2] = useState([]);
useEffect(() => {
const fetchData = async () => {
const response = await axios.get(
`https://pokeapi.co/api/v2/pokemon/${pokeName.pokemon_name}`
);
console.log(response.data);
setPokeList(response.data);
};
fetchData();
}, []);
console.log(pokeList);
Thank you
When the code runs first the pokeList is an empty array and it cannot find the property name. You should create a second state and do something like this
const pokeName = useParams();
const [pokeList, setPokeList] = useState([]);
const [specificPokemon, setSpecificPokemon] = useState({});
useEffect(() => {
const fetchData = async () => {
const response = await axios.get(
"https://pokeapi.co/api/v2/pokemon?limit=151"
);
setPokeList(response.data.results);
const selectedPokemon = response.data.results.find((pokemon) => {
return pokemon.name === pokeName.pokemon_name;
});
setSpecificPokemon(selectedPokemon)
};
fetchData();
}, [])
And don't forget to make the specificPokemon property optional like this specificPokemon?.name
When your component is mounted, pokeList is an empty array.
React will run the following block before the useEffect hook has finished running:
const specificPokemon = pokeList.find((pokemon) => {
return pokemon.name === pokeName.pokemon_name;
});
console.log(specificPokemon);
console.log(specificPokemon.name);
As long as your array is empty, specificPokemon will be undefined and calling specificPokemon.name will trigger your error.
Beware with console.log, its behavior is not always synchronous.
You might think specificPokemon is properly defined because console.log won't necessarily show undefined.
To verify this, use console.log(JSON.stringify(specificPokemon));.

Using React Context with AsyncStorage using hooks

My goal is to use custom hooks created from Context to pass and modify stored values
The final goal is to use something like useFeedContext() to get or modify the context values
What I am actually getting is either the functions that I call are undefined or some other problem ( I tried multiple approaches)
I tried following this video basics of react context in conjunction with this thread How to change Context value while using React Hook of useContext but I am clearly getting something wrong.
Here is what I tried :
return part of App.js
<FeedProvider mf={/* what do i put here */}>
<Navigation>
<HomeScreen />
<ParsedFeed />
<FavScreen />
</Navigation>
</FeedProvider>
Main provider logic
import React, { useState, useEffect, useContext, useCallback } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
const FeedContext = React.createContext();
const defaultFeed = [];
const getData = async (keyName) => {
try {
const jsonValue = await AsyncStorage.getItem(keyName);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.log(e);
}
};
const storeData = async (value, keyName) => {
console.log(value, keyName);
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(keyName, jsonValue);
} catch (e) {
console.log(e);
}
};
export const FeedProvider = ({ children, mf }) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
const [feedLoaded, setFeedLoaded] = useState(false);
let load = async () => {
let temp = await AsyncStorage.getItem("mainUserFeed");
temp != null
? getData("mainUserFeed").then((loadedFeed) => setMainFeed(loadedFeed))
: setMainFeed(defaultFeed);
setFeedLoaded(true);
};
useEffect(() => {
load();
}, []);
useCallback(async () => {
if (!feedLoaded) {
return await load();
}
}, [mainFeed]);
const setFeed = (obj) => {
setMainFeed(obj);
storeData(mainFeed, "mainUserFeed");
};
return (
<FeedContext.Provider value={{ getFeed: mainFeed, setFeed }}>
{children}
</FeedContext.Provider>
);
};
//export const FeedConsumer = FeedContext.Consumer;
export default FeedContext;
The custom hook
import { useContext } from "react";
import FeedContext from "./feedProviderContext";
export default function useFeedContext() {
const context = useContext(FeedContext);
return context;
}
What I would hope for is the ability to call the useFeedContext hook anywhere in the app after import like:
let myhook = useFeedContext()
console.log(myhook.getFeed) /// returns the context of the mainFeed from the provider
myhook.setFeed([{test:1},{test:2}]) /// would update the mainFeed from the provider so that mainFeed is set to the passed array with two objects.
I hope this all makes sense, I have spend way longer that I am comfortable to admit so any help is much appreciated.
If you want to keep using your useFeedContext function, I suggest to move it into the your 'Provider Logic' or I'd call it as 'FeedContext.tsx'
FeedContext.tsx
const FeedContext = createContext({});
export const useFeedContext = () => {
return useContext(FeedContext);
}
export const AuthProvider = ({children}) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
...
return (
<FeedContext.Provider value={{mainFeed, setMainFeed}}>
{children}
</FeedContext.Provider>
);
};
YourScreen.tsx
const YourScreen = () => {
const {mainFeed, setMainFeed} = useFeedContext();
useEffect(() => {
// You have to wait until mainFeed is defined, because it's asynchronous.
if (!mainFeed || !mainFeed.length) {
return;
}
// Do something here
...
}, [mainFeed]);
...
return (
...
);
};
export default YourScreen;

Why does this custom hook return indefined in react?

i am creating a custom hook in React, this is my code:
import {useEffect} from 'react';
const useFetch = (url) => {
useEffect(() => {
const fetchData = () => {
const data = url + "TEST";
return data;
}
fetchData();
})
}
export default useFetch;
It now returns some dummy value but that is just because of testing purposes.
Here is where i invoke my custom hook:
const Data = useFetch("https://customapiurlrandom.com/");
useEffect(() => {
console.log(Data);
}, [])
The thing is, when i check my console i see undefined. And i can't find out why.
Any ideas? Thanks in advance.
Your custom hook didn't return anything. You should add a state to keep the value and return it
import {useEffect, useState} from 'react';
const useFetch = (url) => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = () => {
const data = url + "TEST";
return data;
}
setData(fetchData());
},[url, setData]);
return data;
}
export default useFetch;
And then use like this
const Data = useFetch("https://customapiurlrandom.com/");
useEffect(() => {
console.log(Data);
}, [Data])

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