Reactjs SPA - Pressing IOS Safari back button updates the url but not the component view - reactjs

We have a reactjs SPA application where we want to navigate back and forth using browser back button/swipe the screen, if on Mac.
when a user clicks on a link from home page, they will be navigated to a detail page and the url looks like https://example.com/pdetail?prdt=Computer
On detail page, User has an ability to search, and when the user searches(Say Stationery), we update the url and push the url to history and the the detail page component is updated with the searched data.Like
https://example.com/pdetail?prdt=Stationery
filterSearch(searchval) {
//search related code to set variables
let newUrl = setParams(searchval, 'prdt')
this.props.history.push(`?${newUrl}`);
// dispatch an api call to get data
}
export function setParams(query = "", param) {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set(param, query.trim());
return searchParams.toString();
}
Now when the user browse back using the browser back button, we expect it to go the previous data(https://example.com/pdetail?prdt=Computer) from (https://example.com/pdetail?prdt=Stationery) and Vice Versa as we move back and forth. It is working fine in chrome and IE.
But in Safari, as the user presses back button, url is changing in the browser but the component view is not.
as we navigate back, I noticed that it did not go to ComponentDidMount as we go to the same page. But it goes ComponentDidUpdate so I added my code in there.
componentDidUpdate() {
window.onpopstate = (e) => {
if(this.props.history.action == 'POP')
{
e.preventDefault();
const search = window.location.search;
const params = new URLSearchParams(search);
if(params.has('prdt') === true)
{
const selectedprdt = params.get('prdt');
this.props.fetchDetails('FETCH_PRDT_DETAILS' , selectedprdt);
}
}
}
}
Let me know how can get this back forth page navigation with consistent url and data in Safari.
It is working fine in IE and chrome.
EDIT:
I have edited my code to add an eventListener in componentDidMount and UI is working in Safari. But when I see in the logs, I noticed the event runs multiple times not just once, which is very inefficient as we are making server calls.
componentDidMount()
{
const selectedprdt = getParams(window.location, 'prdt');
if(selectedprdt !== '')
{
this.props.fetchDetails('FETCH_PRDT_DETAILS' , selectedprdt);
}
window.addEventListener('popstate', this.handleBackButton)
}
handleBackButton = (e) =>
{
if(this.props.history.action == 'POP')
{
const search = window.location.search;
const params = new URLSearchParams(search);
if(params.has('prdt') === true)
{
const selectedMarket = params.get('prdt');
this.props.fetchDetails('FETCH_PRDT_DETAILS' , selectedprdt);
}
e.preventDefault();
}
}
Please let me know how to stop eventListener to executing the calls multiple times.
Thanks,

I had to remove the same listener on component Unmount.
componentWillUnMount()
{
window.removeEventListener('popstate', this.handleBackButton)
}

Related

Browser back button breaks React app history

I have ran into a very weird behaviour with the browser back button. I’ve tested on Chrome and Safari, the behaviour is the same.
So, I have a React app that has 3 pages - home (/), signup (/email) and success (/success). The routes are setup via react-router. Every page is in it's own functional component.
The user moves from home to signup via a button and a history.push(). From signup to success via a submit button and a history.push(). Something like this:
const onClick = () => {
history.push({ pathname: '/email', search: window.location.search });
};
Once on the success page, if the user waits 5 seconds, they will be redirected to an eCom website, via window.location.assign():
const useRedirectTimer = (
url: RedirectUrl,
): {
redirectTimerRef: React.MutableRefObject<NodeJS.Timeout | undefined>;
clearRedirectTimer: () => void;
} => {
const history = useHistory();
const { search } = useLocation();
const redirectTimerRef = useRef<NodeJS.Timeout | undefined>();
const clearRedirectTimer = () =>
redirectTimerRef.current && clearTimeout(redirectTimerRef.current);
useEffect(() => {
redirectTimerRef.current = setTimeout(() => {
window.location.assign(url.url);
}, delayInMS);
return () => {
clearRedirectTimer();
};
}, []);
return { redirectTimerRef, clearRedirectTimer };
};
Now, this is where I start having issues. There are 2 main situations:
Not waiting for the eCom website to load fully after the redirect and clicking the browser back button: I am returned to the browser's start page and my app basically disappears from history.
Waiting to be redirected to the eCom website, waiting for the full load, going back to /success page, waiting to be redirected and eCom website be fully loaded again and then pressing the back button - (so basically doing the same thing twice in a row) I am either returned to my app's home page (rarely) or to the browser's start page (more commonly).
Adding a button to the success page and making it do the redirect on click, or even just clicking anywhere on the page and then waiting for the redirect solves the second issue. It seems that no matter how many times I go back and forth, I always end up on the success page.
So my hunch is that at least one element on the page has to be focused once so the page can be returned to. But I could of course be wrong.
Is there any way I can make history preserve my app no matter how many times I go back and forth, and to always return to the success page from the eCom website?

How to reset channel of window.echo on changing of route reactjs

I am using laravel-websockets to listen to event. I have no issue on the back-end side; The issue is on the front-end side.
SCENARIO:
When I go to a specific route post/[slug], the current channel is based on the current slug. When I redirect to the same route but different value of slug, the channel listens to the first value on page refresh and not to the current one.
const Component = () => {
const router = useRouter();
const {slug} = router.query;
useEffect(() => {
window.Echo.private(`post.${slug}`).listen('PrivateEvent', e => {
console.log(e)
});
}, [slug])
}
Example:
On page refresh, go to post/first-slug. Next, click to <Link to="/post/second-slug">About</Link>
The example above should listen to second-slug and not the first-slug.
How can I solve this without hard refresh or <a> tag?
You forgot to stop listening on the previous channel, so the events are still received. I suppose that you end up with two active channels, receiving events for both.
Inside a useEffect() you should return a cleanup function that clears resources created for the effect
Here is how:
const Component = () => {
const router = useRouter();
const {slug} = router.query;
useEffect(() => {
window.Echo.private(`post.${slug}`).listen('PrivateEvent', e => {
console.log(e)
});
return () => window.Echo.private(`post.${slug}`).stopListening('PrivateEvent');
}, [slug])
}
If this does not solve your problem, please:
display the slug in your component (return <div>slug</div>;) to confirm that the navigation really happens ;
show us the whole console log.

React Navigation AddListener Not working Correctly

I am trying to deepLinking and catching the url when url open the page on the screen which is functionally works when my app is not working on the background. However, it doesn't work if app is working on the background.
const isFocused = useIsFocused();
useEffect(() => {
getCode();
}, [isFocused]);
const getCode = async () => {
//we will generate a button in the forget password email, link will include a url ===> mobile://auth/new-password?verification=534396
const url = await Linking.getInitialURL();
console.log('url', url);
if (url?.includes('new-password')) {
//problem, it may not work if app is still working on the background
const query = queryString.parseUrl(url);
const verifyCode = query.query.verification;
setVerificationCode(String(verifyCode));
setIsLoading(false);
} else {
Alert.alert('Something went wrong');
}
};
When I directlinked to application with the link, it console log as "url null". Is my problem on the focusing part or on the getInitialUrl function?
I was experiencing a similar issue.
In my case we used linking from NavigationContainer and it would open the same X screen left on background regardless if the deeplink data had a different value for that screen.
I fixed it by using the getId on Stack.Screen:
const getId = ({ params }) => params?.id;
<Stack.Screen name="X" component={XComponent} getId={getId} />
You can find more info on getId here https://reactnavigation.org/docs/screen/#getid.

Navigate to specific screen on opening notification with Firebase Messaging & React Navigation (React Native, Android)

I've got a react native app using remote notifications with firebase messaging and react navigation. Everything is working great, except after a while (or occasionally) the notification listeners don't work. It could take up to 2 weeks until I get any issues whatsoever. It usually happens when the app has been started for a long time but not used, then when opening a notification, the onNotificationOpenedApp doesn't run, i.e I just see the index screen. I am using "react-native-bootsplash" if that could have anything to do with it?
Also, sometimes an old notification triggers a listener, and I assume that is getInitialNotification that is being invoked twice (or multiple times). Does android semi-close apps that hasn't been used for a while, and then corrupt the listeners when the app opens again? It seems like the app unmounts partially, and then mounts with issues. As soon as the issue occurs, it is way more likely it will continue happening until I restart the app.
Here's the code for registering the listeners:
getInitialNotification
When the user opens the app via a notification, this will be run and return the notification. This is done inside the getInitialUrl prop passed to NavigationContainer (react-navigation component) This is recommended in their docs here
<NavigationContainer
ref={setNavigationRef}
linking={{
config: {
initialRouteName: 'SignedIn',
},
getInitialURL: async () => {
try {
// If app was opened with deeplink
const url = await Linking.getInitialURL();
if (url !== null) return url;
const notification = await messaging().getInitialNotification();
if (notification !== null) {
const {data} = notification;
if (data!.type === 'ARTICLE') {
return `${rootUrl}/site/news/${data!.id}`;
}
if (data!.type === 'TRANSFER') {
return `${rootUrl}/site/player/${data!.id}`;
}
}
return null;
} catch (error) {
return null;
}
},
}}>
<App />
</NotificationContainer>
Inside the index file (App.tsx) I also register a listener for opening a notification when the app is already started but in the background, using onNotificationOpenedApp
onNotificationOpenedApp:
useEffect(() => {
const unsubscribeOnNotificationOpenedApp =
messaging().onNotificationOpenedApp(remoteMessage => {
if (!remoteMessage.data) return;
const {type, id} = remoteMessage.data;
if (!id) return;
if (type === 'TRANSFER') {
loadPlayer(Number(id));
} else {
loadArticle(Number(id));
}
});
return unsubscribeOnNotificationOpenedApp;
}, [])

React Redux, load data from the API on first page load ONLY. Render component once data is loaded

I am DEFINITELY doing something wrong at the core of my app.
I think its where I am making a request to my API. I'm also not happy with the firstLoad flagging to tell the app to call the API.
I am using React/Redux - building from this boiler plate https://www.npmjs.com/package/create-react-app-redux.
It uses mapStateToProps, mapDispatchToProps and connect to glue everything together.
So I have a component called "Shop"
// only true when user first visits url OR presses F5 to refresh.
// Its conveniently false when page rerenders
var firstLoad = true;
const Shop = ({ stateProps, dispatchProps }) => {
if(firstLoad) {
firstLoad = false;
// dispatchProps contains a method called changeState that will update state.shops to the result from the API. It will also set state.loading to true while calling and false when finished.
// Note, I am not modifying state directly. The dispatchProps wraps the action that does so
server.GetShops({url: "api/shops", updateField:"shops", loading:true, token:"abc-xyz"}, dispatchProps);
}
if(stateProps.loading) {
return (
<div>loading...</div>
);
}
var shopUrl = window.location.href; // extract the part of the url after the domain
var selectedShop = stateProps.shops.find(s => {
return s.url === shopUrl;
});
if(!selectedShop) {
window.location.href = "/";
}
return (
<div>
{selectedShop.welcomeMessage}
</div>
);
}
The problem im having is I have to refresh TWICE to see when the welcomeMessage is changed.
So if its originally "hello world" from the database and I change it to "hello UK" in the database.
When I refresh the page, i expect the app to fetch data and display loading.
When it finished fetching data, a re-render occurs and it should show Hello UK in the welcome message.
However this doesn't happen until the second page refresh.
Does what I described make sense to anyone?
You are not making any change based on the value of selectedShop
you should keep the value of selectedShop in a local state variable
const [ selectedShop , setSelectedShop ] = useState({});
then whenever the value is changed from api call update the local state value
useEffect( () => {
var selectedShop = stateProps.shops.find(s => {
return s.url === shopUrl;
});
setSelectedShop(selectedShop);
} , [stateProps.shops])
now when the value changes in stateProps it will update the local state value and trigger a re render .

Resources