useEffect wont work on conditional change when still in execution of async function - reactjs

On a page load I have two useEffects. Both are executing at load, where the first one can possibly set a state, that should trigger the second useEffect one further time. But actually it won't.
Actually it should trigger, as it executes in two cases: When i change the order of these useEffects (could be a solution, but why???), or when i comment out the void getOrPostOnWishlist();, thus when removing the async call from the useEffect. But why is that a problem here?
Here some example code snippet with some comments:
...
const setItemIdToBeHandled = (itemId: number | undefined) =>
setState((prevState) => ({...prevState, itemIdToBeHandled: itemId}));
...
// async, called on second useEffect
const getOrPostOnWishlist = async () => {
if (state.itemIdToBeHandled) {
// if there is an item to be handled, retrieve new wishlist with added item
await addItemToNewWishlist(state.itemIdToBeHandled);
} else if (!state.wishlist) {
// if no wishlist is locally present, check if wishlist exists on api
await checkForPresentWishlist();
}
};
// possibly setting state
React.useEffect(() => {
const urlItemId = UrlSearchParamsHelper.wishlistItemId;
if (urlItemId) {
console.log("found item id in url:", urlItemId);
setItemIdToBeHandled(urlItemId);
}
}, []);
// on state change, but also on load
React.useEffect(() => {
console.log("condition:", state.itemIdToBeHandled); // sticks on 'undefined'
void getOrPostOnWishlist(); // when commented out, above console will show 'undefined', and then an itemId (considering the first useEffect sets the state);
}, [state.itemIdToBeHandled]);
This led to the following output:
But when just commenting out the async call in the second useEffect, this led to:
Googled around, and also tried useCallback, but that didn't work. Doesn't seem to be the issue here, since it's somewhat not about the content of the called function, but about the very fact, that the calling useEffect is not even executed.
It feels like even without await inside the useEffect, a useEffect is still blocked, when it has executed an async function.
Or am i missing something? If some more details are needed, let me know

Related

The setItem function is working fine in Reactjs but localStorage.getItem is not working

In the code I change some recipe content and it saves but when I refresh my page it resets everything and the changes are gone
Here is my code
const LOCAL_STORAGE_KEY = "cookingWithKyle.recipes"
function App() {
const [selectedRecipeId, setSelectedRecipeId] = useState()
const [recipes, setRecipes] = useState(sampleRecipe)
const selectedRecipe = recipes.find(
(recipe) => recipe.id === selectedRecipeId
)
useEffect(() => {
const recipeJSON = localStorage.getItem(LOCAL_STORAGE_KEY)
if (recipeJSON) setRecipes(JSON.parse(recipeJSON))
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(recipes))
}, [recipes])
AFAIK, in React useEffect hook's callback is run asynchronously. So, you cannot control which callback is processed first and end before another.
But here, the main reason of your issue is, even if you have recipes as a dependency in your second useEffect, second useEffect's callback will also be fired on initial mount (on page reload), thus in your code you set a value (probably undefined) to your localStorage and then try to get a value, which was already gone.
So, try to run your second useEffect's callback with a condition:
useEffect(() => {
if (recipes) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(recipes))
}
}, [recipes])
For information, React fires the useEffect hooks callback when it compares dependency list in an array with a previous render (so, try not to use a value of object type as a dependency, otherwise your callback will be fired in every render) and if it sees the difference, it fires useEffect's callback, including the initial render.

Infinite re-render in functional react component

