Next.js behavior on back button pressed - javascript

I have a page I am trying to fix in order to keep scroll position when user presses back button (browser). Let's say I have a component called list, where I show the user some products. To see all the products the user can scroll down the list component. When the user clicks on some product, the application redirects the user to the detail component. Then when the user tries to go back to the list, hits the back button of the browser, the list component gets rendered and it seems like it scrolls to top automatically.
As far as I know, pressing the back button of the browser triggers a window.history.back() action, nothing else happens.
For a solution, I have implemented a variable in the context of my application that saves the scrollY value and then, in the componentWillMount (or useEffect) of the component I am trying to render (list component), I set the scroll position to the value set in the context.
Details of my solution are here, as I have based my entire code in this stack overflow's post:
How to change scroll behavior while going back in next js?
I have checked the value using some logs and the scroll position is saved correctly in the context, however, as I am using a window event listener, it sets the value to zero just after the list component is rendered.
In my code I am not using any kind of scroll configuration, so I was wondering if that behavior is some sort of default for either Next.js or react. It happens when the user hits the back button of the browser, but I am a newbie to next and I don't know if I am missing something or what, I don't even know if this issue has something to do with React or Next.js itself.

This gist may be of assistance as it includes a custom hook to manage scroll position: https://gist.github.com/claus/992a5596d6532ac91b24abe24e10ae81
import { useEffect } from 'react';
import Router from 'next/router';
function saveScrollPos(url) {
const scrollPos = { x: window.scrollX, y: window.scrollY };
sessionStorage.setItem(url, JSON.stringify(scrollPos));
}
function restoreScrollPos(url) {
const scrollPos = JSON.parse(sessionStorage.getItem(url));
if (scrollPos) {
window.scrollTo(scrollPos.x, scrollPos.y);
}
}
export default function useScrollRestoration(router) {
useEffect(() => {
if ('scrollRestoration' in window.history) {
let shouldScrollRestore = false;
window.history.scrollRestoration = 'manual';
restoreScrollPos(router.asPath);
const onBeforeUnload = event => {
saveScrollPos(router.asPath);
delete event['returnValue'];
};
const onRouteChangeStart = () => {
saveScrollPos(router.asPath);
};
const onRouteChangeComplete = url => {
if (shouldScrollRestore) {
shouldScrollRestore = false;
restoreScrollPos(url);
}
};
window.addEventListener('beforeunload', onBeforeUnload);
Router.events.on('routeChangeStart', onRouteChangeStart);
Router.events.on('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => {
shouldScrollRestore = true;
return true;
});
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
Router.events.off('routeChangeStart', onRouteChangeStart);
Router.events.off('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => true);
};
}
}, [router]);
}

Looking at your url, using shallow routing could solve the problem. Where the URL will get updated. And the page won't get replaced, only the state of the route is changed. So you can change your logic according to that.
A good example is in the official documentation:
https://nextjs.org/docs/routing/shallow-routing
And you might use display: 'hidden' to hide and show your components conditionally according to your state!
It's a way around but it could be even more useful depending on your exact situation !

