React Router: Go back to specific page - reactjs

I have a settings page, with a few sub routes:
--/home
--/articles
--/settings
--/Account(default)
--/Help
--/About
I would like it in such a way: when I'm in any one of the sub pages under settings, and the browser go back in history, it should always go to the (default)Account page. Going back again will follow the normal browser history behavior.
What's the best way of handling it?

You can use history:
import createHistory from "history/createBrowserHistory"
const history = createHistory()
history.push("/account")
<Router history={history}/>

After quite some research, it seems there's no easy way of doing it, discussions in react-router github wouldn't disagree. A workaround seems to be the consensus. Which is a bit raw to my liking, but anything that works is good.
So instead of keeping every sub page under settings with their own route(to be handled by react-router), I have them controlled by state in settings component. And the following event handling is used:
componentDidMount = () => {
window.onpopstate= () => {
if(this.state.currentSubPage !== 'account-settings') {
this.props.history.push('/settings');
} else {
window.onpopstate = null;
}
};
}

Related

Prevent flash of wrong page in NextJS app after MSAL-React redirect to/from Azure AD B2C

Context & Reproducible Scenario
I'm using the combination of these libraries and tools:
NextJS 12+ (based on React 18+)
MSAL-Browser 2.25+ and MSAL-React 1.6+ (Microsoft's libs for OpenID login against Azure B2C)
I'm using the Auth Code + PKCE redirect flow so this is the flow for users:
They land on /, the home page
They click a /me router link
They go to Azure B2C to log in because said page has this logic:
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={loginRequest}>
where loginRequest.state is set to router.asPath (the "intended" page: /me)
Note that the page is also wrapped in a <NoSsr> component based off Stack Overflow.
User logs in on Azure B2C, gets redirected back to my app at / (the root)
â›” Problem: the user now briefly sees the / (home) page
After a very brief moment, the user gets sent to /me where they are signed in
The MSAL docs don't seem to have much on the state property from OIDC or this redirect behavior, and I can't find much about this in the MSAL sample for NextJS either.
In short: the issue
How do I make sure MSAL-React in my NextJS application send users to the "intended" page immediately on startup, without briefly showing the root page where the Identity Server redirects to?
Relevant extra information
Here's my custom _app.js component, which seems relevant because it is a component that triggers handleRedirectPromise which causes the redirect to intended page:
export default function MyApp({ Component, pageProps }) {
return (
<MsalProvider instance={msalInstance}>
<PageHeader></PageHeader>
<Component {...pageProps} />
</MsalProvider>
);
}
PS. To help folks searching online find this question: the behavior is triggered by navigateToLoginRequestUrl: true (is the default) in the configuration. Setting it to false plainly disables sending the user to the intended page at all.
Attempted solutions with middleware
I figured based on how APP_INITIALIZERs work in Angular, to use middleware like this at some point:
// From another file:
// export const msalInstance = new PublicClientApplication(msalConfig);
export async function middleware(_request) {
const targetUrlAfterLoginRedirect = await msalInstance.handleRedirectPromise()
.then((result) => {
if (!!result && !!result.state) {
return result.state;
}
return null;
});
console.log('Found intended target before login flow: ', targetUrlAfterLoginRedirect);
// TODO: Send user to the intended page with router.
}
However, this logs on the server's console:
Found intended target before login flow: null
So it seems middleware is too early for msal-react to cope with? Shame, because middleware would've been perfect, to allow as much SSR for target pages as possible.
It's not an option to change the redirect URL on B2C's side, because I'll be constantly adding new routes to my app that need this behavior.
Note that I also tried to use middleware to just sniff out the state myself, but since the middleware runs on Node it won't have access to the hash fragment.
Animated GIF showing the flashing home page
Here's an animated gif that shows the /home page is briefly (200ms or so) shown before /me is properly opened. Warning, gif is a wee bit flashy so in a spoiler tag:
Attempted solution with custom NavigationClient
I've tried adding a custom NavigationClient to more closely mimic the nextjs sample from Microsoft's repository, like this:
import { NavigationClient } from "#azure/msal-browser";
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/performance.md#how-to-configure-azuremsal-react-to-use-your-routers-navigate-function-for-client-side-navigation
export class CustomNavigationClient extends NavigationClient {
constructor(router) {
super();
this.router = router;
}
async navigateInternal(url, options) {
console.log('đź‘Ť Navigating Internal to', url);
const relativePath = url.replace(window.location.origin, "");
if (options.noHistory) {
this.router.replace(relativePath);
} else {
this.router.push(relativePath);
}
return false;
}
}
This did not solve the issue. The console.log is there allowing me to confirm this code is not run on the server, as the Node logs don't show it.
Attempted solution: go through MSAL's SSR docs
Another thing I've tried is going through the documentation claiming #azure/msal-react supports Server Side Rendering (SSR) but those docs nor the linked samples demonstrate how to solve my issue.
Attempted solution in _app.tsx
Another workaround I considered was to sniff out the hash fragment client side when the user returns to my app (and make sure the intended page is also in that state). I can successfully send the OpenID state to B2C like this...
const extendedAuthenticationRequest = {
...authenticationRequest,
state: `~path~${asPath}~path~`,
};
...and see it returned in the Network tab of the dev tools.
However, when I try to extract it in my _app.tsx still doesn't work. I tried this code from another Stack Overflow answer to get the .hash:
const [isMounted, setMounted] = useState(false);
useEffect(() => {
if (isMounted) {
console.log('====> saw the following hash', window.location.hash);
const matches = /~path~(.+)~path~/.exec(window.location.hash);
if (matches && matches.length > 0 && matches[1]) {
const targetUrlAfterOpenIdRedirect = decodeURIComponent(matches[1]);
console.log("Routing to", targetUrlAfterOpenIdRedirect);
router.replace(targetUrlAfterOpenIdRedirect);
}
} else {
setMounted(true);
}
}, [isMounted]);
if (!isMounted) return null;
// else: render <MsalProvider> and the intended page component
This does find the intended page from the state and executes routing, but still flashes the /home page before going to the intended page.
Footnote: related GitHub issue
Submitted an issue at MSAL's GitHub repository too.

Browser forward button not active with React.js

I have a react application using typescript that is set up like a wizard with different pages in each step. I have a 'Forward' and 'Back' button on the page that I created and they work just fine.
What the problem is the browser forward button, I got the back button working. Everytime i go back either using the browser button or my custom back button the forward button is always disabled.
on one page I have this code that fixes my back button
const [finishStatus, setFinishStatus] = useState(false)
const onBackButtonEvent = () => {
if (!finishStatus) {
setFinishStatus(true)
history.push('previous page')
}
}
useEffect(() => {
window.history.pushState(null, 'null', window.location.pathname)
window.addEventListener('popstate', onBackButtonEvent)
return () => {
window.removeEventListener('popstate', onBackButtonEvent)
}
}, [])
can i do something similar to this for the forward button?
I have tried several solutions I have found but nothing is working.
Thanks
Because the browser's forward button is only enabled when the user is navigated back in the browsing history. Which isn't what's happening here:
history.push('previous page')
You're navigating the user forward to a page they were previously viewing, but it's still forward navigation. The browser isn't going to intuitively look at the page and see if it's familiar based on the user's browser history.
To navigate the user backward in standard JavaScript, you might do something like:
history.go(-1)
or:
history.back()
Or, if your history object is imported from react-router-dom or something of that nature, it might be:
history.goBack()
You may need to check the vendor documentation depending on the nature of your history object.

Check cookie is exist in React

I have used js-cookie in my React application, here I just use a simple condition to check whether the cookie is matched or not and push history to the following route, here is my code:
const readCookie = () => {
let cookie = false;
if(cookie = Cookies.get("isLogged")){
history.push('/Homepage');
setAuthenticated(true);
}else{
history.push('/');
}
}
useEffect(() => {
readCookie()
},[]);
But the problem is, if I have many routes, so I just have to write many if conditions as much as my existing routes to check and push to that specific route, I think it doesn't make sense to do that. So, I just want to know that is there any easier way? To check the cookies and push to their paths or routes.
Yes, you need to create Public and Private Routes please follow the steps mentioned in the link below:
https://medium.com/wesionary-team/private-and-public-routes-with-react-router-e5f814ccff2f
and you can also check this repo
https://github.com/thanhbinhtran93/react-router-example

How to track pageviews on React

So basically the problem i have is that my site is using reactjs so it doesn't reload and i need to detect the change on the URL in order to send a new pageview to Google Analytics. Has anyone deal with this before?
EDIT: Maybe i was a little unclear, im using Google Tag Manager and i have no control over the code on the page but i can request for dataLayers.
The obvious solution is to ask for a dataLayer when the page changes but since i would like to avoid doing this i was strictly asking if anyone knows a way to detect this kind of changes on the DOM from GTM.
For single page apps, you can track 'virtual' pageviews like given in the docs:
When the page changes, do
ga('set', 'page', '/new-page.html');
After this point, if your do a 'send', it will track this page.
ga('send', 'pageview');
I'd suggest you use something like react-ga for doing it a little more conveniently, it has functions like
ReactGA.pageview('/about/contact-us');
and ReactGA.modalview('/about/contact-us');
You need to look for all the paths using a Route like this,
<Route path="/" component={updateTracking} />
<Switch>
..... //further actual routes
</Switch>
and then send the pageview using the global Google Analytics (ga) method (accessible through the window object) using window.location.pathname
const updateTracking = () => {
window.ga('send', 'pageview', {
page: window.location.pathname
});
}
Note: You need to put the tracking code you got from Google Analytics in the main HTML (index.html) for it all to work.
Alternatively, you can listen for createBrowserHistory changes and send a pageview event.
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
history.listen(() => {
window.ga('send', 'pageview', {
page: window.location.pathname
})
}
)
The useLocation hook was added in 5.1.0 & can be used:
In your App.js, import the following
import React, { useState } from 'react';
import { useLocation } from "react-router-dom";
The following assumes you have created the react application using create-react-app or some other mechanism to pick GA_ID in your application from ENV
In your Environment variables, make sure you have : REACT_APP_GA_ID
function initialiseAnalytics() {
const TRACKING_ID = process.env.REACT_APP_GA_ID;
ReactGA.initialize(TRACKING_ID);
}
function usePageTracking() {
const location = useLocation();
const [initialized, setInitialized] = useState(false);
useEffect(() => {
initialiseAnalytics();
setInitialized(true);
}, []);
useEffect(() => {
if (initialized) {
ReactGA.pageview(location.pathname + location.search);
}
}, [initialized, location]);
}
Finally, in your App function, call the page tracking -
function App() {
usePageTracking();
// Other Code Goes Here
return (
<div className="App">
<p>Sample Application</p>
</div>
);
}
Anyone still looking for the answer, here it is.
This is the expected behavior for Single Page Application(SPA) like React.
Solution is to update the Tracker when your route changes
You can deal with this problem in two ways
1- In all your route containers use set command from ga and update the tracker
ga('set', 'page', '/new-page.html');
2- In all your route containers use send command from ga and update the tracker
ga('send', 'pageview', '/new-page.html');
The second solution using send command is not recommended by Google Analytics
This is because fields passed via the send command are not set on the tracker—they apply to the current hit only. Not updating the tracker will cause problems if your application sends any non-pageview hits (e.g. events or social interactions), as those hits will be associated with whatever page value the tracker had when it was created.
Ref: Google Anaylytics

React Router getHistory

It is possible to get the list of browser history? For example, if I navigate as follows: page1 -> page2 -> page1 -> page5, I want to be able to get an array of ["page1", "page2", "page1", "page5"] (so effectively history.pop() should give the URL of goBack()
There's no such API definition or implementation in either History object of browser or history library that used by react-router. It will cause some security problems (considering the situation of cross-domain).
However, you can listen for changes of location and store them manually. It won't bring security issues since the operations are all limited in a single domain:
history.listen((location, action) => {
console.log(action, location.pathname, location.state);
// you can store the result anywhere you like
const prevUrls = window.sessionStorage.getItem('prevUrls') || [];
prevUrls.push(location.pathname);
window.sessionStorage.setItem('prevUrls', prevUrls);
})
Then you can use window.sessionStorage.getItem('prevUrls') to get list of history.

Resources