How to detect history change action (back, forward, etc) in react-router-dom v6? - reactjs

All I could find concerning listening to the location change is a code like:
const location = useLocation();
useEffect(() => {}, [location]);
But the issue I'm trying to solve is to understand whether the user went back or forward on the page, which the location object doesn't provide by itself.
My business requirement is to log out the user when they press "back" and the "back" page was a log in page which successfully logged the user in. I solved flagging the location via location.state to understand whether it's a post-successful-login route. Now I want to listen to the "back" action in the browser and emit the logout flow based on location.state.
If you have a better solution I'm all ears.

If you only need to know how a user got to the current page then react-router-dom#6 has a useNavigationType hook that returns the type of navigation.
useNavigationType
declare function useNavigationType(): NavigationType;
type NavigationType = "POP" | "PUSH" | "REPLACE";
Example:
import { NavigationType, useNavigationType } from 'reat-router-dom';
...
const navigationType = useNavigationType();
...
switch (navigationType) {
case NavigationType.POP:
// back navigation
case NavigationType.PUSH:
// forward navigation
case NavigationType.REPLACE:
// redirection
default:
// ignore
}
Example:
import {
NavigationType,
useLocation,
useNavigationType.
} from 'reat-router-dom';
...
const navigationType = useNavigationType();
const location = useLocation();
useEffect(() => {
if (location.pathname === "/login" && navigationType === NavigationType.POP) {
// dispatch logout action
}
}, [location, navigationType]);
If you want to couple listening for navigation actions and calling a callback then consider something like this answer.

Related

How to run a function when user clicks the back button, in React.js?

I looked around and tried to find a solution with React router.
With V5 you can use <Promt />.
I tried also to find a vanilla JavaScript solution, but nothing worked for me.
I use React router v6 and histroy is replaced with const navigate = useNavigation() which doesn't have a .listen attribute.
Further v6 doesn't have a <Promt /> component.
Nevertheless, at the end I used useEffect clear function. But this works for all changes of component. Also when going forward.
According to the react.js docs, "React performs the cleanup when the component unmounts."
useEffect(() => {
// If user clicks the back button run function
return resetValues();;
})
Currently the Prompt component (and usePrompt and useBlocker) isn't supported in react-router-dom#6 but the maintainers appear to have every intention reintroducing it in the future.
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:
useBackListener(({ location }) =>
console.log("Navigated Back", { location })
);
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.

How can I stay the user in the same page?

