react setting state in a condition inside useEffect - reactjs

I'm trying to avoid showing an alert in React Native more than once.
To do that, I am trying to update the state inside a condition which is inside a useEffect:
const [permissionStatus, setPermissionStatus] = useState('');
const [permissionsAlertShown, setPermissionsAlertShown] = useState(false);
useEffect(() => {
function handleAppStateChange() {
if (
AppState.currentState === 'active' &&
permissionStatus === 'denied' &&
!permissionsAlertShown
) {
setPermissionsAlertShown(true);
Alert.alert(
...
);
}
}
AppState.addEventListener('change', handleAppStateChange);
}, [permissionStatus, permissionsAlertShown]);
My issue is that if I navigate away from my app and come back to it, AppState.currentState changes and since setPermissionsAlertShown(true) is ignored, I am shown the alert again.
How do I handle this situation?

The answer was to create a callback function that will remove the listener. I will share if someone else ever looks for this.
const [permissionStatus, setPermissionStatus] = useState('');
const [permissionsAlertShown, setPermissionsAlertShown] = useState(false);
const handleAppStateChange = useCallback(() => {
if (
AppState.currentState === 'active' &&
permissionStatus === 'denied' &&
!permissionsAlertShown
) {
setPermissionsAlertShown(true);
Alert.alert(
...
);
}
}, [permissionStatus, permissionsAlertShown]);
useEffect(() => {
AppState.addEventListener('change', handleAppStateChange);
return () => AppState.removeEventListener('change', handleAppStateChange);
}, [handleAppStateChange]);

You just need to persist the state, with asyncStorage or some other storage.
The ideal case is to use what persisted of your action (an permission enabled, an API data saved etc).

Related

React Hooks: How to get updated value of a state in useEffect

Need to update the boolean state value on map click. And based on that updated value, new component rendered.
const [layerClick, setLayerClick] = useState(false);
useEffect(() => {
if (streetsCurrentHeatStateMap !== null) {
streetsCurrentHeatStateMap.on('click', (e) => {
if (layerClick === false) {
popupToggler();
}
})
}
}, [
layerClick,
]);
And here is the popupToggler function
const popupToggler = async () => {
setLayerClick((layerClick) => !layerClick);
}
And it is used for the conditional rendering of the Popup Component.
{layerClick && (
<PopupContent />
)}
So problem is: It always call the popuptoggler function, mean the value of state not updated. Any help?

How to keep the state variable value intact upon screen reload

I am new to React and I tried to toggle the Login/Logout based on the current state of authentication. I've used Google OAuth to perform the authentication.
I have a state variable to say if the user is authenticated or not and is defaulted to false. Upon successful authentication, I set the state to true.
Now the problem is, after completing a successful authentication, when I refresh the screen, the screen reloads and I see the console.log printing false and login appears momentarily. And after a second the console.log prints true and then the logout appears. How do I avoid showing login screen (for that one second after refreshing the screen) when the authentication is completed? Can someone help me please? Thanks.
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
setIsAuthenticated(false)
}, [])
const handleSuccessAuth = x => {
setIsAuthenticated(true)
}
const handleFailureAuth = x => {
setIsAuthenticated(false)
}
const handleLogout = x => {
setIsAuthenticated(false)
}
console.log(isAuthenticated)
if(!isAuthenticated)
{
return (
<div>
<LoginView
handleSuccessAuth = {handleSuccessAuth}
handleFailureAuth = {handleFailureAuth}
/>
</div>
)
}
else
{
return (
<div>
<LogoutView
handleLogout = {handleLogout}
/>
</div>)
}
If your variable goes from false to true it means your code is doing something, probably an AJAX call, my recommendation is to show a loading scree/message until the AJAX request is completed.
There is no way to keep the variable intact on reload but you can keep a variable that tracks if the user authentication has been initialized and show a loading indicator in the meantime
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [authLoaded, setAuthLoaded] = useState(false)
// this does nothing, passing false to useState above sets the initial value
// useEffect(() => {
// setIsAuthenticated(false)
// }, [])
const handleSuccessAuth = x => {
setIsAuthenticated(true)
setAuthLoaded(true)
}
const handleFailureAuth = x => {
setIsAuthenticated(false)
setAuthLoaded(true)
}
const handleLogout = x => {
setIsAuthenticated(false)
}
console.log(isAuthenticated)
if (!authLoaded) {
return <div>loading...</div>
}
if(!isAuthenticated)
{
return (
<div>
<LoginView
handleSuccessAuth = {handleSuccessAuth}
handleFailureAuth = {handleFailureAuth}
/>
</div>
)
}
else
{
return (
<div>
<LogoutView
handleLogout = {handleLogout}
/>
</div>)
}
I believe olivier-boisse alluded to using localStorage to persist your state. You can use an useEffect hook to persist your isAuthenticated to localStorage when the value updates, and use a state initializer function to read in the initial state from local storage.
const [isAuthenticated, setIsAuthenticated] = useState(() => {
return !!JSON.parse(localStorage.getItem('auth');
});
useEffect(() => {
localStorage.setItem('auth', JSON.stringify(isAuthenticated));
}, [isAuthenticated]);

React hooks : how to watch changes in a JS class object?

I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.
What I understand is that you can get/set a state by using
const [myState, setMyState] = React.useState(myStateValue);
So. My component runs some functions based on the url prop :
const playlist = new PlaylistObj();
React.useEffect(() => {
playlist.loadUrl(props.url).then(function(){
console.log("LOADED!");
})
}, [props.url]);
Inside my PlaylistObj class, I have an async function loadUrl(url) that
sets the apiLoading property of the playlist to true
gets content
sets the apiLoading property of the playlist to false
Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :
<div
className={classNames({
'api-loading': playlist.apiLoading
})}
>
But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console.
It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?
I tested
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
//refresh playlist if its URL is updated
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
})
}, [props.playlistUrl]);
And this, but it seems more and more unlogical to me, and, well, does not work.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setPlaylist(playlist); //added this
})
}, [props.playlistUrl]);
I just want my component be up-to-date with the playlist object. How should I handle this ?
I feel like I'm missing something.
Thanks a lot!
I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
setPlaylist(playlist); // <-- this playlist reference doesn't change
})
}, [props.playlistUrl]);
I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.
const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setIsLoading(false);
});
}, [props.playlistUrl]);
Use the isLoading state in the render
<div
className={classNames({
'api-loading': isLoading,
})}
>
I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl)
.then(function() {
console.log("LOADED!");
})
.finally(() => setIsLoading(false));
}, [props.playlistUrl]);
Here you go:
import React from "react";
class PlaylistAPI {
constructor(data = []) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
removeEventListener(fn) {
this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
}
setPlayList(data) {
this.data = data;
this.notif();
}
loadUrl(url) {
console.log("called loadUrl", url, this.data)
}
notif() {
this.listeners.forEach(fn => fn());
}
}
export default function App() {
const API = React.useMemo(() => new PlaylistAPI(), []);
React.useEffect(() => {
API.addListener(loadPlaylist);
/**
* Update your playlist and when user job has done, listerners will be called
*/
setTimeout(() => {
API.setPlayList([1,2,3])
}, 3000)
return () => {
API.removeEventListener(loadPlaylist);
}
}, [API])
function loadPlaylist() {
API.loadUrl("my url");
}
return (
<div className="App">
<h1>Watching an object by React Hooks</h1>
</div>
);
}
Demo in Codesandbox