After looking for another solution that does not use the window.scroll and similar methods, I have found a solution.
1st solution (worked, but for me that I have an infinite list that is loaded via API call, sometimes the window.scroll method wasn't accurate): I take the window.scrollY value and set it in the session storage, I did this before leaving the list page, so in the details page, if user hits the back button, at the moment the page is loading, I get the Y value from session storage and use the window.scroll method to force the page to scroll to the previously configured value.
As I mentioned earlier, this worked, but in my case, I have a list that is populated from an async API call, so sometimes the page loaded without all the images and the scroll was already configured, then the images and data were loaded and the user ended up seeing some other place in the page rather than the desire position.
2nd solution: In my case we are talking about a e commerce app, so I found this solution useful as it focuses in a particular item with its corresponding ID instead of the Y coord of the window. Scroll Restoration in e commerce app

Related

restoring scroll position in react project(hooks)

I have searched a few answers regarding my issue but found one relevant. Unfortunately, it is even on the class component, so I want to restore the scroll position after navigating back on the functional component. Here I will share the source code link on Stackblitz
There are few issues here.
First:
setPosts(res.data.slice(0, 20), handleScrollPosition());
You see, setter in useState does not have second argument as this.setState() has. But why, can you ask, your handleScrollPosition is called at all?(since you can see it's called in debugger). The reason is that you've written this as handleScrollPosition() so it's called immediately. To demonstrate my point, if you change it to correct "passing a callback" form:
setPosts(res.data.slice(0, 20), handleScrollPosition);
As you already have in class-based version, you will see it's never called. Similarly,
[].map(() => {}, 1,2,3,4, handleScrollPosition())
will also call handleScrollPosition() even though obviously [].map() does not process 6th argument.
So what you should do? First, let's move scroll restoring into separate useEffect:
useEffect(() => {
const scrollPosition = sessionStorage.getItem("scrollPosition");
if (scrollPosition) {
window.scrollTo(0, parseInt(scrollPosition));
sessionStorage.removeItem("scrollPosition");
}
}, []);
obviously it did not work well, since it will be called immediately, before data is fetched. So let's add dependency on posts:
useEffect(() => {...
}, [posts]);
But it will not work correctly, still. The reason is it's triggered on initial render when posts is empty... so let's add check "only after posts are loaded":
useEffect(() => {
if (posts.length) {
const scrollPosition = sessionStorage.getItem("scrollPosition");
if (scrollPosition) {
window.scrollTo(0, parseInt(scrollPosition));
sessionStorage.removeItem("scrollPosition");
}
}
}, [posts]);
Now it works as expected and restores scroll position.
PS btw, I don't think that writting position to sessionStorage is the best approach. Working with multiple tabs will make a mess. If speaking without sample code, I'd see alternative by making separate route "also the list but with ID of element we should scroll to". And then link "back to the list" will target that route, with passing of entity ID you are currently viewing details for.

history.push using react-router-dom

I am navigating from one page to another using history.push which is available from below
import { withRouter } from 'react-router-dom
I am able to navigate properly but i have a requirement that if i move from Page A to Page B, i should not be allowed to go back to previous page using Browser back button.
I know this can be achieved by window.redirect but i dont want to use that. The problem with that is the entire state and redux store information is lost. Does anyone know if i can use withRouter and still be able to achieve the requirement above.
You could use the history.replace('/Whatever_screen') to replace the current page in the stack.
replace(path, [state]) - (function) Replaces the current entry on the history stack.
Second option:
You could use the below code to block the user to going back in the history.
componentDidMount() {
const { history } = this.props;
window.onpopstate = function (event) {
history.go(1);
};
}
Working Example:
MDN reference here:
There is no way to clear the session history or to disable the back/forward navigation from unprivileged code. The closest available solution is the location.replace() method, which replaces the current item of the session history with the provided URL.

Want to have an event handler for the browser's back button with next.js

I am having a modal which while opening pushes a hash to url example.com/#modal, on browser back button click, I want to recognise that event so that I can toggle the state of modal. the point is, since Im using it with next.js (server side rendering), I will not be having access to window object (correct me if I am wrong). so I need an alternate way to handle the event of browser back button.
You can use next/router's beforePopState to act on changes to the session history navigation (back/forward actions), and make sure it'll only happen when leaving the current page.
useEffect(() => {
router.beforePopState(({ as }) => {
if (as !== router.asPath) {
// Will run when leaving the current page; on back/forward actions
// Add your logic here, like toggling the modal state
}
return true;
});
return () => {
router.beforePopState(() => true);
};
}, [router]); // Add any state variables to dependencies array if needed.
#Summy I hope your issue is resolved by now. If not you can try this for the browser back button:-
Next.js + React Go back to the previous page
If you want to use hashbang URLs you can't use SSR, since /# and /#/one is the same route server-side, so there is no way for the server to know what to render, it will need to send a basic template and let the client fill it, in that case, I think using CRA or Parcel with React Router and its HashRouter is a better option, that way you will have a single index.html and let the client decide what to render.
in NextJs we can use beforePopState function and do what we want such close modal or show a modal or check the back address and decide what to do
https://stackoverflow.com/a/60702584/4717739
https://stackoverflow.com/a/69560739/4717739

How to intercept browser navigation and prompt user in React (including Browser Navigation Away and Tab close)?

Here's my scenario:
I have an editor in a React component that might have unsaved data.
The only way to know this, for now, is to call a function that returns a boolean.
So I have to determine if the component has unsaved data when user is trying to 1. navigate away 2. pressed any browser navigation buttons 3. Closed the tab or window.
I'm using React Router 5
I have tried using Prompt like this:
<Prompt
when={this.getIsEditorDirty()}
message={location => `Are you sure you want to quit editing ?`}
/>
But when only takes the value as state as expected not through a function. So it doesn't behave correctly.
Since if the editor is dirty or not can be only be known through a function (assuming getIsEditorDirty()), I don't seem to find any proper way to intercept any kind of user navigation.
What could be the possible solution or way around here ?
I don't know if it's still the case in Router 5, but in 4 I do it by returning true from function passed as "message" prop if the editor is "clean".
getBlockMessage = () => {
return this.isChanged() ? LEAVE_MSG : true;
}
// ...
<Prompt message={this.getBlockMessage}/>
Here's the full implementation of the solution in addition to what #g_ain has posted. This should clarify the solution even further.
This also includes the tricky part where the usage of when in Prompt is needed to prevent an endless navigation loop. (If you are using a custom Confirmation Modal)
//Assuming this is the callback from
//your custom prompt modal's YES button
onConfirmExitClick() {
history.push(this.state.nextLocation);
}
shouldShowExitConfirmModal(nextLocation) {
if (this.getIsEditorDirty()) {
this.setState({
nextLocation,
showExitConfirmModal: true
});
return false;
} else {
return true;
}
}
//Inside render()
//Notice the when prop, it doesn't fire when the custom modal is showing
<Prompt
when={!this.state.showExitConfirmModal}
message={nextLocation => this.shouldShowExitConfirmModal(nextLocation)}
/>

Dom node insertion/removal without mutation observers

I'm building a Tour component in React whose purpose is to introduce the user to the web app's interface. Parts of the "Tour" involve validating the user's actions, (e.g. if the current step involves opening a modal, once the user does so, the "Tour" should progress otherwise it should show an error if the user tries to progress by clicking 'Next').
For this I need to detect changes in the DOM, (e.g. a modal being opened or a div with a specific class appearing). I've had some ideas about wiring up an 'onNext' function that progresses the tutorial once the user interacts with certain target elements (e.g. 'Open Modal' button), but this seems like a hack, I want to govern the progression of the tour only by the actual elements present in the DOM not by listening for clicks that will result in the necessary elements showing up eventually.
One of the big constraints is avoiding MutationObservers in addition to usage of jQuery. With that said, I'm interested in hunches about how to validate the dom, how would one use pure javascript and the dom to determine the addition and removal of elements?
I think you're best served by implementing a Flux architecture to handle this. Redux is a good fit.
Create a Redux Reducer for your tour progression. The state of this reducer should be a key that corresponds to the current step of the tour that the user is within.
All components used in the tour should have access to this tour state as a prop. Use this prop to determine functionality. I.e. for your example of a dialog that must be opened, the code might look like this, within a relevant component;
openModal(){
if(this.props.tourStep == 'prompt_modal_open'){
ActionCreator.progressTourStep();
}
// code for actually opening the modal goes here
},
someOtherAction(){
if(this.props.tourStep == 'prompt_modal_open'){
//Display error message here
} else {
//normal action result here
}
}
When the user is not taking the tour, simply set tourStep in the reducer to undefined, and any tour related functionality will be turned off.
Alternately, if you want to keep your components clean and "dumb", you can put this logic directly into the action creator with the help of Redux-Thunk;
ActionCreator.openModal = function(){
return function(dispatch, getState){
var state = getState();
if(state.tourStep == 'prompt_modal_open'){
dispatch({type: 'progress_tour_step'});
}
dispatch({type: 'open_modal'});
}
}
ActionCreator.someOtherAction = function(){
return function(dispatch, getState){
var state = getState();
if(state.tourStep != undefined){
dispatch({type: 'show_error'});
} else {
dispatch({type: 'some_other_action_type'});
}
}
}

Categories