How to detect route change in microfrontend app - reactjs

I have microfrontend app consisting of two react projects let's call them A and B and if a user clicks a certain link in A project then url is changed. But how can I detect url change in project B. Now I have componentDidUpdate function which checks current location.pathname with previous one. But componentDidUpdate is run only when url is changed within A or B.
componentDidUpdate(prevProps) {
const {
location,
} = this.props;
if (location.pathname.includes('/messages/inbox/') && prevProps.location.pathname !== location.pathname) {
...logic code
}
}

Use reatc-router OR react-router-dom, handle it by useEffect
import { useEffect } from "react";
import { useLocation} from "react-router-dom";
const App = () => {
const location = useLocation();
useEffect(() => {
console.log("Changed");
}, [location.pathname]);
return ();
}
export default App;
You can handle locaton.search instead of location.pathname

You can use history.listen method when trying to detect the route are change.
Docs:
You can listen for changes to the current location using history.listen.
Example :
useEffect(() => {
// To start listening for location changes...
const unlisten = history.listen(({ action, location }) => {
// The current location changed.
});
// Later, when you are done listening for changes
// should have cleanup to remove them
return () => {
unlisten();
};
}, [])

Related

How to controling browser back button with react router dom v6?

I've been looking for this question and found it but they're using class components and react router dom v5
What i want is
When user click browser back button I'll redirect them to home page
If you are simply wanting to run a function when a back navigation (POP action) occurs then a possible solution is to create a custom hook for it using the exported NavigationContext.
Example:
import { UNSAFE_NavigationContext } from "react-router-dom";
const useBackListener = (callback) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
const listener = ({ location, action }) => {
console.log("listener", { location, action });
if (action === "POP") {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Usage:
import { useNavigate } from 'react-router-dom';
import { useBackListener } from '../path/to/useBackListener';
...
const navigate = useNavigate();
useBackListener(({ location }) =>
console.log("Navigated Back", { location });
navigate("/", { replace: true });
);
If using the UNSAFE_NavigationContext context is something you'd prefer to avoid then the alternative is to create a custom route that can use a custom history object (i.e. from createBrowserHistory) and use the normal history.listen. See my answer here for details.
Update w/ Typescript
import { useEffect, useContext } from "react";
import { NavigationType, UNSAFE_NavigationContext } from "react-router-dom";
import { History, Update } from "history";
const useBackListener = (callback: (...args: any) => void) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator as History;
useEffect(() => {
const listener = ({ location, action }: Update) => {
console.log("listener", { location, action });
if (action === NavigationType.Pop) {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Well after a long journey to find out how to do that finally i came up with this solution
window.onpopstate = () => {
navigate("/");
}
I came up with a pretty robust solution for this situation, just using browser methods, since react-router-v6's API is pretty sketchy in this department right now.
I push on some fake history identical to the current route (aka a buffer against the back button). Then, I listen for a popstate event (back button event) and fire whatever JS I need, which in my case unmounts the component. If the component unmounts WITHOUT the use of the back button, like by an onscreen button or other logic, we just clean up our fake history using useEffect's callback. Phew. So it looks like:
function closeQuickView() {
closeMe() // do whatever you need to close this component
}
useEffect(() => {
// Add a fake history event so that the back button does nothing if pressed once
window.history.pushState('fake-route', document.title, window.location.href);
addEventListener('popstate', closeQuickView);
// Here is the cleanup when this component unmounts
return () => {
removeEventListener('popstate', closeQuickView);
// If we left without using the back button, aka by using a button on the page, we need to clear out that fake history event
if (window.history.state === 'fake-route') {
window.history.back();
}
};
}, []);
You can go back by using useNavigate hook, that has become with rrd v6
import {useNabigate} from "react-router-dom";
const App = () => {
const navigate = useNavigate();
const goBack = () => navigate(-1);
return (
<div>
...
<button onClick={goBack}>Go back</button>
...
</div>
)
}
export App;
I used <Link to={-1}>go back</Link> and its working in v6, not sure if it's a bug or a feature but seems there is no error in console and can't find any documentation stating this kind of approach
You can try this approach. This worked for me.
import { useNavigate, UNSAFE_NavigationContext } from "react-router-dom";
const navigation = useContext(UNSAFE_NavigationContext).navigator;
const navigate = useNaviagte();
React.useEffect(() => {
let unlisten = navigation.listen((locationListener) => {
if (locationListener.action === "POP") {
//do your stuff on back button click
navigate("/");
}
});
return(() => {
unlisten();
})
}, []);
I'm on rrd#6.8 and testing John's answer worked for me right away for a simple "GO back 1 page", no useNavigate needed:
<Link to={-1}>
<Button size="sm">← Back </Button>
</Link>
So as a simple back button this seems to work without unexpected errors.

Pass parameter to page, data not updating

I have a navigation component where I'm passing a parameter to another page, the parameter is getting passed, however, the data in the dropdown is not updating for the passed ID:
nav:
<Link to='/service/ServiceAppointment/${car.Make}'> { serviceAppointment } </Link>
appointment page:
const ScheduleAppointment = () => {
const { id } = useParams();
console.log (id); //I can see the ID passed to the page in the console
useEffect(() => {
console.log(id); //the ID is not there
scheduleAppointment(id);
});
const Appointment= () => {
//call to API for open dates
//the ID never gets here
}
}
Router:
<Route exact path='/service/appointment/:id' component={ ScheduleAppointment } />
how can I get the appointment page to change when a new ID is passed to it?
Dependencies argument of useEffect is useEffect(callback, dependencies)
Let's explore side effects and runs:
Not provided: the side-effect runs after every rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after EVERY rendering
});
}
An empty array []: the side-effect runs once after the initial rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
}
Has props or state values [prop1, prop2, ..., state1, state2]: the side-effect runs only when any dependency value changes.
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state]);
}
in your case try this way
useEffect(() => {
console.log(id); //the ID is not there
scheduleAppointment(id);
},[id]);
Please update the below link instead of your code
<Link to=`/service/ServiceAppointment/${car.Make}`> { serviceAppointment } </Link>
I hope it will work for you! Thanks.

