React hooks - dependencies rerun hooks - reactjs

I keep struggling with the same issue with React Hooks. The dependency array.
I have many hooks that should trigger and handle different events.
For instance:
useEffect(() => {
const doSomethingWith = (notification: Notifications.Notification) => {
...
setUser({ notifications: badgeCount });
};
notificationListener.current = Notifications.addNotificationReceivedListener(
(notification) => {
if (user) {
doSomethingWith(notification);
}
}
);
return () => {
Notifications.removeNotificationSubscription(notificationListener);
};
}, [setExpoPushToken, setUser, user]);
In this simplified code, you can see that I track notifications and when I get it, I use the data in the notification and update the user object with it.
However, this whole thing will run whenever the user is updated. Like, if I update the user first name - the notification hook will run. That makes little sense to me. It forces me to add if statements inside these hook functions which is a waste and makes my code ugly.
What am I missing? How do I handle this better?

Related

What is the best way to execute a function after TWO or more async functions finish in React?

I have two functions that run asynchronously getting data from the API. Both of them are called from their own useEffect().
I have a third function that needs to run once those two functions have been fully completed.
How can this be accomplished?
Edit:
Both of the async functions look like this:
useEffect(() => {
fetchBudgetBucketsData();
}, [fiscalYear]);
useEffect(() => {
fetchBudgetBucketsData();
}, [fiscalYear]);
const fetchBudgetsData = async () => {
setIsFetchingBudgets(true);
const res = await getBudgets(orgID, `${parseInt(fiscalYear)}`, '', budgetType);
setIsFetchingBudgets(false);
if (isErrorResponse(res)) {
console.warn(res.details);
message.error(res.displayText);
return;
}
setBudgets(res.budgets);
};
const fetchBudgetBucketsData = async () => {
setIsLoadingBudgetBuckets(true);
if (orgID === undefined) {
return;
}
const res = await getBudgetBuckets(orgID, fiscalYear);
setIsLoadingBudgetBuckets(false);
if (isErrorResponse(res)) {
console.warn(res.details);
message.error(res.displayText);
return;
}
setBudgetBuckets(res.buckets);
};
Whenever the budget data or bucket data is updated, I want to call another function that checks for errors. However when the page loads, I need it to wait for both of those functions to be finished before it checks for errors.
Edit #2:
After some debugging, it looks like the issue might have to do with when React updates the state. Since I am trying to check for errors in data saved in the state.
One way could be chaining Promises.
Promise.all([ApiCall1, ApiCall2])
// At this point two promises above will be resolved
.then(() => ApiCall3)
Read more
I discovered the issue was caused by how React chooses when to update the state and not how I was calling these functions asynchronously.
I was able to call my Error check function by hooking it into the output of the data fetch calls. This makes sure that the error check only runs when either the budgets or buckets are edited and finished being changed.
useEffect(() => {
getWarningsAndErrors();
}, [budgets, budgetBuckets]) //Update errors whenever we edit budgets or buckets

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();
}, []);

Test useSelector value change post-mount

I have a React component which displays a message when a user goes offline and then another message when the user goes back online. The online/offline status is handled by another part of the codebase and the values are stored on the Redux store.
The component uses the useSelector hook to get the offline status like this:
const isOffline = useSelector(deviceSelectors.isOffline);
and I am using a useEffect hook in order to update the status and display the different DOM like this:
useEffect(() => {
if (isOffline) {
setUserHasBeenOffline(true);
if (userWentOnline) {
setUserWentOnline(false);
}
}
if (userHasBeenOffline && !isOffline) {
setUserWentOnline(true);
setUserHasBeenOffline(false);
}
}, [isOffline, userHasBeenOffline, userWentOnline]);
Now I am trying to test the codepath when the user has been offline and then they come back online.
I've got a perfectly working test like this for the initial codepath:
const setupMount = setupWithProvider({
render: () => <NetworkStatus />,
[[deviceSelectors.isOffline, false]],
initialState: {
device: {
offline: false,
},
},
});
it('should render the network status alert for when the user has gone offline', () => {
const { wrapper } = setupMount({
selectors: [[deviceSelectors.isOffline, true]],
});
expect(wrapper.find('...').exists()).toBeTruthy();
});
My question is, how can I update the selector's value after mount? It doesn't seem that this information is easily accessible on the internet, as I've looked a lot.
What I've tried:
I've tried destructuring dispatch too and dispatch the action I need to update the selector's return value indirectly, but setupWithProvider's dispatch is a mock function. And I couldn't find a working way with this.
I checked if the property was available to the wrapper as a prop to update it, but it was not.

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)
},

Authentication listeners when refactoring to React hooks

I'm having a little trouble figuring out how to change my authentication handling component when refactoring from a React class to React hooks.
Here's the relavant code in my class:
state = {
user: null
}
componentDidMount() {
authGetUser(user => {
if (user !== this.state.user) {
this.setState({user})
}
})
}
componentWillUnmount() {
authUnsubscribe()
}
handleAuthClick = () => {
if (this.state.user) {
authSignOut()
} else {
authSignIn()
}
}
And here it is with hooks:
const [user, setUser] = useState<firebase.User | null>(null)
useEffect(() => {
return authUnsubscribe() // runs on mount and unmount only
}, [])
useEffect(() => {
authGetUser(usr => setUser(usr))
}, [])
const handleAuthClick = () => {
if (user) {
authSignOut()
} else {
authSignIn()
}
}
Also, here are my other relevant methods:
const authGetUser = (callback: (user: firebase.User | null) => void) => {
initFirebase()
authUnsubscribe()
userUnsubscribe = firebaseAuth.onAuthStateChanged(callback)
}
export const authUnsubscribe = () => {
if (userUnsubscribe) {
userUnsubscribe()
}
}
const authSignIn = () => {
googleAuth.signIn().then((googleUser: any) => {
var credential = firebase.auth.GoogleAuthProvider.credential(googleUser.getAuthResponse().id_token)
firebaseAuth.signInAndRetrieveDataWithCredential(credential)
})
}
const authSignOut = () => {
googleAuth
.signOut()
.then(firebaseAuth.signOut())
}
Both examples work. However, when I log out and log in with the hooks version, I get an error message in console saying
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 the componentWillUnmount method.
which suggests that the cleanup is not done properly.
Yes, I know I could just continue using the version with the class which works. But I want to understand React hooks better by solving this.
Any ideas?
Wouldn't this works for you? You could use a single useEffect().
React Hooks API DOCs
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
setUser(null); // <--- TRY DOING SOMETHING LIKE THIS
subscription.unsubscribe();
};
},
[],
);
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect. In our example, this means a new
subscription is created on every update. To avoid firing an effect on
every update, refer to the next section.
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
Generally, this happens when we have asynchronous requests and the component is unmounted before, occurring memory leak. Obviously, that this not occur in class-based components because we have componentDidMount() and componentWillUnmount() hooks, so it's more confident than useEffect() that we have manipulated the state, so I think that you need to identify the reason for the application unmount and there is the solution.
You should use one useEffect() instead two like this:
useEffect(() => {
authGetUser(usr => setUser(usr))
return authUnsubscribe() // runs on mount and unmount only
}, [])

Resources