A react app using hooks. In useEffect there is an api-call to populate content on page.
Each element fetched is clickable. And a click will open details for the clicked element in a new browser window/tab.
When working in the new window, the user can make changes on that object. The user will then close that tab or at least just go back to the main window/tab.
Question is how I can detect that the user is coming back to the main window. This is because I want to re-fetch data from API. Thus, I want to rerun useEffect.
While googling I found this:
https://www.npmjs.com/package/react-page-visibility
Is that really what I'm looking for? Reading the docs I'm not really sure if that can be the solution to my issue. Is there another way to solve this?
You can use the visibilitychange event to achieve that:
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') {
console.log("Tab reopened, refetch the data!");
}
};
useLayoutEffect(() => {
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
}, []);
Codesandbox
With React 18 you can use 'visibilitychange' with react brand new hook useSyncExternalStore
export function useVisibilityChange(serverFallback: boolean) {
const getServerSnapshot = () => serverFallback;
const [getSnapshot, subscribe] = useMemo(() => {
return [
() => document.visibilityState === 'visible',
(notify: () => void) => {
window.addEventListener('visibilitychange', notify);
return () => {
window.removeEventListener('visibilitychange', notify);
};
},
];
}, []);
Gist with hook
P.S:Don't forget cross-browser usage
Related
I have a component in my react native app that loads sessions related to a particular individual. In the useEffect() of that component I both load the sessions when the component comes into focus, and unload those sessions within the cleanup.
export const ClientScreen = (props) => {
const isFocused = useIsFocused();
const client = useSelector((state) => selectActiveClient(state));
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
return () => dispatch(unloadSessions()); // Cleaning up here...
};
loadSessions(props);
}
}, [isFocused, client?.id]);
const updatedProps = {
...props,
client,
};
return <ClientBottomTabNavigator {...updatedProps} />;
};
Generally the component is working as expected. However, I do notice that if I load the component with one client, then navigate away, and then come back to the component by loading a new client, that for a brief moment the sessions pertaining to the previous client show before being replaced the sessions relevant to the new client.
My question is, shouldn't the unloadVisits() that runs on cleanup -- which sets sessions to an empty array -- prevent this? Or is this some kind of react behavior that's holding onto the previous state of the component? How can I ensure this behavior doesn't occur?
Cleanup function should appear before the closing-brace of the useEffect hook
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
};
loadSessions(props);
}
return () => dispatch(unloadSessions()); // Cleaning up here... // <--- here
}, [isFocused, client?.id]);
as commented, your loadSessions returns a cleanup function, but you don't do anything with it. And the effect where you call loadSessions(props) does not return anything, that's why it does not clean up.
Edit:
I made a mistake, loadSessions returns a Promise of a cleanup function. And it is impossible to "unwrap" this Promise and get to the cleanup function itself in a way that you can return it in your effect. You have to move the cleaup function out of the async function loadSessions.
But you don't need async/await for everything:
useEffect(() => {
if (isFocused && client?.id) {
loadClientSessions(client.id).then(dispatch);
return () => dispatch(unloadSessions());
}
}, [isFocused, client?.id]);
I have 2 screens in Stack Navigator.
All Categories,
Add a new category
In the All Categories screen, all categories are displayed.
useEffect(() => {
loadCategories();
}, []);
This is the useEffect hook that load all the categories.
I have made a touchable opacity to navigate to Add a new category screen, So users can easily add a new category if needed.
What I'm expecting to do: So after adding a new category and going back to the All Categories screen loadCategories() should run again, So the user can see the newly added category there. But the problem is when I add a new category and go back the loadCategories() function doesn't execute again. What will be the cause for this?
If you want to load categories when screen is focus
function Categories({ navigation }) {
React.useEffect(() => {
const loadCategories = navigation.addListener('focus', () => {
loadCategories();
});
return loadCategories;
}, [navigation]);
...rest
}
Often we need to fetch data when we come to some screens back,
For this, there is a hook from the #react-navigation/native library (useFocusEffect), which we can use to accomplish such requirements.
For Ex.
import { useFocusEffect } from '#react-navigation/native';
function Profile({ userId }) {
const [user, setUser] = React.useState(null);
useFocusEffect(
React.useCallback(() => {
const unsubscribe = API.subscribe(userId, user => setUser(user));
return () => unsubscribe();
}, [userId])
);
return <ProfileContent user={user} />;
}
In the above code, Whenever the ProfileContent screen is getting focused, the code inside the useFocusEffect will re-run.
Refer to official docs for in-depth understanding.
I found this answer which is relatable to this question.
https://stackoverflow.com/a/62703838/19317939
import {useIsFocused} from '#react-navigation/native';
...
const isFocused = useIsFocused();
useEffect(() => {
if (isFocused) loadCategories();
}, [isFocused]);
I'm having trouble getting datas from redux when I refresh my page
const [filters, setFilters] = useState<string[]>(props.filters);
useEffect(() => {
(() => {
if (!_.isEqual(filters, props.filters)) {
setFilters(props.filters);
}
})();
});
My filters are undefined even though when I check redux devtools, there is datas in filters.
I need to trigger an event in my front to display my filters.
Anyone have an idea please ?
Edit:
(if I click any element in my page it load my filters)
If I add a setTimeout on refresh it works but I'm not sure using setTimeout is a solution
useEffect(() => {
setTimeout(() => {
setFilters(props.filters);
}, 1500);
}, []);
May be you just missed returning the inner functions:
Or just call the setFilters like:
useEffect(() => {
if (!_.isEqual(filters, props.filters)) {
setFilters(props.filters);
}
});
If I've got a function that creates a confirm popup when you click the back button, I want to save the state before navigating back to the search page. The order is a bit odd, there's a search page, then a submit form page, and the summary page. I have replace set to true in the reach router so when I click back on the summary page it goes to the search page. I want to preserve the history and pass the state of the submitted data into history, so when I click forward it goes back to the page without error.
I've looked up a bunch of guides and went through some of the docs, I think I've got a good idea of how to build this, but in this component we're destructuring props, so how do I pass those into the state variable of history?
export const BaseSummary = ({successState, children}: BaseSummaryProps) => {
let ref = createRef();
const [pdf, setPdf] = useState<any>();
const [finishStatus, setfinishStatus] = useState(false);
const onBackButtonEvent = (e) => {
e.preventDefault();
if (!finishStatus) {
if (window.confirm("Your claim has been submitted, would you like to exit before getting additional claim information?")) {
setfinishStatus(true);
props.history.push(ASSOCIATE_POLICY_SEARCH_ROUTE); // HERE
} else {
window.history.pushState({state: {successState: successState}}, "", window.location.pathname);
setfinishStatus(false);
}
}
};
useEffect(() => {
window.history.pushState(null, "", window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent);
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
};
}, []);
Also I'm not passing in the children var because history does not clone html elements, I just want to pass in the form data that's returned for this component to render the information accordingly
first of all, I think you need to use "useHistory" to handling your hsitry direct without do a lot of complex condition, and you can check more from here
for example:
let history = useHistory();
function handleClick() {
history.push("/home");
}
now, if you need to pass your history via props in this way or via your code, just put it in function and pass function its self, then when you destruct you just need to write your function name...for example:
function handleClick() {
history.push("/home");
}
<MyComponent onClick={handleClick} />
const MyComponent = ({onClick}) => {....}
I fixed it. We're using reach router, so everytime we navigate in our submit forms pages, we use the replace function like so: {replace: true, state: {...stateprops}}. Then I created a common component that overrides the back button functionality, resetting the history stack every time i click back, and using preventdefault to stop it from reloading the page. Then I created a variable to determine whether the window.confirm was pressed, and when it is, I then call history.back().
In some scenarios where we went to external pages outside of the reach router where replace doesn't work, I just used window.history.replaceStack() before the navigate (which is what reach router is essentially doing with their call).
Anyways you wrap this component around wherever you want the back button behavior popup to take effect, and pass in the successState (whatever props you're passing into the current page you're on) in the backButtonBehavior function.
Here is my code:
import React, {useEffect, ReactElement} from 'react';
import { StateProps } from '../Summary/types';
export interface BackButtonBehaviorProps {
children: ReactElement;
successState: StateProps;
}
let isTheBackButtonPressed = false;
export const BackButtonBehavior = ({successState, children}: BackButtonBehaviorProps) => {
const onBackButtonEvent = (e: { preventDefault: () => void; }) => {
e.preventDefault();
if (!isTheBackButtonPressed) {
if (window.confirm("Your claim has been submitted, would you like to exit before getting additional claim information?")) {
isTheBackButtonPressed = true;
window.history.back();
} else {
isTheBackButtonPressed = false;
window.history.pushState({successState: successState}, "success page", window.location.pathname); // When you click back (this refreshes the current instance)
}
} else {
isTheBackButtonPressed = false;
}
};
useEffect(() => {
window.history.pushState(null, "", window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent);
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
};
}, []);
return (children);
};
I have a hook that executes the function passed to it when someone leaves the current page, or when they leave the website
const useOnPageLeave = (handler) => {
useEffect(() => {
window.onbeforeunload = undefined;
window.addEventListener('beforeunload', (event) => { //When they leave the site
event.preventDefault(); // Cancel the event as stated by the standard.
handler();
});
return () => {
handler(); //When they visit another local link
document.removeEventListener('beforeunload', handler);
};
}, []);
};
When using Firefox or Safari, this alert message appears(only when they leave the site, not when they visit a local link), and I don't want this alert message to show
If I remove the line event.preventDefault(), then the handler function is not executed, and I need the handler event to be executed
Here is an MRE of the problem
const useOnPageLeave = (handler) => {
useEffect(() => {
window.onbeforeunload = () => handler();
window.addEventListener('beforeunload', (event) => {
handler();
});
return () => {
handler();
document.removeEventListener('beforeunload', handler);
};
});
};
There might be a problem with using a listener inside of useEffect.
const useOnPageLeave = (handler) => {
useEffect(() => {
}, []); // useEffect will only get called once on component did mount
};
So You might want to consider trying to take your logic outside of useEffect
or
Adding a useState hook as a listener might be the way to go
const useOnPageLeave = (handler) => {
const [userLeaving, setUserLeaving] = useState(false);
useEffect(() => {
if (userLeaving) handler();
}, [userLeaving]); // useEffect will only get called once on component did mount AND each time userLeaving state updates
};
It looks like firefox and safari browsers are showing this message be default. Not much you can do about it. But there is a good news. It seems they are targeting only beforeunload event but not unload. So you could try to use that.
Here is more info about that: https://support.mozilla.org/en-US/questions/1304313
You can simply handle the unload event on window. For example the following code does an HTTP request while the page is being closed:
window.addEventListener('unload', function (event) {
(async () => {
const response = await fetch('/my-endpoint')
console.log(response)
})()
})
Do note however that the use of the unload event is discouraged. See MDN docs for details (TL;DR: it's unreliable, especially on mobile).