I am trying to set the state of a variable "workspace", but when I console log the data I get an infinite loop. I am calling the axios "get" function inside of useEffect(), and console logging outside of this loop, so I don't know what is triggering all the re-renders. I have not found an answer to my specific problem in this question. Here's my code:
function WorkspaceDynamic({ match }) {
const [proposals, setProposals] = useState([{}]);
useEffect(() => {
getItems();
});
const getItems = async () => {
const proposalsList = await axios.get(
"http://localhost:5000/api/proposals"
);
setProposals(proposalsList.data);
};
const [workspace, setWorkspace] = useState({});
function findWorkspace() {
proposals.map((workspace) => {
if (workspace._id === match.params.id) {
setWorkspace(workspace);
}
});
}
Does anyone see what might be causing the re-render? Thanks!
The effect hook runs every render cycle, and one without a dependency array will execute its callback every render cycle. If the effect callback updates state, i.e. proposals, then another render cycle is enqueued, thus creating render looping.
If you want to only run effect once when the component mounts then use an empty dependency array.
useEffect(() => {
getItems();
}, []);
If you want it to only run at certain time, like if the match param updates, then include a dependency in the array.
useEffect(() => {
getItems();
}, [match]);
Your use of useEffect is not correct. If you do not include a dependency array, it gets called every time the component renders. As a result your useEffect is called which causes setProposals then it again causes useEffect to run and so on
try this
useEffect(() => {
getItems();
} , []); // an empty array means it will be called once only
I think it's the following: useEffect should have a second param [] to make sure it's executed only once. that is:
useEffect(() => {
getItems();
}, []);
otherwise setProposal will modify the state which will trigger a re-render, which will call useEffect, which will make the async call, which will setProposal, ...

Infinite Loop with useEffect - ReactJS

I have a problem when using the useEffect hook, it is generating an infinite loop.
I have a list that is loaded as soon as the page is assembled and should also be updated when a new record is found in "developers" state.
See the code:
const [developers, setDevelopers] = useState<DevelopersData[]>([]);
const getDevelopers = async () => {
await api.get('/developers').then(response => {
setDevelopers(response.data);
});
};
// This way, the loop does not happen
useEffect(() => {
getDevelopers();
}, []);
// This way, infinte loop
useEffect(() => {
getDevelopers();
}, [developers]);
console.log(developers)
If I remove the developer dependency on the second parameter of useEffect, the loop does not happen, however, the list is not updated when a new record is found. If I insert "developers" in the second parameter of useEffect, the list is updated automatically, however, it goes into an infinite loop.
What am I doing wrong?
complete code (with component): https://gist.github.com/fredarend/c571d2b2fd88c734997a757bac6ab766
Print:
The dependencies for useEffect use reference equality, not deep equality. (If you need deep equality comparison for some reason, take a look at use-deep-compare-effect.)
The API call always returns a new array object, so its reference/identity is not the same as it was earlier, triggering useEffect to fire the effect again, etc.
Given that nothing else ever calls setDevelopers, i.e. there's no way for developers to change unless it was from the API call triggered by the effect, there's really no actual need to have developers as a dependency to useEffect; you can just have an empty array as deps: useEffect(() => ..., []). The effect will only be called exactly once.
EDIT: Following the comment clarification,
I register a developer in the form on the left [...] I would like the list to be updated as soon as a new dev is registered.
This is one way to do things:
The idea here is that developers is only ever automatically loaded on component mount. When the user adds a new developer via the AddDeveloperForm, we opportunistically update the local developers state while we're posting the new developer to the backend. Whether or not posting fails, we reload the list from the backend to ensure we have the freshest real state.
const DevList: React.FC = () => {
const [developers, setDevelopers] = useState<DevelopersData[]>([]);
const getDevelopers = useCallback(async () => {
await api.get("/developers").then((response) => {
setDevelopers(response.data);
});
}, [setDevelopers]);
useEffect(() => {
getDevelopers();
}, [getDevelopers]);
const onAddDeveloper = useCallback(
async (newDeveloper) => {
const newDevelopers = developers.concat([newDeveloper]);
setDevelopers(newDevelopers);
try {
await postNewDeveloperToAPI(newDeveloper); // TODO: Implement me
} catch (e) {
alert("Oops, failed posting developer information...");
}
getDevelopers();
},
[developers],
);
return (
<>
<AddDeveloperForm onAddDeveloper={onAddDeveloper} />
<DeveloperList developers={developers} />
</>
);
};
The problem is that your getDevelopers function, calls your setDevelopers function, which updates your developers variable. When your developers variable is updated, it triggers the useEffect function
useEffect(() => {
getDevelopers();
}, [developers]);
because developers is one of the dependencies passed to it and the process starts over.
Every time a variable within the array, which is passed as the second argument to useEffect, gets updated, the useEffect function gets triggered
Use an empty array [] in the second parameter of the useEffect.
This causes the code inside to run only on mount of the parent component.
useEffect(() => {
getDevelopers();
}, []);

Why can't useEffect access my state variable in a return statement?

I don't understand why my useEffect() React function can't access my Component's state variable. I'm trying to create a log when a user abandons creating a listing in our app and navigates to another page. I'm using the useEffect() return method of replicating the componentWillUnmount() lifecycle method. Can you help?
Code Sample
let[progress, setProgress] = React.useState(0)
... user starts building their listing, causing progress to increment ...
console.log(`progress outside useEffect: ${progress}`)
useEffect(() => {
return () => logAbandonListing()
}, [])
const logAbandonListing = () => {
console.log(`progress inside: ${progress}`)
if (progress > 0) {
addToLog(userId)
}
}
Expected Behavior
The code would reach addToLog(), causing this behavior to be logged.
Observed Behavior
This is what happens when a user types something into their listing, causing progress to increment, and then leaves the page.
The useEffect() method works perfectly, and fires the logAbandonListing() function
The first console.log() (above useEffect) logs something greater than 0 for the progress state
The second console.log() logs 0 for the progress state, disabling the code to return true for the if statement and reach the addToLog() function.
Environment
Local dev environment of an app built with Next.js running in Firefox 76.0.1
nextjs v 8.1.0
react v 16.8.6
I'd really appreciate some help understanding what's going on here. Thanks.
I think it is a typical stale closure problem. And it is hard to understand at first.
With the empty dependency array the useEffect will be run only once. And it will access the state from that one run. So it will have a reference from the logAbandonListing function from this moment. This function will access the state from this moment also. You can resolve the problem more than one way.
One of them is to add the state variable to your dependency.
useEffect(() => {
return () => logAbandonListing()
}, [progress])
Another solution is that you set the state value to a ref. And the reference of the ref is not changing, so you will always see the freshest value.
let[progress, setProgress] = React.useState(0);
const progressRef = React.createRef();
progressRef.current = progress;
...
const logAbandonListing = () => {
console.log(`progress inside: ${progressRef.current}`)
if (progressRef.current > 0) {
addToLog(userId)
}
}
If userId is changing too, then you should add it to the dependency or a reference.
To do something in the state's current value in the useEffect's return function where the useEffects dependencies are am empty array [], you could use useReducer. This way you can avoid the stale closure issue and update the state from the useReducer's dispatch function.
Example would be:
import React, { useEffect, useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "set":
return action.payload;
case "unMount":
console.log("This note has been closed: " + state); // This note has been closed: 201
break;
default:
throw new Error();
}
}
function NoteEditor({ initialNoteId }) {
const [noteId, dispatch] = useReducer(reducer, initialNoteId);
useEffect(function logBeforeUnMount() {
return () => dispatch({ type: "unMount" });
}, []);
return <div>{noteId}</div>;
}
export default NoteEditor;
More info on this answer
When you return a function from useEffect, it behaves like componentWillUnmount so I think it only runs while cleaning up. You'd need to actually call logAbandonListing like:
useEffect(() => {
logAbandonListing();
}, []);
So it runs everytime a component re-renders. You can read more about useEffect on https://reactjs.org/docs/hooks-effect.html
It's written excellently.
I tried using this sandbox to explain my answer.
Basically you are returning a function from your useEffect Callback. But that returned function is never really invoked so it does no actually execute and thus log the abandon action. If you look at the Code in the sandbox I have added a wrapper Parens and () afterwards to actually cause the method to be invoked leading to console.log executing.