React native screens not re-rendering when custom hook state got changed

I am using a custom hook in app purchases.
const useInAppPurchase = () => {
const context = useContext(AuthGlobal)
const [isFullAppPurchased, setIsFullAppPurchased] = useState(false)
useEffect(() => {
console.log(`InAppPurchase useEffect is called`)
getProductsIAP()
return async () => {
try {
await disconnectAsync()
} catch (error) {}
}
}, [])
....
}
When I used this hook at AccountScreen (where I do the purchase) Account screen is getting re-rendered once the payment is done.
i.e. isFullAppPurchased is changing from false -> true
const AccountScreen = (props) => {
const width = useWindowDimensions().width
const {
isFullAppPurchased,
} = useInAppPurchase()
return (
// value is true after the purchase
<Text>{isFullAppPurchased}</Text>
)
}
But I am using the same hook in CategoryList screen and after the payment is done when I navigate to the CategoryList screen, The values (isFullAppPurchased) is not updated (still false).
But when I do the re-rendering manually then I get isFullAppPurchased as true.
const CategoryList = (props) => {
const navigation = useNavigation()
const { isFullAppPurchased } = useInAppPurchase()
return (
// still value is false
<Text>{isFullAppPurchased}</Text>
)
}
What is the reason for this behaviour ? How should I re-render CategoryList screen once the payment is done ?
Thank you.
I see hook make API request only on mount, if whole parent component didn't unmount and rendered a new, value of hook stays same.
E.g. dependencies array is empty - [] so hook doesn't request data again.
Probably better idea is to pass isFullAppPurchased via context or redux from top level.
And put state and function to update that state in same place.

