react router prevent calling unstable_usePrompt if search params changed - react-router

I'm trying to prevent user from abandoning the page if he has some unsaved changes.
However, react router seems to trigger this prompt if the search params change, even if I am on the same page.
const navigation = useNavigation();
unstable_usePrompt({
when: didValuesChange(),
message:
"Are you sure you want to navigate away from this page? You have some unsaved changes!",
});
I need to check if the current url is the same as the next url without the search params and then skip the prompt, but I can't figure out how to do that.
useNavigation hook returns the location property which states: "This tells you what the next location is going to be."
However, it is always undefined when I try to navigate to other pages.
Docs
For example, setting "{activeTab: 9}" (with useSearchParams) params to "/tours/5" triggers the prompt that I am leaving the page. Even if it is the same page. How can I prevent that trigger by comparing the current route without params with the next route? (Or some other way)
React router v6.8.1
const notOnSamePage = () => {
//?
}
unstable_usePrompt({
when: didValuesChange() && notOnSamePage(),
message:
"Are you sure you want to navigate away from this page? You have some unsaved changes!",

Related

Infinite Redirect Loop on Google One Tap Signin

I'm having trouble finding any documentation in regards to Google One Tap UX and how to persist signin state after a signin redirect. I am using the html api, check the code here:
setTimeout(function () {
let target = document.getElementById('google-signin');
target.innerHTML = '<div id="g_id_onload" data-client_id="x" data-context="signin" data-login_uri="https://x/account/google/callback" data-auto_select="true" data-itp_support="true"></div>';
var s = document.createElement("script");
s.src = 'https://accounts.google.com/gsi/client';
document.head.appendChild(s);
console.log('appended script', s);
}, 30000);
</script>
Essentially I am delaying this signin popup for 30 seconds, that part works fine but soon after this is what happens:
Sign in occurs
Redirect happens
Server redirects back to the referer page
After 30 seconds the process starts again
I would have assumed the google sdk would set a cookie or something somewhere but I guess it does not, either that I'm supposed to handle persisting signin state through my own means. I just want to know the correct approach here.
My question is: How does google know if a user has already signed in using Google One Tap UX?
Figured out a solution. Google allows you to put a property on your div tag called data-skip_prompt_cookie="yourcookie" this will skip the one tap prompt if that cookie is present with a truthy value.
What I did was on my server callback in asp.net I added a cookie to the response. This ensures the prompt is only disabled once someone actually signs in.
Response.Cookies.Append(
"yourcookie", "true");
This ensures when my server redirects back to the originating page, the cookie exists and the one tap does not show up again

How to remember user choices in react router?

I am using react router to build a single page app.Now, everything is fine, except remembering the previous status. I mean: each time when user chooses one option in a select dom, then click a link component to jump to another route path; then, if this user clicks back button from browser to go back to the previous page, he can not see his previous choice, everything lost!
The desired behavior you are talking about is about saving the State of the Component. Even though the router renders the desired Component, it does not save the State of that Component.
However, you can pass in a desired State via URL Parameters, but that doesn't like a solution for you due to what exactly you are trying to achieve.
Passing data in through the URL to achieve the state may sound like a good idea, but this won't work due to the URL having to be updated as the User selects different options, and this will trigger new rendering. This will happen because you are currently on that route.
//the route path can be defined like this:
localhost:123/#/userselection/:selectedid
When you navigate it will look something like this
localhost:123/#/userselection/1
localhost:123/#/userselection/2
localhost:123/#/userselection/3
etc.
//Now if you are on
localhost:123/#/userselection/2
and the User changes to selectionid 3, you will have to trigger a render
which is not usually desired behaviour
An idea may be to have some sort of ComponentStateService that is declared at a root level of your application. Use this Service to set and read desired state. So when the User selects something, have an Object with the Component Name, and its stateData.
export default class ComponentStateService{
_stateData = {};
constructor(defaultState = null){
if (defaultState){
this._stateData = defaultState;
}
}
getComponentStateData(componentName){
if (this._stateData){
return this._stateData[componentName];
}
}
setComponentStateData(componentName, stateData){
this._stateData[componentName] = stateData;
}
}
Then initialize it something like this at a high level:
import ComponentStateService from './componentStateService';
var componentState = new ComponentStateService(//default state passed in here);
export default componentState;
Then to use it, import it to each component
import componentState from '../index';
// call this to set the state
componentState.setComponentStateData("component1", {});
// and retrieve state
let savedState = componentState.getComponentStateData("component1");
Then you retrieve or update as necessary. You can make use of the LifeCycle Hooks that react has in order to read and set the data before the Comopnent is Rendered. Also you will need to ensure that you are correctly updating the data for the state upon User changing their selection.
As you can see though, the major flaw is you cannot share your link and expect someone else to see what you see, since it will persist in a Users Browser Session.
You can create a base class that imports this, so you don't have to import it every time.
You can also dump the entire state to local storage so it persists beyond a session, and load it up on app start

Keycloak redirect_fragment conflicts with react-router hashRouter

I'm running into an issue with the redirection that happens after a user of my app authenticates with Keycloak.
My app uses react-router hashRouter. When the initial redirect happens, I get a redirect_fragment that looks something like this:
http://localhost:3000/lol.html?redirect_fragment=%2F&redirect_fragment=%2Fstate%3D1c5900ee-954f-4532-b01c-dcf5d88f07a2%26code%3DKZNXVqQCcIXTCFu2ZIkx4quXa6zJb59zGKpNIhZwfNo.d2786d1e-67cd-437f-a873-bad49126bad4&redirect_fragment=%2Fstate%3D51a9cb44-b80a-4c14-8f3d-f04dfdb84377%26code%3Dp5cKQ7xVCR_n1s4ucXZTSE3O1T5lwNri_PBKD07Mt1Y.63364a83-f04f-4e64-a33e-faf00f6cd4ff&redirect_fragment=%2Fstate%3D05155315-ab60-4990-8d4e-444c7cce9748%26code%3DBxxpf_uMB28rKAQ6MXFTTrL9RE4rC3UtwCMXLu_K1Zo.4ce56da0-8e52-47e3-a0f2-4f982599bb98#/state=f3e362e4-c030-40ac-80df-9f9882296977&code=8HHTgd3KdlfwcupXR_5nDV0CqZNPV1xdCu3udc6l5xM.97b3ea71-366a-4038-a7ce-30ac2f416807
The URL keeps growing from there. I've read a few posts already that indicate that redirection from keycloak might have a problem with client-side routing via location.hash ... Any thoughts would be appreciated!
I think I figured it out!
The redirection loop seems to stop if I use the 'noslash' hashRouter instead of the default which contains a slash.
My URLs look like this: localhost:3000/lol.html#client/side/route
instead of this: localhost:3000/lol.html#/client/side/route
The redirection now seems to terminate appropriately after one redirect, but now I'm running into a different problem where the hash portion of my route is not being honored by react-router...
EDIT: I figured the second issue out
react-router creates a wrapper around window.location that it uses to tell which client side "page" it is currently on. I found that this wrapper was out of sync with window.location.
Check this console output out. This was taken immediately after the redirection resolved (and the page was blank):
history pathname is /state=aon03i-238hnsln-soih930-8hsdlkh9-982hnkui-89hkgyq-8ihbei78-893hiugsu
history hash is (empty)
window.location pathname is /lol.html
window.location hash is #users/1
The state=blah-blah-blah in the history.pathname is part of the redirect url that keycloak sends back after auth. You'll notice that window.location is updated to the correct path / hash, but that history seems to be one URL behind. Maybe keycloak directly modifies window.location to perform this redirection?
I tried using a history.push(window.location.hash) to push the hash fragment and update react router, but got the error "this entry already exists on the stack". Since it clearly is not on the top of the location stack, this led me to believe that react-router compares window.location with its internal location to figure out where it ultimately is. So how did I get around this?
I used history.replace() instead, which just replaces the entry on the top of the stack with a new value, instead of pushing a new entry to the stack. This also makes sense, since we don't want users who navigate "back" in their browsers to go back to that /state=blah-blah-blah url <-- replace eliminates this entry from the history stack.
One final piece: react-router history.location, like window.location, has both pathname and hash components. HashRouter uses the history.location.pathname component to keep track of the client side route after the hash in the browser. The equivalent of this in window.location is stored in window.location.hash, so we will be using this as the value passed to history.replace() instead of window.location.pathname. This confused me for a bit, but makes sense when you think about it.
react-router history also keeps track of its current route with a prepended / instead of a prepended #, since it's just treating it like any normal URL. Before I called history.replace(), I needed to take my window.location.hash, replace the leading hash with a / and then pass that value history.replace()
const slashPath = window.location.hash.replace('#', '/');
history.replace(slashPath);
Whew!

How to have react-router in electrode app push in an action file

I have a react app using the walmart electrode framework with uses react router. My question is
a) how can push a route during an action trigger? I tried importing push from react-router but I got a method not found error. I tried instead to use browserHistory.push and that sets the url but for some reason login renders only at /#/login?_k=jazzx rather than at /login.
b) how can I get it to do the /resoure urls rather than the hash #/resource urls. It's a single page app. I realize that it's doing that because its a single page app, but Is there a setting for that? Whats the best practice?
c) what is the querystring that electrode is attaching to things - is that for dev only?
export const tryLogin = (returnUrl = '/') => {
return (dispatch) => {
browserHistory.push('/login'); //this doesn't seem to render the route /#/login_k=somestring does work
return dispatch(createLoginAction({ returnUrl }));
};
}
;
The URL /#/login?_k=jazzx implies that you are using a hash history, but you are attempting to change the URL using browserHistory. You should be passing the browserHistory to your <Router> if you want to use the browser history API (aka have clean URLs).
If you use the browserHistory, you will need to have some sort of code on your server to handle routing set up.

What's the replacement for history's useBeforeUnload to show a confirmation dialog in react-router?

In the example here and in this issue comment it is suggested to use the useBeforeUnload function of history.
However, this function is gone. It seems to be removed without replacement in the current 4.x version:
Removed the "middleware" API (i.e. all "use" functions).
What's a good way to handle catch these "before unload" cases now in order to show a confirmation dialog?
Taking a look at the code for history's createBrowserHistory, it looks like the block function does what you need. From the docs:
// Register a simple prompt message that will be shown the
// user before they navigate away from the current page.
const unblock = history.block('Are you sure you want to leave this page?')
// Or use a function that returns the message when it's needed.
history.block((location, action) => {
// The location and action arguments indicate the location
// we're transitioning to and how we're getting there.
// A common use case is to prevent the user from leaving the
// page if there's a form they haven't submitted yet.
if (input.value !== '')
return 'Are you sure you want to leave this page?'
})
// To stop blocking transitions, call the function returned from block().
unblock()
https://github.com/mjackson/history#blocking-transitions