Graphql subscriptions inside a useEffect hook doesn't access latest state

I'm building a basic Slack clone. So I have a "Room", which has multiple "Channels". A user subscribes to all messages in a Room, but we only add them to the current message list if the new message is part of the user's current Channel
const [currentChannel, setCurrentChannel] = useState(null);
const doSomething = (thing) => {
console.log(thing, currentChannel)
}
useEffect(() => {
// ... Here I have a call which will grab some data and set the currentChannel
Service.joinRoom(roomId).subscribe({
next: (x) => {
doSomething(x)
},
error: (err: any) => { console.log("error: ", err) }
})
}, [])
I'm only showing some of the code here to illustrate my issue. The subscription gets created before currentChannel gets updated, which is fine, because we want to listen to everything, but then conditionally render based on currentChannel.
The issue I'm having, is that even though currentChannel gets set correctly, because it was null when the next: function was defined in the useEffect hook, doSomething will always log that currentChannel is null. I know it's getting set correctly because I'm displaying it on my screen in the render. So why does doSomething get scoped in a way that currentChannel is null? How can I get it to call a new function each time that accesses the freshest state of currentChannel each time the next function is called? I tried it with both useState, as well as storing/retrieving it from redux, nothing is working.
Actually it is related to all async actions involving javascript closures: your subscribe refers to initial doSomething(it's recreated on each render) that refers to initial currentChannel value. Article with good examples for reference: https://dmitripavlutin.com/react-hooks-stale-closures/
What can we do? I see at least 2 moves here: quick-n-dirty and fundamental.
We can utilize that useState returns exact the same(referentially same) setter function each time and it allows us to use functional version:
const doSomething = (thing) => {
setCurrentChannel(currentChannelFromFunctionalSetter => {
console.log(thing, currentChannelFromFunctionalSetter);
return currentChannelFromFunctionalSetter;
}
}
Fundamental approach is to utilize useRef and put most recent doSomething there:
const latestDoSomething = useRef(null);
...
const doSomething = (thing) => { // nothing changed here
console.log(thing, currentChannel)
}
latestDoSomething.current = doSomething; // happens on each render
useEffect(() => {
Service.joinRoom(roomId).subscribe({
next: (x) => {
// we are using latest version with closure on most recent data
latestDoSomething.current(x)
},

Resources