Are all variables inside a useEffect absolutely required to be listed as dependencies?

Do all variables used inside a useEffect always, absolutely, without exception need to be specified as dependencies?
My use case (simplified for demonstration purposes) involves different functions being executed depending on the width of the browser window, but not run when browser window changes:
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
useEffect(() => {
isDesktop ? scrollToTop() : scrollToTopOfArticle()
}, [selectedArticle])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
);
}
If I add isDesktop to the dependencies then the effect runs whenever the user resizes the window between mobile and desktop, which is not desired, but I'm also aware of the dogma that everything used inside the effect must be listed as a dependency.
Any suggestions on how to reconcile these two requirements?
If you want to make a useEffect() only responsive to a change in selectedArticle, use isDesktop and selectedArticle to initialize component states. Whenever selectedArticle changes, the first useEffect() will update both states with the passed-in props, and trigger the second useEffect() to re-run on the next render.
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
const [desktop, setDesktop] = useState(isDesktop)
const [article, setArticle] = useState(selectedArticle)
useEffect(() => {
if (article !== selectedArticle) {
setDesktop(isDesktop)
setArticle(selectedArticle)
}
}, [isDesktop, selectedArticle, article])
useEffect(() => {
if (desktop) scrollToTop()
else scrollToTopOfArticle()
}, [desktop, article])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
)
}
Alternatively, you can abstract this latching behavior to another hook so that isDesktop only updates to its live value when selectedArticle changes. Note that selectedArticle still needs to be a dependency of the scroll action effect, so that the useEffect() will trigger the scroll action on every change to selectedArticle even if isDesktop has not changed values since the last trigger.
const useLatch = (value, deps) => {
const [state, setState] = useState(value)
const effect = useCallback(() => { setState(value) }, [value])
useEffect(effect, deps)
return state
}
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
const latchedIsDesktop = useLatch(isDesktop, [selectedArticle])
useEffect(() => {
if (latchedIsDesktop) scrollToTop()
else scrollToTopOfArticle()
}, [latchedIsDesktop, selectedArticle])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
)
}
Are all variables inside a useEffect absolutely required to be listed as dependencies?
Yes or else it will generate a warning.
That's why it's best to have clear idea what each effect should do (i.e separation of concern).
Thus, you can have two separate effects with different dependencies.
Something like:
useEffect(
() => {
scrollToTopOfArticle();
}, [selectedArticle]
);
useEffect(
() => {
if (selectedArticle && isDesktop) {
scrollToTop();
}
}, [isDesktop, selectedArticle]
)

React Hooks - Removing component after use

I'm trying to wrap my head around this. My custom hook is supposed to create a simple popup with the desired input and remove after 3 seconds. Of course, currently, it re-renders every time the counter has reset. How can I make it render only once and then be removed from the dom?
export function createPopup(content, popupType) {
const [message, setMessage] = useState(content)
const [type, setType] = useState(popupType)
const [value, setCounter] = useState(3)
const myTimer = setTimeout(() => {
return setCounter(value - 1)
}, 1000)
useLayoutEffect(() => {
const id = setTimeout(() => {
setCounter(value + -1);
}, 1000);
return () => {
clearTimeout(id);
};
}, [value])
return (
<>
{value !== 0 &&
<PopupMolecule type={type}>
{message}
</PopupMolecule>
}
</>
)
}
I think you want something more like this:
export function createPopup(content, popupType) {
const [visible, setVisible] = useState(true);
useEffect(() => {
setTimeout(() => setVisible(false), 3000);
}, []);
return (
<>
{visible &&
<PopupMolecule type={popupType}>
{content}
</PopupMolecule>
}
</>
)
}
There are still some improvements that need to made here (i.e. fading out on exit or some transition, and the way this is setup you can't use it more than once), but this should fix the problem you stated.
This will show your popup for three seconds on mount, then make your popup invisible and unmount it from the DOM. The empty array in the useEffect hook lets it know to only trigger on mount (and unmount if you return a value). You also don't need the other state variables that you're not updating. Those can just be passed in as function parameters.

Resources