In React Router v6, how to check form is dirty before leaving page/route

Below are the package versions I'm using.
React version - 16.13.1
react-router-dom version - 6.0.0-beta.0
react-redux version 7.2.0
Material UI version 4.11.0
How/what is the best way to check that a form isDirty (has changed) when the user is trying to leave the current page? I would like to prompt "Are you sure want to leave...." if the form isDirty.
I will fetch the data from within useEffect() and use a redux reducer to render the UI.
Should I declare a variable to keep the original fetched data for dirty checking?
This is what I am doing, but it is not working correctly.
component.js
useEffect(() => {
props.fetchUserInfo();
})
action.js
export function fetchUserInfo() {
return (dispatch) => {
dispatch({type: USER_INITIALSTATE, {Name: 'abc', Age: 20}}
)
}
}
userReducer.js
const initialState = {
processing: false,
success: false,
fail: false,
Profile: {}
}
let oriState;
let State;
const UserReducer = (state = initialState, action) => {
if (action.type === USER_INITIALSTATE) {
oriState = {Profile: action.data};
State = {...state, Profile: action.data};
return {...state, Profile: action.data};
} else if (action.type === OTHERS_ACTION) {
//update field change
return {...state, xxx}
}
}
export const userIsDirty = state => {
if (oriState && State) {
return JSON.stringify(oriState.Profile) !== JSON.stringify(State.Profile);
}
return false;
};
export default UserReducer;
So in my component I call userIsDirty to return the isDirty boolean, but I haven't figured out how to catch the leave page event and use this as a trigger to do the dirty form checking.
So how to detect leaving the current page? I tried something on useEffect return(component umount), but the props is not getting the updated INITIALSTATE state (meaning I will get Profile: {}), because it only runs once, but if I add the useEffect optional array argument, I get an infinite loop(maybe I set it wrong?).
useEffect(() => {
props.fetchUserInfo();
return () => {
console.log(props); //not getting initial state object
};
}, []);
Am I doing this the correct way? What have I missed? Is there a better/correct solution to achieve what I want?
Thanks #gdh, useBlocker is the one I want. I am using it to popup a confirmation dialog.
I will share my complete codesandbox, I believe this may be helpful for someone in the future.
show confirmation dialog by using useBlocker
Update:
Prompt, usePrompt and useBlocker have been removed from react-router-dom. This answer will not currently work, though this might change. The github issue, opened Oct 2021, is here
The answer...
This answer uses router v6.
You can use usePrompt.
usePrompt will show the confirm modal/popup when you go to another route i.e. on mount.
A generic alert with message when you try to close the browser. It handles beforeunload internally
usePrompt("Hello from usePrompt -- Are you sure you want to leave?", isBlocking);
You can use useBlocker
useBlocker will simply block user when attempting to navigating away i.e. on unmount
A generic alert with message when you try to close the browser. It handles beforeunload internally
useBlocker(
() => "Hello from useBlocker -- are you sure you want to leave?",
isBlocking
);
Demo for both 1 & 2
You can also use beforeunload. But you have to do your own logic. See an example here
Just adding an additional answer for React Router v6 users.
As of v6.0.0-beta - useBlocker and usePrompt were removed (to be added back in at a later date).
It was suggsested if we need them in v6.0.2 (current version at the time of writing) that we should use existing code as an example.
Here is the code directly from the the alpha for these hooks.
So to add the hooks back in would be this code (anywhere in your app for usage):
** I only copied the code for react-router-dom - if you're using native, then you'll need to check the above link for the other usePrompt hook
/**
* These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
* Thanks for the idea #piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
* Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
*/
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
* Blocks all navigation attempts. This is useful for preventing the page from
* changing until some condition is met, like saving form data.
*
* #param blocker
* #param when
* #see https://reactrouter.com/api/useBlocker
*/
export function useBlocker( blocker, when = true ) {
const { navigator } = useContext( NavigationContext );
useEffect( () => {
if ( ! when ) return;
const unblock = navigator.block( ( tx ) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker( autoUnblockingTx );
} );
return unblock;
}, [ navigator, blocker, when ] );
}
/**
* Prompts the user with an Alert before they leave the current screen.
*
* #param message
* #param when
*/
export function usePrompt( message, when = true ) {
const blocker = useCallback(
( tx ) => {
// eslint-disable-next-line no-alert
if ( window.confirm( message ) ) tx.retry();
},
[ message ]
);
useBlocker( blocker, when );
}
Then the usage would be:
const MyComponent = () => {
const formIsDirty = true; // Condition to trigger the prompt.
usePrompt( 'Leave screen?', formIsDirty );
return (
<div>Hello world</div>
);
};
#Devb your question and update were super helpful and saved me a lot of time. Thank you! created a HOC based on your code. might be useful to someone.
props on Wrapped Component:
setPreventNavigation - sets when to block navigation
provideLeaveHandler - sets the function that will run when you try to change a route and you are blocked for navigation
confirmNavigation - continue navigation
cancelNavigation - stop Navigation
import React, { useEffect, useState, useCallback } from 'react'
import { useNavigate, useBlocker, useLocation } from 'react-router-dom'
export default function withPreventNavigation(WrappedComponent) {
return function preventNavigation(props) {
const navigate = useNavigate()
const location = useLocation()
const [lastLocation, setLastLocation] = useState(null)
const [confirmedNavigation, setConfirmedNavigation] = useState(false)
const [shouldBlock, setShouldBlock] = useState(false)
let handleLeave = null
const cancelNavigation = useCallback(() => {
setshouldBlock(false)
},[])
const handleBlockedNavigation = useCallback(
nextLocation => {
if (
!confirmedNavigation &&
nextLocation.location.pathname !== location.pathname
) {
handleLeave(nextLocation)
setLastLocation(nextLocation)
return false
}
return true
},
[confirmedNavigation]
)
const confirmNavigation = useCallback(() => {
setConfirmedNavigation(true)
}, [])
useEffect(() => {
if (confirmedNavigation && lastLocation) {
navigate(lastLocation.location.pathname)
}
}, [confirmedNavigation, lastLocation])
const provideLeaveHandler = handler => {
handleLeave = handler
}
useBlocker(handleBlockedNavigation, shouldBlock)
return (
<WrappedComponent
{...props}
provideLeaveHandler={provideLeaveHandler}
setPreventNavigation={setShouldBlock}
confirmNavigation={confirmNavigation}
cancelNavigation={cancelNavigation} />
)
}
}
Posting this for someone who wants custom UI pop-up/modal box instead for browser's default prompt and they are using react-router (v4) with history.
You can make use of custom history and configure your router like
import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()
...
import { history } from 'path/to/history';
<Router history={history}>
<App/>
</Router>
and then in your custom prompt component you can make use of history.block like
import { history } from 'path/to/history';
class MyCustomPrompt extends React.Component {
componentDidMount() {
this.unblock = history.block(targetLocation => {
// take your action here
return false;
});
}
componentWillUnmount() {
this.unblock();
}
render() {
//component render here
}
}
Add this MyCustomPrompt in your components where ever you want to block navigation.
It seems you are looking for the beforeunload event.
Read carefully as not all browsers are compliant with event.preventDefault().
In the event handler you can do the checks you want and call prevent the window to close depending on your requirements.
Hope this helps.
The hooks #gdh mentioned in his answer were removed by the developers team of react-router. Because of that you can't use usePrompt or useBlocker with the current version of react-router (v6).
But the team mentioned they are heavily working on the features. reference
If somebody wants to implement the changes the remix team made to offer the functionalities of the hooks you can take a look at this answer from github. here
I have figure out a solution which can show custom dialog, block navigation and resume asynchronous.
You can find a discussion about this on github.
https://github.com/remix-run/react-router/issues/8139
With everyone's help, I made the final solution.
import { useState, useContext, useEffect, useRef, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
import type { History } from 'history';
/** #description Blocks all navigation attempts.
* #param when {boolean} Whether to start intercepting navigation.
* #example
* const [flag, setFlag, next] = usePrompt(false);
* useEffect(() => {
* if (flag) {
* // do something like show a dialog etc;
* // at the right time resume bocked navigate
* next();
* }
* }, [flag]);
*/
export const usePrompt = (when = false) => {
const [flag, setFlag] = useState(false);
const confirm = useRef<any>(null);
const context = useRef<any>(null);
const { navigator } = useContext(NavigationContext);
const blockNavigator = navigator as History;
const next = useCallback(() => {
confirm.current();
context.current?.retry?.();
}, [flag]);
useEffect(() => {
if (!when) return;
const unblock = blockNavigator.block((tx) => {
setFlag(true);
context.current = tx;
});
confirm.current = unblock;
return unblock;
}, [blockNavigator, when]);
return [flag, setFlag, next] as const;
};
I was facing the same situation of attempting to utilize a customized "pleasant" UI confirmation dialog integrating with react router v6 beta's useBlocker hook for blocking route transitions when the current route's form has unsaved modifications. I started with the code from the codesandbox linked in the UPDATED section at the bottom of this question. I found this custom hook implementation to not work for all of my needs, so I adapted it to support an optional regular expression parameter to define a set of routes that should not be blocked. Also of note, the codesandbox implementation returns a boolean from the callback passed into useBlocker, but I found this has no effect or usefulness, so I removed this. Here is my full TypeScript implementation of a revised custom hook:
useNavigationWarning.ts
import { useState, useEffect, useCallback } from 'react';
import { useBlocker, useNavigate, useLocation } from 'react-router-dom';
import { Blocker } from 'history';
export function useNavigationWarning(
when: boolean,
exceptPathsMatching?: RegExp
) {
const navigate = useNavigate();
const location = useLocation();
const [showPrompt, setShowPrompt] = useState<boolean>(false);
const [lastLocation, setLastLocation] = useState<any>(null);
const [confirmedNavigation, setConfirmedNavigation] = useState<boolean>(
false
);
const cancelNavigation = useCallback(() => {
setShowPrompt(false);
}, []);
const handleBlockedNavigation = useCallback<Blocker>(
nextLocation => {
const shouldIgnorePathChange = exceptPathsMatching?.test(
nextLocation.location.pathname
);
if (
!(confirmedNavigation || shouldIgnorePathChange) &&
nextLocation.location.pathname !== location.pathname
) {
setShowPrompt(true);
setLastLocation(nextLocation);
} else if (shouldIgnorePathChange) {
// to cancel blocking based on the route we need to retry the nextLocation
nextLocation.retry();
}
},
[confirmedNavigation, location.pathname, exceptPathsMatching]
);
const confirmNavigation = useCallback(() => {
setShowPrompt(false);
setConfirmedNavigation(true);
}, []);
useEffect(() => {
if (confirmedNavigation && lastLocation?.location) {
navigate(lastLocation.location.pathname);
// Reset hook state
setConfirmedNavigation(false);
setLastLocation(null);
}
}, [confirmedNavigation, lastLocation, navigate]);
useBlocker(handleBlockedNavigation, when);
return [showPrompt, confirmNavigation, cancelNavigation] as const;
}

Can't perform a react state update error when using useEffect in a custom express route

I have a problem with my custom express route. Every time I change page via my custom route component I get a react no-op error :
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 a useEffect cleanup function.
Here is my custom route component/handler :
import React from 'react';
import { Route } from 'react-router-dom';
import { AuthenticationContext } from '../../contexts/authentication/context';
import Unauthorized from '../Unauthorized';
import axios from 'axios';
const ProtectedRoute = ({ component: Component, redirect: Redirect, contextProvider: ContextProvider, path, ...routeProps }) => {
const { authenticationState: {isAuthenticated, isFetchingTheUser, currentUser} } = React.useContext(AuthenticationContext)
const [authorized, setAuthorized] = React.useState(['Admin']);
const [isFetchingAuthorizations, setIsFetchingAuthorizations] = React.useState(false);
React.useEffect(() => {
console.log("useEffect from protected route")
setIsFetchingAuthorizations(true);
axios.get(`${global.REST_API_ADDR}/api/pages/${encodeURIComponent(path)}`)
.then((response) => {
setAuthorized(response.data.authorized);
setIsFetchingAuthorizations(false);
})
.catch((error) => {
setIsFetchingAuthorizations(false);
console.log("Protected route use Effect error : ", error);
})
}, [path])
return (
<Route {...routeProps}
render={ props => {
if(isFetchingTheUser || isFetchingAuthorizations) return <div>Chargement...</div>
if(isAuthenticated && authorized.includes(currentUser.rank)){
return ContextProvider ? <ContextProvider><Component {...props} /></ContextProvider> : <Component {...props} />
}else if(isAuthenticated && !authorized.includes(currentUser.rank)) {
return <Unauthorized {...props} />;
}
else{
return <Redirect {...props}/>;
}
}}/>
);
};
export default ProtectedRoute;
If delete the part with useEffect() I no longer receive a warning in the console, nevertheless I really need this hook to check that the user has permission to access this page
Can someone please enlighten me ?
Thanks in advance
EDIT 1 :
I tried to create an empty component and accessed it via my same custom route (ProtectedRoute) and there is no warning, this "empty component" doesnt have any useEffect in it and it seems to be the problem in the others components... So I got this warning when I try to access a component with useEffect in it...
EDIT 2 :
With further testing I can affirm with certitude that the problem is coming from the useEffect in my ProtectedRoute component, if i manually set the "authorized" array and the "isFetchingAuthorizations" it works fine. The problem seems to come from the setter in the http request (if I comment only the setters it works fine too...)
EDIT 3 : I added an useEffect in my empty component to fetch all my users and display it, and it throw a warning like all others components. I guess the problem comes from having an useEffect in the component...
EDIT 4 : I added a console.log in my empty component's useEffect, it seems that the useEffect is triggered even tho the component is not returned ! It might be the problem here !
EDIT 5 : The problems seems to come from the fact 'authorized' state is keeped between each routing, so when the user ask for a new route the 'autorized' state is filled with the previous page authorizations array... don't know how to fix it, i'm trying to empty it after the route has been served... if aynone have some tips
I have one suggest for your case like this:
useEffect(() => {
let didCancel = false; // to trigger handle clean up
const fetchData = async () => {
try {
const result = await .get(`${global.REST_API_ADDR}/api/pages/${encodeURIComponent(path)}`);
if (!didCancel) {
setAuthorized(result...);
setIsFetchingAuthorizations(result...);
}
} catch (error) {
if (!didCancel) {
setIsFetchingAuthorizations(result...);
}
}
};
fetchData()
return {
didCancel = true; // clean up useEffect
}
})
Hope help you !
So here is what I think, you clean up your http requests before you unmount. I wrote something that might work for you, try it out and let me know. Also here is a helpful article on this https://medium.com/#selvaganesh93/how-to-clean-up-subscriptions-in-react-components-using-abortcontroller-72335f19b6f7
useEffect(() => {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
setIsFetchingAuthorizations(true);
const fetchData = axios.get(`${global.REST_API_ADDR}/api/pages/${encodeURIComponent(path)}`)
.then((response) => {
setAuthorized(response.data.authorized);
setIsFetchingAuthorizations(false);
})
.catch((error) => {
setIsFetchingAuthorizations(false);
console.log("Protected route use Effect error : ", error);
})
fetchData();
return () => {
source.cancel();
};
}, [path]);

Resources