I'm having a problem with useEffect and async-await function and I'm not sure how to explain. Let me try: I have two components, parent and child, and the parent sends some props to the child, including a funtion to modify some state and the state. The child use this props in useEffect hook to generate new states and use the funtion inside a async-await function to modify the states back in the parent. The child uses this function inside a for loop inside an async function, however, useEffect keeps runing and doesn't allow me to modify all states in the parent. This is a very simplified code, I hope is enough:
function in parent
const createEntry = async(entry) => {
const dummyEntries = clone(entries);
console.log('Entradas:');
console.log(dummyEntries);
dummyEntries.push(entry);
setEntries(dummyEntries)
return json.id;
}
It sends entries and createEntry to child
functions in child
useEffect(()=>{
//create some states, called entries, with props.entries
},[props.entries])
const Save = async () => {
if (Object.values(error).some((e) => e.value === true)) {
setModal(true);
} else if (props.accion === 'crear') {
for (const item of entries){
await props.createEntry(item)
}
history.push(`/path/`);
} else if (props.accion === 'editar') {
for (const item of entries){
if (!item.id) {
console.log(item)
await props.createEntry(item)
}
}
history.push(`/path`);
}
};
Only the last term in entries of Child component is saved in entries of Parent component
Ok, thank you to the commenters! This is the working code:
function in parent
const createEntry = async(new_entries) => {
const dummyEntries = clone(new_entries);
for (const entry of new_entries){
dummyEntries=[...dummyEntries,json]
}
setEntries(dummyEntries)
}
functions in child
useEffect(()=>{
//create some states, called entries, with props.entries
},[props.entries])
const Save = async () => {
if (Object.values(error).some((e) => e.value === true)) {
setModal(true);
} else if (props.accion === 'crear') {
await props.createEntry(entries)
history.push(`/path/`);
} else if (props.accion === 'editar') {
await props.createEntry(entries)
history.push(`/path`);
}
};
I use the async-await because there are some fetchs in the middle.
Related
I am using a MUI Autocomplete field that takes an array for options.
I created this hook that takes the input value and fetches the API based on it.
This is the code for it:
import axios from "axios";
import { useEffect, useState } from "react";
export default function useFetchGames(searchString) {
const [gameSearch, setGameSearch] = useState([]);
useEffect(() => {
if (searchString) setGameSearch(fetchData(searchString));
}, [searchString]);
return gameSearch;
}
const generateSearchOptions = (array) => {
const tempArray = [];
array.map((item) => {
tempArray.push(item.name);
});
return tempArray;
};
async function fetchData(searchString) {
const res = await axios
.post("/api/games", { input: searchString })
.catch((err) => {
console.log(err);
});
return generateSearchOptions(res.data);
}
And then i am calling this hook in the component where i have the autocomplete element.
const searchOptions = useFetchGames(inputValue);
The issue is,useFetchGames is supposed to return an array since the state is an array. But whenever the input changes, i get an error that you cant filter or map an object. Basically Autocompolete element is trying to map searchOptions but it is not an array.
I even tried to log its type with log(typeof searchOptions); and it returns an object.
I dont understand what I am doing wrong.
Edit: Here is the log of res.data. it is an array of objects. That is why i am remapping it to an array of just the names.
you get the promise back when you invoked fetchData(searchString) as it is an async function which always return a promise back
I would do it as
useEffect(() => {
// wrapping it as a async function so we can await the data (promise) returned from another async function
async function getData() {
const data = await fetchData(searchString);
setGameSearch(data);
}
if (searchString) {
getData();
}
}, [searchString]);
also refactoring this generateSearchOptions function in order to remove the creation of temp array which I feel is not required when using a map - below as
const generateSearchOptions = (array) => array.map(item => item.name)
I tried to modify the code found here > useLocalStorage hook
so that I could save a Set() to localStorage.
useLocalStorage.js
import { useState, useEffect } from "react";
// Hook
export const useLocalStorage = (key, initialValue) => {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
// Get from local storage by key
const item = JSON.parse(window.localStorage.getItem(key));
// Parse stored json or if none return initialValue
if(initialValue instanceof Set) {
return (item ? new Set([...item]) : initialValue);
} else {
return item ? item : initialValue
}
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
//const addValue = (value) => {
// setStoredValue(prev => new Set(prev).add(value));
//};
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
if(valueToStore instanceof Set){
setStoredValue( new Set([...valueToStore]) );
} else {
setStoredValue(valueToStore);
}
// Save state
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
useEffect(() => {
// Save to local storage
if (typeof window !== "undefined") {
if(storedValue instanceof Set){
window.localStorage.setItem(key, JSON.stringify([...storedValue]));
} else {
window.localStorage.setItem(key, JSON.stringify(storedValue));
}
}
}, [storedValue]);
return [storedValue, setValue /*,addValue */];
}
in my React Component I'm trying to set the value based on the previous state.
reactComponent.js
const [itemUrlIdSet, setItemUrlIdSet] = useLocalStorage('itemUrlIdSet', new Set());
const addItemUrlIdToSet = (item) => setItemUrlIdSet(prev => new Set(prev).add(item));
useEffect(() =>
addItemUrlIdToSet(`text changes when props change`);
}, [props]);
if I replace the setValue with addValue, it works fine... the Set in storedValue state updates, and the localStorage updates. If I use setValue, the storedValue never actually changes.
Add is less than ideal, there will be times I need to replace my Set() with a completely new Set() not based on the previous Set().
I'm stumped as to why it's not working with setValue. I can't figure out what I'm doing wrong.
Have you tried the functional update instead of setting the value directly?
setStoredValue((prev) => new Set([...valueToStore]))
Note: Here is my solution to the problem.
Ok I think I figured it out. In reactComponent.js I adjusted my code like this >
const addItemUrlIdToSet = (item) => setItemUrlIdSet(prev => new Set(prev).add(item));
to this
const addItemUrlIdToSet = (item) => setItemUrlIdSet(prev => prev.add(item));
I think whats happening is the reference to the Set() was not updating the State inside the hook. I know usually you should not directly mutate the state like this. I am unsure if there is a better way. but its working.
Edit: I also added the modification that #okapies suggested, in useLocalStorage.js
setStoredValue((prev) => new Set([...valueToStore]) );
I have the common warning displaying upon loading of my web app but never again...
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
EDIT****
It is caused by this chunk of code. I have narrowed it down to one function. It blows up when I try to setMoisture state. I am not sure why.
function getData (){
Axios.get("http://localhost:3001/api/get-value").then((response) => {
const recievedData = response.data;
const dataValue = recievedData.map((val) => {
return [val.value]
})
if (loading === true){
setLoading(false);
}
return parseInt(dataValue);
}).then((resp)=>setMoisture(resp))
}
React.useEffect(() => {
if (moisture === "initialState"){
getData();
}
}, []);
Posting the answer here (based from the comments) for completeness.
Basically, use local variables and cleanup function towards the end of useEffect(). Using this as reference:
Similar situation here
You should declare the function inside the useEffect or add it as a dependency. one way to do it's just moving your function inside the hook.
// I assumed that your useState hooks looks something similar.
const [moisture, setMoisture] = React.useState('initialState')
const [loading, setLoading] = React.useState(true)
React.useEffect(() => {
function getData() {
Axios.get("http://localhost:3001/api/get-value").then((response) => {
const recievedData = response.data;
const dataValue = recievedData.map((val) => {
return [val.value]
})
if(loading === true) {
setLoading(false);
}
return parseInt(dataValue);
}).then((resp => setMoisture(resp)))
}
if (moisture === "initialState"){
getData();
}
}, [])
You also probably want to first set your data to the state and then change your loading state to false, this is gonna prevent some bugs. This is another way to do it and manage the loading state and the promises
React.useEffect(() => {
function getData() {
setLoading(true)
Axios.get("http://localhost:3001/api/get-value")
.then((response) => {
const dataValue = response.data.map((val) => {
return [val.value]
})
// This is going to pass 0 when can't parse the data
setMoisture(parseInt(dataValue) || 0)
setLoading(false)
})
}
getData()
}, [])
I have a function that registers an effect hook, but it fails because inside the effect I need an object which at the time of running is not defined yet. Through debugging, I've noticed that the object (publicTypeIndex, in this case) is populated after the execution of the async callback.
Here is my code:
export function useNotesList() {
const publicTypeIndex: any = usePublicTypeIndex();
const [notesList, setNotesList] = React.useState<TripleDocument>();
React.useEffect(() => {
if (!publicTypeIndex) {
return;
}
(async () => {
const notesListIndex = publicTypeIndex.findSubject(solid.forClass, schema.TextDigitalDocument);
if (!notesListIndex) {
// If no notes document is listed in the public type index, create one:
const notesList = await initialiseNotesList()
if (notesList == null) {
return;
}
setNotesList(notesList);
return;
} else {
// If the public type index does list a notes document, fetch it:
const notesListUrl = notesListIndex.getRef(solid.instance);
if (typeof notesListUrl !== 'string') {
return;
}
const document = await fetchDocument(notesListUrl);
setNotesList(document);
}
})();
}, [publicTypeIndex])
return notesList;
}
The usePublicTypeIndex function was written as follows:
export async function usePublicTypeIndex() {
const [publicTypeIndex, setPublicTypeIndex] = React.useState<TripleDocument>();
React.useEffect(() => {
fetchPublicTypeIndex().then(fetchedPublicTypeIndex => {
if (fetchedPublicTypeIndex === null) {
console.log("The fetched public type index is null");
return;
}
console.log("Fetched Public Type Index: ");
console.log(fetchedPublicTypeIndex);
setPublicTypeIndex(fetchedPublicTypeIndex);
});
}, []);
return publicTypeIndex;
}
I'd like to find a way to wait for the usePublicTypeIndex() function to return before executing the publicTypeIndex.findSubject(solid.forClass, schema.TextDigitalDocument);code. What's the best way to do it?
Thank you
return in useEffect used as componentWillUnmount, and for cleaning mostly.
In your case, just move the async func into the if block
if (publicTypeIndex) {
(async () => { ... }
}
useEffect will always render at least once when the component mounts.. I ran into same issue as yours before while working on a react weather app.
i used useRef to overcome this issue:
// to stop the useEffect from rendering when the app is mounted for the first time
const initialMount = useRef(true);
useEffect(() => {
// to stop useEffect from rendering at the mounting of the app
if (initialMount.current) {
initialMount.current = false;
} else {
if (publicTypeIndex) {
// do all the stuff you have to do after your object is populated
}
}
},[publicTypeIndex]);
What you did is technically correct, there is no better way to wait for the prop since the callback will run anyway. But you could clean up the code a bit, like:
React.useEffect(() => {
if (publicTypeIndex) {
(async () => {
const notesListIndex = publicTypeIndex.findSubject(
solid.forClass,
schema.TextDigitalDocument
);
if (!notesListIndex) {
// If no notes document is listed in the public type index, create one:
const notesList = await initialiseNotesList();
if (notesList !== null) {
setNotesList(notesList);
}
} else {
// If the public type index does list a notes document, fetch it:
const notesListUrl = notesListIndex.getRef(solid.instance);
if (typeof notesListUrl === 'string') {
const document = await fetchDocument(notesListUrl);
setNotesList(document);
}
}
})();
}
}, [publicTypeIndex]);
Basically there is no point with those returns, you could easily check for required conditions and then invoke the code.
I want to push an object in my array, at every loop of a forEach,
But at every loop, it seems that my array becomes empty, so my array only have the last object pushed in,
It looks like this line from my code doesn't work :
setSeriesLikedDetails([...seriesLikedDetails, dataSerieDetail.data]);
because when I do the
console.log("seriesLikedDetails ", seriesLikedDetails);
instead of having an array of objects, I always have 1 object in the array (the last one pushed in)..
Here's a part of the code of the component :
function Likes(props) {
const [moviesLiked, setMoviesLiked] = useState([]);
const [seriesLiked, setSeriesLiked] = useState([]);
const [moviesLikedDetails, setMoviesLikedDetails] = useState([]);
const [seriesLikedDetails, setSeriesLikedDetails] = useState([]);
useEffect(() => {
loadMoviesLikedDetails();
loadSeriesLikedDetails();
}, [moviesLiked, seriesLiked]);
async function loadMoviesLikedDetails() {
setMoviesLikedDetails([]);
moviesLiked.forEach(async movie => {
try {
const dataMovieDetail = await axios.get(
`https://api.themoviedb.org/3/movie/${movie}?api_key=381e8c936f62f2ab614e9f29cad6630f&language=fr`
);
console.log("MovieDetail ", dataMovieDetail.data);
setMoviesLikedDetails(movieDetails => [
...movieDetails,
dataMovieDetail.data
]);
} catch (error) {
console.error(error);
}
});
}
async function loadSeriesLikedDetails() {
setSeriesLikedDetails([]);
seriesLiked.forEach(async serie => {
try {
const dataSerieDetail = await axios.get(
`https://api.themoviedb.org/3/tv/${serie}?api_key=381e8c936f62f2ab614e9f29cad6630f&language=fr`
);
console.log("SerieDetail ", dataSerieDetail.data);
setSeriesLikedDetails(serieDetails => [
...serieDetails,
dataSerieDetail.data
]);
} catch (error) {
console.error(error);
}
});
}
That is most likely because in the forEach callback the seriesLikedDetails is allways the same reference whereas setSeriesLikedDetails changes the actual array you want to track.
So when your on the last iteration of the forEach you just add the last value to the initial array and set it as the current array.
By doing this way instead:
async function loadSeriesLikedDetails() {
const newArray = [...seriesLikedDetails];
const promises = seriesLiked.map(async serie => {
try {
const dataSerieDetail = await axios.get(
`https://api.themoviedb.org/3/tv/${serie}?api_key=381e8c936f62f2ab614e9f29cad6630f&language=fr`
);
console.log("SerieDetail ", dataSerieDetail.data);
newArray.push(dataSerieDetail.data);
} catch (error) {
console.error(error);
}
});
Promise.all(promises).then(()=>setSeriesLikedDetails(newArray));
}
You will update the state once with the correct new value.
seriesLikedDetails is being cached here, and is only updating when the component rerenders. Because loadSeriesLikedDetails() isn't called a second time during the rerenders, your initial seriesLikedDetails remains an empty array.
Let's go over what happens internally (Your code):
Component gets rendered. useEffect()'s get fired, and run.
Axios calls are being made, and then a call to setSeriesLikedDetails() is made. seriesLikedDetails now contains one element.
Component is now rerendered. When component is rerendered, loadSeriesLikedDetails(); is NOT called as seriesLiked hasn't changed.
Axios calls are continued to be made (Called from the original render), but the current value of seriesLikedDetails remains an empty array, because this is still happening in the original render.
Avoid using Promise.all here as you may want to do sequential updates to your UI as updates come in. In this case, you can use setSeriesLikedDetails with a function, to always pull the current value to update it:
function Likes(props) {
const [moviesLiked, setMoviesLiked] = useState([]);
const [seriesLiked, setSeriesLiked] = useState([]);
const [moviesLikedDetails, setMoviesLikedDetails] = useState([]);
const [seriesLikedDetails, setSeriesLikedDetails] = useState([]);
useEffect(() => {
loadSeriesLikedDetails();
}, [seriesLiked]);
useEffect(() => {
console.log("seriesLikedDetails ", seriesLikedDetails);
}, [seriesLikedDetails]);
async function loadSeriesLikedDetails() {
seriesLiked.forEach(async serie => {
try {
const dataSerieDetail = await axios.get(
`https://api.themoviedb.org/3/tv/${serie}?api_key=381e8c936f62f2ab614e9f29cad6630f&language=fr`
);
console.log("SerieDetail ", dataSerieDetail.data);
setSeriesLikedDetails(currSeriesDetails => [...currSeriesDetails, dataSerieDetail.data]);
} catch (error) {
console.error(error);
}
});
}
It's as simple as passing a function:
setSeriesLikedDetails(currSeriesDetails => [...currSeriesDetails, dataSerieDetail.data]);
In addition, you may also wish to reset the array at the start (Or write it so you only fetch the movies/series you haven't fetched already):
async function loadSeriesLikedDetails() {
setMoviesLikedDetails([]); // Reset array
See Functional Updates apart of the docs.