In a React project, I have a state gameResults with a array of games, and I have a function to get the list of games based on a query :
useEffect(() => {
const timeoutId = setTimeout(() => {
if (gameQuery.length > 0) {
axios.get(`/api/games/${gameQuery}`).then((response) => {
const igdbGames: IGDBGame[] = response.data.games;
const formatedGames = formatGames(igdbGames);
setGameResults(formatedGames);
});
}
}, 300);
return () => clearTimeout(timeoutId);
}, [gameQuery]);
For each game, I don't have the cover, so I get the cover for each game :
const loadGamesImages = async () => {
for (let i = 0; i < gameResults.length; i++) {
axios
.get(`/api/cover/${gameResults[i].id}`)
.then((response) => {
const coverUrl: IGDBCover = response.data.covers[0];
const newGame = {
...gameResults[i],
cover: coverUrl.url.replace("//", "https://"),
};
const newGames = gameResults.filter(
(game: Game) => game.id !== newGame.id
);
setGameResults([...newGames, newGame]);
})
.catch((error) => {
console.log("error", error);
});
await sleep(300);
}
console.log("finish");
};
useEffect(() => {
loadGamesImages();
}, [gameResults.length]);
Here is my problem : when React update the state, the old state is not there anymore. I explain : for the first cover, it's ok the new state has the first game covered. But when he make a new state for the second game, as you can see i get the gameResults state, but in this one the first game has no cover anymore.
Here is the result :
What have I done wrong ?
Each one of your looped asynchronous calls closes over the initial binding of the stateful gameResults - and gameResults starts out empty. For example, with the first Promise that resolves, these line:
const newGames = gameResults.filter(
(game: Game) => game.id !== newGame.id
);
setGameResults([...newGames, newGame]);
have the gameResults refer to the empty array, so setGameResults properly spreads the empty array plus the just-added newGame.
But then on further Promise resolutions, they also close over the initially-empty gameResults - all the async calls happened before the component re-rendered.
Use a callback instead, so that the async calls don't overwrite each other:
setGameResults((gameResults) => {
const newGames = gameResults.filter(
(game) => game.id !== newGame.id
);
return [...newGames, newGame];
});
(also note that there's no need to explicitly note the type of a parameter that TS can already infer automatically: (game: Game) can be just game)
Once this is working, I'd also suggest tweaking your code so that, when the effect hook runs again, only covers that have not been retrieved yet get requested again. This'll save you from unnecessarily making duplicate requests.
Related
please I'm solving one problem (just learning purposes). I'm using useState() hook and then, after some timeout I want add next items into array from remote fetch.
My code snippet look like:
const [tasks, setTasks] = useState([]);
const url = 'https://pokeapi.co/api/v2/pokemon?offset=0&limit=5';
// asnynchronous call. Yes, it's possible to use axios as well
const fetchData = async () => {
try {
let tasksArray = [];
await fetch(url)
.then((response) => response.json())
.then((data) => {
data.results.map((task, index) => {
// first letter uppercase
const taskName = task.name.charAt(0).toUpperCase() + task.name.slice(1);
tasksArray.push({ id: index, name: taskName });
});
});
console.log('Added tasks:' + tasks.length);
setTasks(_.isEmpty(tasks) ? tasksArray : [...tasks, tasksArray]);
} catch (error) {
console.log('error', error);
}
};
useEffect(() => {
// Add additional example tasks from api after 5 seconds with
// fetch fetchData promise
setTimeout(fetchData, 5000);
}, []);
Code works fine with useEffect() hook. But in async function my array is empty when I add some tasks within five seconds and it will be replaced by fetched data and one empty
I added Butter and Milk within 5 seconds to my Shopping list
But after timeout my tasks array will be replaced by remote fetched data.
And as you can see, tasks array lenght is 0 (like console.log() says)
Please, can you exmplain me, why my tasks array is empty if there exists 2 items before 5 seconds.
Of course, I'm adding my tasks to the list normally after hit Enter and handleSubmit
const handleSubmit = (e) => {
e.preventDefault();
//creating new task
setTasks([
{
id: [...tasks].length === 0 ? 0 : Math.max(...tasks.map((task) => task.id)) + 1,
name: newTask,
isFinished: false,
},
...tasks,
]);
setNewTask('');
}
Thanks for help or explain. It the problem that useEffect is called after rendering? Of this causing async behavior?
I could not understand your code fully correctly but my guess is
the fetchData function you have declared might refer to the tasks at the time of declaration.
so every time you call fetchData you might not see the changed tasks state...
if this is the case try using useCallback with the dependency tasks...
what useCallback does is it stores the function in memory and if smth in dependency changes the function's logic changes to dependencies you declared.
If you have used eslint, calling a function inside useEffect will give you error similar to below
The ‘fetchOrg’ function makes the dependencies of useEffect Hook (at line 6) change on every render. Move it inside the useEffect callback. Alternatively, wrap the ‘fetchOrg’ definition into its own useCallback() Hook
Your code is confusing. You can place everything inside an useEffect, and I believe the thing you are trying to achieve is a long poll query. (for that you use setInterval() and you have to clear the function in useEffect
my solution for you:
const [tasks, setTasks] = useState([]);
const url = 'https://pokeapi.co/api/v2/pokemon?offset=0&limit=5';
const request = {method: GET, Headers: {"Content-type":"application/json"}}
useEffect(() => {
function fetchData(){
fetch(url, request).then(res => res.json().then(data => {
data.results.map((task, index) => {
const taskName = task.name.charAt(0).toUpperCase() + task.name.slice(1);
setTasks((prevState) => ([...prevState,{ id: index, name: taskName }]));
});
})
}
const interval = setInterval(() => {
fetchData();
}, 5000)
return () => clearInterval(interval)
}, []);
please do not forget two things:
This approach is only good when you have 5 to 10 simultaneous clients connected since it is not performance effective on the backend, I would recommend instead an alternative based on realtime communication (with the listening for new events.
please do not forget to specify the {method, headers, body) in the fetch function
I want to display a mapped list where "UserName" is an entry value from a Firebase Realtime Database corresponding to the author of each entry.
The following code, inside the get(UsernameRef).then((snapshot) =>{}) scope, returns an undefined reference error as expected, 'UserName' is assigned a value but never used and 'UserName' is not defined
const [RecipeLibrary, setRecipeLibrary] = React.useState([]);
React.useEffect(() => {
const RecipeLibraryRef = ref(db, "Recipes/");
onValue(RecipeLibraryRef, (snapshot) => {
const RecipeLibrary = [];
snapshot.forEach((child) => {
const AuthorUserId = child.key;
child.forEach((grandChild) => {
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
get(UserNameRef).then((snapshot) => {
const UserName = snapshot.val();
});
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
...grandChild.val(),
});
});
});
setRecipeLibrary(RecipeLibrary);
console.log({ RecipeLibrary });
});
}, []);
I've tried:
Using a React state to pass the variable -> Can't use inside React useEffect
Exporting and Importing a separate function that returns the desired UserName -> return can only be used in the inner scope
Moving the list .push inside the Firebase get scope -> React.useState can no longer access the list
I'm hoping there is a simple solution here, as I am new.
Your time and suggestions would mean a lot, thank you!
Update:
I got the RecipeLibrary array to contain the desired "UserName" entry, named author by moving the array .push inside the .then scope. Here is a log of that array at set (line 59) and at re-render (line 104).
child.forEach((grandChild) => {
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
get(UserNameRef).then((snapshot) => {
const UserName = snapshot.val();
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
authorId: AuthorUserId,
...grandChild.val(),
});
});
});
});
setRecipeLibrary(RecipeLibrary);
console.log(RecipeLibrary);
However, now the mapped list is not rendering at all on screen.
Just some added context with minimal changes to original code, been stuck on this so long that I'm considering a full re-write at this point to jog my memory. Oh and here is the bit that renders the mapped list in case:
<Box width="75%" maxHeight="82vh" overflow="auto">
{RecipeLibrary.map((RecipeLibrary) => (
<Paper
key={RecipeLibrary.name}
elevation={3}
sx={{
etc...
This is a tricky one - the plainest option might be to move push() and setRecipeLibrary() inside the then() callback so they're all within the same scope, but that would have some terrible side effects (for example, triggering a re-render for every recipe retrieved).
The goal (which you've done your best to achieve) should be to wait for all the recipes to be loaded first, and then use setRecipeLibrary() to set the full list to the state. Assuming that get() returns a Promise, one way to do this is with await in an async function:
const [RecipeLibrary, setRecipeLibrary] = React.useState([]);
React.useEffect(() => {
const RecipeLibraryRef = ref(db, "Recipes/");
onValue(RecipeLibraryRef, (snapshot) => {
// An async function can't directly be passed to useEffect(), and
// probably can't be accepted by onValue() without modification,
// so we have to define/call it internally.
const loadRecipes = async () => {
const RecipeLibrary = [];
// We can't use an async function directly in forEach, so
// we instead map() the results into a series of Promises
// and await them all.
await Promise.all(snapshot.docs.map(async (child) => {
const AuthorUserId = child.key;
// Moved out of the grandChild loop, because it never changes for a child
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
// Here's another key part, we await the Promise instead of using .then()
const userNameSnapshot = await get(UserNameRef);
const UserName = userNameSnapshot.val();
child.forEach((grandChild) => {
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
...grandChild.val(),
});
});
}));
setRecipeLibrary(RecipeLibrary);
console.log({ RecipeLibrary });
};
loadRecipes();
});
}, []);
Keep in mind that Promise.all() isn't strictly necessary here. If its usage makes this less readable to you, you could instead execute the grandChild processing in a plain for loop (not a forEach), allowing you to use await without mapping the results since it wouldn't be in a callback function.
If snapshot.docs isn't available but you can still use snapshot.forEach(), then you can convert the Firebase object to an Array similar to Convert A Firebase Database Snapshot/Collection To An Array In Javascript:
// [...]
// Change this line to convert snapshot
// await Promise.all(snapshot.docs.map(async (child) => {
await Promise.all(snapshotToSnapshotArray(snapshot).map(async (child) => {
// [...]
// Define this somewhere visible
function snapshotToSnapshotArray(snapshot) {
var returnArr = [];
snapshot.forEach(function(childSnapshot) {
returnArr.push(childSnapshot);
});
return returnArr;
}
Note that if get() somehow doesn't return a Promise...I fear the solution will be something less straightforward.
I'm quite new to the React-TS world and I have recently been playing with useState and useEffect hooks only basically.
I have the following functional component inside which I'd like to fetch N items the first time and then start a periodic function that fetches the last item from the response data, updating the current state.
const fetcher = async (url: string) => await axios.get(url).then((res: AxiosResponse) => res.data);
type AirflowData = {
value: number; // perc values from 0 to 1
timestamp: number; // UTC time
};
const ActionDetector: React.FC = () => {
const [alerts, setAlerts] = useState<AirflowData[]>([]);
useEffect(() => {
// Fetch the latest N alerts first
getAlerts(100);
// Then start fetching the last alert every N milliseconds
const interval = setInterval(() => getLatestAlert(), 1000);
// Clear interval
return () => {
clearInterval(interval);
};
}, []);
/**
* Return the alert data after fetching it.
* #param numAlerts number of the last N alerts to return
*/
const getAlerts = async (numAlerts: number) => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
setAlerts(fetchedAlerts.slice(-numAlerts));
};
/**
* Return the latest alert data available.
*/
const getLatestAlert = async () => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
const latestFetchedAlert = fetchedAlerts.slice(-1)[0];
const latestAlert = alerts.slice(-1)[0];
if (latestFetchedAlert && latestAlert && latestFetchedAlert.timestamp !== latestAlert.timestamp) {
// Append the alert only if different from the previous one
setAlerts([...alerts, latestFetchedAlert]);
}
};
console.log(alerts);
return <></>
}
export default ActionDetector
The problem with this approach is that latestAlert is always undefined and that is due, if I understood how React works under the hood correctly, to the initial state change re-rendering trigger. After getAlerts() is called and fires setAlerts(...), the component starts the re-rendering and so, since getLatestAlert() is called inside the useEffect only the first time (the first render), it always read alerts as the initialized empty array.
I don't know if this is the correct reason behind this, but how can I achieve what I'm trying to do the right way?
The fundamental issue is that when updating state based on existing state, you need to be sure you have the latest state information. Your getLatestAlerts function closes over the alerts constant that was in scope when it was created, so it only ever uses that version of the constant (not the updated one from a subsequent render). Your useEffect setInterval callback closes over the getLatestAlerts function that was in scope when it was created, and only ever uses that version of the function.
To be sure you have the latest state, use the callback version of the state setter instead of the constant:
const getLatestAlert = async () => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
const latestFetchedAlert = fetchedAlerts.slice(-1)[0];
if (latestFetchedAlert) {
setAlerts(alerts => {
const latestAlert = alerts.slice(-1)[0];
if (latestFetchedAlert && latestAlert && latestFetchedAlert.timestamp !== latestAlert.timestamp) {
// Append the alert only if different from the previous one
alerts = [...alerts, latestFetchedAlert];
}
return alerts;
});
}
};
Purely as a side note, I wouldn't use the idiom you seem to be using to get the last item from an array, array.slice(-1)[0]. Instead, I'd either use array[array.length - 1], or use the at method which just achieved Stage 4 and will be in this year's spec (it's easily polyfilled for older environments).
I couldn't find a similar question here, so here it goes:
I created a custom hook useBudget to fetch some data.
const initalState = {
budget_amount: 0,
};
const useBudget = (resource: string, type: string) => {
const [budgetInfo, setBudget] = useState(initalState);
useEffect(
() => {
(async (resource, type) => {
const response = await fetchBudgetInfo(resource, type);
setBudget(response);
})(resource, type);
}, []);
return [budgetInfo];
};
And on the component that uses that hook, I have something like this:
const [budgetInfo] = useBudget(resource, type);
const [budgetForm, setBudgetForm] = useState({ warningMsg: null, errorMsg: null, budget: budgetInfo.budget_amount });
The problem is: The initial state of this component does not update after the fetching. budget renders with 0 initially and keeps that way. If console.log(budgetInfo) right afterwards, the budget is there updated, but the state is not.
I believe that this is happening due to the asynchronicity right? But how to fix this?
Thanks!
I could get to a fix, however, I am not 100% that this is the best/correct approach. As far as I could get it, due to the asynchronicity, I am still reading the old state value, and a way to fix this would be to set the state inside useEffect. I would have:
const [budgetInfo] = useBudget(resource, type);
const [appState, setAppState] = useState({ budget: budgetInfo.budget_amount });
useEffect(
() => {
setAppState({ budget: budgetInfo.budget_amount });
}, [budgetInfo]);
But it's working now!
Working example: https://stackblitz.com/edit/react-wiewan?file=index.js
Effects scheduled with useEffect don’t block the browser from updating the screen - that's why 0 (initialState) is displayed on the screen. After the value is fetched, the component stays the same as there is no change in its own state (budgetForm).
Your solution updates component's state once budgetInfo is fetched hence triggering a re-render, which works but seems to be rather a workaround.
useBudget could be used on its own:
const useBudget = (resource, type) => {
const [budgetInfo, setBudget] = useState(initalState);
const fetchBudgetInfo = async () => {
const response = await (new Promise((resolve) => resolve({ budget_amount: 333 })))
setBudget(response)
}
useEffect(() => {
fetchBudgetInfo(resource, type)
}, [])
return budgetInfo
};
const App = props => {
const { budget_amount } = useBudget('ok', 'ok')
return (
<h1>{budget_amount}</h1>
);
}
fetchBudgetInfo is split out since if we make effect function async (useEffect(async () => { ... })), it will be implicitly returning a promise and this goes against its design - the only return value must be a function which is gonna be used for cleaning up. Docs ref
Alternatively, consider retrieving data without a custom hook:
const fetchBudgetInfo = async (resource, type) => {
const response = await fetch(resource, type)
return response
}
useEffect(() => {
const response = fetchBudgetInfo(resource, type)
setAppState((prevState) => { ...prevState, budget: response.budget_amount });
}, []);
Notably, we are manually merging old state with the new value, which is necessary if appState contains several values - that's because useState doesn't shallowly merge objects (as this.setState would) but instead completely replaces state variable. Doc ref
On a somewhat related note, there is nothing wrong with using object to hold state, however using multiple state variables offers few advantages - more precise naming and the ability to do individual updates.
I cannot wrap my head around the issue below.
The issue relates to the asynchronous setState dimension. Usually I use the callback, but doesn't seem appropriate here.
My goal is to create a state (that I will be able to sort) which is obtained by iterating on different states which are themselves created in a map.
The function below calls my different methods, the ones we're interested in are the 2 last ones. getUserPoints and sortArrayforUserRank.
getPlayersByUser = () => {
database
.ref(`pools/${this.state.selectedValue}`)
.once("value")
.then(data => {
for (let item in data.val()) {
this.setState({
users: this.state.users.concat([item])
});
this.setState({ [item]: data.val()[item] });
}
})
.then(this.makePlayersArray)
.then(this.getUserPoints)
.then(this.sortArrayforUserRank);
getUserPoints = () => {
this.state.users.map(user => {
// Create the dynamic name of the state, 1 for each user
let userPoints = `${user}points`;
// initializing the state for userPoint to be at 0 for future calculation
this.setState({ [userPoints]: 0 });
this.state[user].map(player => {
database
.ref(`players/${player}`)
.child("points")
.once("value")
.then(data => {
let points = parseInt(data.val());
this.setState(state => ({
[userPoints]: points + state[userPoints]
}));
});
});
});
The getUserPoints allow me to dynamically create the state.userPoints summing all the points from the players for each user.
Then I was expecting the sortArrayforUserRank below to use the updated state.userPoints to create my final userArrayPoints state.
sortArrayforUserRank = () => {
this.state.users.map(user => {
let userPoints = `${user}points`;
this.setState(state => ({
userArrayPoints: state.userArrayPoints.concat([
{ [user]: state[userPoints] }
])
}));
});
Currently the userArrayPoints gets populated with 4 objects {[user]:0} instead of the final sum of points for each user. The issue there is that sortArrayforUserRank gets called before the previous setState are done
I would have loved to use the setState callback in getUserPoints but since I'm in the player map function it will get called for each player whereas I want to handle it at the user lvl to have the final sum of points.
I tried to use componentDidUpdate, and made sur to use functionnal setState as per those articles but couldn't figure it out.
https://medium.com/#shopsifter/using-a-function-in-setstate-instead-of-an-object-1f5cfd6e55d1
https://medium.freecodecamp.org/functional-setstate-is-the-future-of-react-374f30401b6b
Your help will be glady appreciated,
thanks
You can't do what you are trying here with setState because it is asynchronous and will conflict with the different states available on each iteration in that for () loop.
What you can do is extract the state first, manipulate it as needed, then run setState (at least in this one below)
.then(data => {
// Pull out what you want to mess with here first
const users = [ ...this.state.users ];
const dataValObj = data.val();
// Then use spread operator (or Object.assign/Array.concat)
this.setState({
users: [
...users,
...Object.keys(dataValObj)
],
...dataValObj
});
})
And it seems you followed a similar pattern throughout the code. Try and apply what I've done here to the other areas that are using loops with setState inside them.
found one way to do it using promise all below. It allows me to remove the setState from the loop and directly work on it instead of have to rely on 2 setstate. Both comments/answer helped me to process that,
getUserPoints = () => {
this.state.users.map(user => {
// Create the dynamic name of the state, 1 for each user
let userPoints = `${user}points`;
// initializing the state for userPoint to be at 0 for future calculation
this.setState({ [userPoints]: 0 });
let userCounter = 0;
let promiseArrays = [];
this.state[user].map(player => {
let promise = database
.ref(`players/${player}`)
.child("points")
.once("value")
.then(data => {
let points = parseInt(data.val());
return (userCounter = userCounter + points);
});
promiseArrays.push(promise);
});
Promise.all(promiseArrays).then(() =>
this.setState({
userArrayPoints: this.state.userArrayPoints.concat({
[user]: userCounter
})
})
);
});
};
Just use second argument in this.setState.
Second argument is a function that will be called after seting State.
this.setState({
name:value
},() => {this.nameOfTheFunctionYouWantToRunNext() });