Browser forward button not active with React.js - reactjs

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.

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.

update state of the parent screen if an update occurs in current screen

I am using react-navigation 6 and react-native CLI, to make a chat application. I want to achieve this feature which is common in every chat application that when we send a message to someone, and go back to homescreen of the app (where all conversations are listed), the last message sent, can be seen.
Like if I sent message and pressed the back button, it will navigate me to home screen where all my conversations are, and it should update the conversation where I sent the message.
I have tried route.params, but it gives a warning that non-serializable values found.
React navigation warning
Also, I have heard that passing setState function to child component is very bad practice as it is mentioned here
I also tried navigation_events along with useEffect , this was a surprise to me that it didn't work either.
When I refresh the screen manually, or logout and log in, then it refreshes completely, but doesn't when I go back from application.
React.useEffect(() => {
navigation.addListener('focus', e => {
fetchConvos();
});
return () => {};
}, []); //also tried [navigation] instead of []
const fetchConvos = () => {
fetch('http://localhost:15000/' + id + '/conversations', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
})
.then(res => res.json())
.then(received => {
if (received?.response !== false) {
setConversations(received);
}
});
};
I have checked the id , received and even setConversations, they all are updating, but my screen still rendering the old messages.
Also, I don't want to use Async Storage or redux for this simple problem like this.
I can share the complete code if this isn't enough.
EDIT
I figured out one more way to update it may help clarify the situation more.
React.useEffect(() => {
navigation.addListener('focus', e => {
setConversations([]); //first setting convos to empty
fetchConvos(); //then fetching new data
});
return () => {};
}, []);
But this method is quite slow as I am updating the state of conversations twice.
I would appreciate if someone can help me here.
By taking the last 2 samples of code, I'd go down the route of setting the state to change the data. Like, I don't know the structure of your code completely. But i'm assuming you're using useEffect inside some component, right? In that case, React Context might be what you're looking for:
How to use react hooks on react-native with react-navigation
It allows to share informations, without having to build a structured store like redux. You should probably working on redesign a bit the code as, if you're following the current logic, you're going to split data pool of the conversation in the menu and load them when the "back" navigation event occurs, right?
Whilst the conversation data should be shared and available to both components, regardless where you're.
At least I'd rethink it this way to allow consistent data throughout the whole application.
Unless you've to do something specific and on-spot, of course.

undefined object error when using react navigation to go back a screen after deleting data

I have a simple location tracking app built with react native, react navigation and mongoose. A user can save a series of their locations in tracks, which are listed on their home screen. When a user presses on any of these tracks, they are taken to a 'track detail' screen, which displays more information about that track. This track detail page also contains a delete button:
<Button
title="Delete Track"
onPress={() => {
navigation.goBack(); // go back to home screen
deleteTrack(_id) // function to delete the track via mongoose
}}
/>
The deleteTrack() function is very simple and uses Axios to connect to my backend, which uses mongoose to delete the record with the id provided.
const deleteTrack = async (id) => {
await trackerApi.delete("/tracks", { data: { id } })
}
Both of these functions work perfectly individually ie, the deleteTrack function removes the correct data via mongoDB and the navigation.goBack() successfully takes user back to the home screen.
HOWEVER, when these two functions are combined, I get:
TypeError: undefined is not an object (evaluating 'track.locations')
This error is located at:
in TrackDetailScreen (at SceneView.tsx:122)
in StaticContainer
in StaticContainer
...etc
What seems to be happening, is that the data is deleted on MongoDb, but the app is still trying to render the track detail screen with the deleted data. I'm not sure why this would be the case when I have left the screen via react navigation? I have tried a number of fixes, included conditional rendering and using await to delay the deleteTrack function until user has navigated away, but I get that same error. What am I doing wrong?

Google One Tap SignIn Popup not showing

I was trying to implement Google One Tap SignIn in my project. At the first time after building the project the google one tap prompt will display. But next time onwards if we refresh the page also the prompt is not displaying.
Here is my code snippet.
import { addScript } from 'Util/DOM';
/**
* Loads One Tap Client Library
*/
const loadOneTapClientLibrary = async() => {
await addScript('https://accounts.google.com/gsi/client');
}
/**
* Loads One Tap Javascript API
* #param {*} resolve
*/
const loadOneTapJsAPI = (resolve) => {
window.onload = () => {
google.accounts.id.initialize({
client_id: "My client Id",
callback: data => resolve(data)
});
google.accounts.id.prompt();
}
}
export const loadOneTap = async() => {
return new Promise( (resolve, reject) => {
loadOneTapClientLibrary();
loadOneTapJsAPI(resolve);
})
}
After page loads i am calling loadOneTap();
To avoid One Tap UI prompting too frequently to end users, if users close the UI by the 'X' button, the One Tap will be disabled for a while. This is the so-called "Exponental Cool Down" feature. More detail at: https://developers.google.com/identity/one-tap/web/guides/features#exponential_cool_down
I believe you triggered this feature during development. To avoid this, use Chrome incognito mode (and restart the browser when necessary).
As noted by Guibin this is the OneTap exponential cool down feature, it can be easily triggered during development when testing auth flow, but also legitimately when the end user clicks the close icon by mistake. On sites where Google login is optional this might seem pragmatic (i.e. the user genuinely wants to dismiss the popup prompt in favor of alternative login methods), however on a site where Google is the sole login identity provider and you are using the Javascript API instead of HTML api then this can manifest as broken functionality - i.e. no login prompt - and you want to avoid telling your users to use incognito or clear cache/cookies at all costs..
You can potentially handle this with some fallback logic..
window.google.accounts.id.prompt((notification) => {
if(notification.isNotDisplayed() || !notification.isDisplayed()) {
// #ts-ignore
const buttonDiv = window.document.createElement("div")
buttonDiv.setAttribute("id", "googleLoginBtn")
document.getElementsByTagName('body')[0].appendChild(buttonDiv);
// #ts-ignore
window.google.accounts.id.renderButton(
document.getElementById("googleLoginBtn"),
{ theme: "outline", size: "large" } // customization attributes
);
}
This renders a button to login that isn't subject the one-tap cool down feature. We're early days into playing with this so there may be other invariants with regards to state you need to consider (e.g. can isNotDisplayed return true when already logged in) - we already observed some oddities where isDisplayed and isNotDisplayed can both be false on the same invocation of the callback.
Extra note: I recall reading the user can disable all one tap features too, so if you're using the javascript API instead HTML api you will need the fallback to SignIn with Google button.

React Router: Go back to specific page

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;
}
};
}

Resources