Every time I reload the my account page, it will go to the log in page for a while and will directed to the Logged in Homepage. How can I stay on the same even after refreshing the page?
I'm just practicing reactjs and I think this is the code that's causing this redirecting to log-in then to home
//if the currentUser is signed in in the application
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged(userAuth => {
unsubscribe();
resolve(userAuth); //this tell us if the user is signed in with the application or not
}, reject);
})
};
.....
import {useEffect} from 'react';
import { useSelector } from 'react-redux';
const mapState = ({ user }) => ({
currentUser: user.currentUser
});
//custom hook
const useAuth = props => {
//get that value, if the current user is null, meaning the user is not logged in
// if they want to access the page, they need to be redirected in a way to log in
const { currentUser } = useSelector(mapState);
useEffect(() => {
//checks if the current user is null
if(!currentUser){
//redirect the user to the log in page
//we have access to history because of withRoute in withAuth.js
props.history.push('/login');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[currentUser]); //whenever currentUser changes, it will run this code
return currentUser;
};
export default useAuth;
You can make use of local storage as previously mentioned in the comments:
When user logs in
localStorage.setItem('currentUserLogged', true);
And before if(!currentUser)
var currentUser = localStorage.getItem('currentUserLogged');
Please have a look into the following example
Otherwise I recommend you to take a look into Redux Subscribers where you can persist states like so:
store.subscribe(() => {
// store state
})
There are two ways through which you can authenticate your application by using local storage.
The first one is :
set a token value in local storage at the time of logging into your application
localStorage.setItem("auth_token", "any random generated token or any value");
you can use the componentDidMount() method. This method runs on the mounting of any component. you can check here if the value stored in local storage is present or not if it is present it means the user is logged in and can access your page and if not you can redirect the user to the login page.
componentDidMount = () => { if(!localStorage.getItem("auth_token")){ // redirect to login page } }
The second option to authenticate your application by making guards. You can create auth-guards and integrate those guards on your routes. Those guards will check the requirement before rendering each route. It will make your code clean and you do not need to put auth check on every component like the first option.
There are many other ways too for eg. if you are using redux you can use Persist storage or redux store for storing the value but more secure and easy to use.

Disable routing to a specific page directly in Gatsby.js

I have success page which has to be displayed when the form is submitted in Gatsby. But the user can directly visit /success page. Is there any way to prevent it?
There are a couple ways to do this. One is outlined here by Gatsby and involves creating a client-only route (that is, Gatsby won't have React render it server-side, but will route to your component on the client/browser). That looks something like this:
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/app/)) {
page.matchPath = "/app/*"
// Update the page.
createPage(page)
}
}
A route I've taken to do this, especially when it's not a particularly sensitive page, is to render the page, but check for some condition on render and use navigate if the condition isn't satisfied.
For example, if you stored formSubmitted in AppContext, you could do something like this:
import React, { useContext } from "react"
import { navigate } from "gatsby"
const SuccessPage = () => {
const { formSubmitted } = useContext(AppContext)
return formSubmitted ? <div>Thanks!</div> : navigate("/contact", { replace: true })
}

How do I send custom data in react-router goBack method like push and replace methods?

I am using react-router-dom v4 and able to send custom data to the new screen using push(path, state) and replace(path, state) methods in "props.history.location"
I want to send the data back to the previous screen but could not achieve using go(n) or goBack() or goForward().
How can I solve the scenario when I need to send data back to the previous screen?
I don't think you can pass params directly when going back.
However, it is possible to accomplish this in different ways.
You can implement redux and have an action/reducer for this. (requires some boilerplate work)
Or easier, you can store your data in localstorage and read it in the page. Be careful to use async for localstorage when reading data.
These two should work.
use replace instead of push maybe.
I faced the same problem and I didn't find any official suggestion to achieve this, following is my workaround.
In the following code snippet I keep track of locations in history stack(since there is no direct way to get the history stack)
import {Switch, useHistory} from 'react-router-dom';
import { Action, History, Location } from "history";
const history = useHistory();
const locations = [];
locations.push(history.location);//since the first location is not passed to the listener we have to initialize the locations like this to keep record of the first page that user loads
history.listen((location: Location, action: Action) => {
if (Action.Push == action) {
locations.push(location);
} else if (Action.Pop == action) {
locations.pop();
} else if (Action.Replace == action) {
locations.pop();
locations.push(location);
}
})
Then I write the following back method which receives a payload that is going to be passed to the previous page.
import _ from 'lodash';
export const back = (state) => {
if (!history) {
throw 'History was not set';
}
const uniqueLocations = _.uniqBy(locations, (l: Location) => l.key);//for some reason there will be duplicates in the locations array, using uniqBy method of lodash, we can remove the duplicates
if (uniqueLocations.length >= 2) {
const targetLocation = uniqueLocations[uniqueLocations.length - 2];
history.go(-1);
history.push({
...targetLocation,
state: {
...targetLocation.state,
backState: state,
}
})
}
}
Now calling the back() method will pass the state argument of the back method to the previous screen which can be accessed like this: props.location.state.backState in the previous screen.
You could pass the information about the previous route as state whenever you change the page. To avoid code duplication, you might create a custom hook that wraps history-push. It could look something like this:
import { useHistory, useLocation } from 'react-router-dom'
export const useRouter = () => {
const history = useHistory()
const location = useLocation()
const push = (url: string, state?: any) => {
history.push(url, { ...state, from: location.pathname })
}
return { push }
}
Now, when changing the page using the custom hook:
const { push } = useRouter()
<button onClick={() => push("/profile")}> Go to profile </button>
You will be able to access the previous route:
const location = useLocation()
console.log(location.state) // { from: '/previous-path' }
Having from in state, you could navigate to the previous page and pass some info there:
push(location.state.from, { foo: 'bar' })
What you are asking is complicated because security reasons. The app should not know what happen when somebody clicks "back" - so you shouldn't be able to manipulate with that state either.
BUT
here is a workaround doing what you need:
history.replace(`/previous/url`, { state });
setTimeout(() => history.goBack());
you will need to have stored the previous url
the state will be available only for first render, so you have to store it in state
goBack() or go(-1) are used to Go back to the previous history entry. If you want to send new data to the previous route, you might as well use history.push({pathname: prevPath, state: {data: 1}});
the prevPath is what you might pass on to the nextRoute using a location state.

How to know if react-router can go back to display back button in react app

There seems to be no API way to detect if you can go back from the current page in an app. One can check the history length, but that includes the forward and backward stack.
I want to display the back button only when the user actually can go back.
You can check location.key if you have a location key that means you routed in-app. But if you don't that means you come from outside of the app or you just open the app in a new tab etc.
import { useHistory, useLocation } from "react-router-dom";
const history = useHistory();
const location = useLocation();
<Button
onClick={() => {
if (location.key) {
history.goBack();
}
}}
disabled={location.key}
/>;
For V6 and newer:
import { useHistory, useLocation } from "react-router-dom";
const history = useHistory();
const location = useLocation();
<Button
onClick={() => {
if (location.key !== 'default') {
history.goBack();
}
}}
disabled={location.key === 'default'}
/>;
You can do it by checking history.action if its value is POP, then there is no route to go back. That's the only way I found.
<Button
onClick={() => {
if (history.action !== 'POP') history.goBack();
}}
disabled={history.action === 'POP'}
/>
I think I found a workaround
There is onpopstate event:
Calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event is only triggered by performing a browser action, such as clicking on the back button (or calling history.back() in JavaScript), when navigating between two history entries for the same document.
And I checked it: react router saves at state a key property:
On history walking it may contains a data like:
event.state: {key: "jr2go2", state: undefined}
And when we go back to last page on wich we entered the site or app, this state property will be null:
event.state: null
,
So we can handle it with redux + rxjs (for example) like this:
// epic
export const handleHistoryWalkEpic = () =>
fromEvent<PopStateEvent>(window, 'popstate')
.pipe(
map(event => setCanGoBack(!!event.state)),
)
// reducer
case NavigationBarActionsTypes.SET_CAN_GO_BACK:
return {
...state,
canGoBack: action.payload.canGoBack,
}
And you should set canGoBack to true on any push history action, it's possible with your any router handler component:
componentWillUpdate(nextProps: any) {
let { location } = this.props;
if (nextProps.history.action === 'PUSH') {
this.props.setCanGoBack()
}
}
You can do it by checking if location.history is > 2.
Example to show or not the back button in your app:
onBack={ history.length > 2 ? () => { history.goBack() } : null }
it gives 'POP' and 'PUSH' value using this.props.location.action

Resources