Next.js: getInitialProps not working with history (back button)?

Created on 10 Oct 2017  ·  53Comments  ·  Source: vercel/next.js

I have an app with a single page index.js, and I parse the query parameters in getInitialProps:

static async getInitialProps ({query}) {
    console.log('PAGE', query.screenId);
    try {
        const projectData = await ProjectLoader.loadProject(query);
        return projectData;
    }
    catch (err) {
        console.error(err);
    }
};

I use <Link href={pathname, query} prefetch> tags to build my links.

The screenId property is key to finding the right page to render.

However, when I use the back button in the browser to a page without screenId set, getInitialProps doesn’t return screenId:undefined as expected, but another value (as you can see in the console log):

getInitialProps not working with history

When I refresh the page, I get the right properties and see the correct results.

The “imaginary” screenId is actually a valid value, but I have no idea where this value comes from.

Why is this happening?

Most helpful comment

Why is this closed ? :(

All 53 comments

This is interesting to me as I am facing a similar issue with "back button". Could you try using the latest version of next.js and share your repo if possible?

We're facing similar issues, any pointers would be appreciated.

This may not be the solution to the above problem, but the last few days I had a big “a-ha” moment with the <Link> component when you’re using custom routing:

You need _both_ href and as properties, they _both_ need to be valid, and they will most likely to be _different_ from each other(!).

Example: <Link href={'/mynextjspage?articleId=' + articleId} as={'/articles/' + articleId}>

as will be the link as you _want_ it to appear, while href will be a reference to a Page component in the /pagesfolder with parameters added as a query string.

@liweinan0423 @anselmdk Are you using custom routing?

I think our issue is that we use one central dispatcher (pages/dispatcher.js). The dispatcher decides which components to load, based on the url (we're loading data from an API behind the scenes).
This works perfectly until we start hitting the browser's "back" button. Then you're redirected to a random page. It seems like the issue is that our links look like this:

<Link
  href="/dispatcher"
  as="/page/about"
>
  <a>home</a>
</Link>

The href is always set to /dispatcher on all pages.

I've looked a little into other issues, and it seemed like the solution mentioned here would solve it. In short the idea is to append a unique query string, which would then make href look like something like this: /dispatcher?pretty=url1. Unfortunately that triggers another problem - the links just stop working (clicking them does nothing). I've isolated the issue by taking the code from https://github.com/zeit/next.js/tree/master/examples/parameterized-routing and making it run on one file only:

import React from 'react'
import Link from 'next/link'

export default class extends React.Component {
  static getInitialProps ({ query }) {
    return { id: query ? query.id : 'error'}
  }

  render () {
    return <div>
      <h1>My {this.props.id} blog post</h1>
      <ul>
        <li><Link href='/test?id=first' as='/test/first'><a>My first blog post</a></Link></li>
        <li><Link href='/test?id=second' as='/test/second'><a>My second blog post</a></Link></li>
        <li><Link href='/test?id=third' as='/test/last'><a>My last blog post</a></Link></li>
      </ul>
      <p>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua.
      </p>
    </div>
  }
}

Clicking any of the three links does nothing. When the query strings are removed from the links (so href only contains /test) it works fine again - unless the history part. A funny observation is that when after removing them you add them back in, and the component is hot reloaded, both links and history work - until you refresh the page.

I hope anyone's had similar issues and/or can point us into a direction for solving this.

I am having the same issue..

Same here.

Does anyone solved this problems?

@anselmdk / @tomsoderlund are you using a custom server to handle that sort of routing? Might be helpful to see that as well.

So, because nobody answered, neither here nor on Slack, we solved this issue by creating a custom version of link.js.
I think all we did was to change https://github.com/zeit/next.js/blob/canary/lib/link.js#L75
to Router[changeMethod](href + '?uri=' + as, as, { shallow }).
Feels a bit hacky, but it works :)

@anselmdk can verify this works for the Link component, but if using Router.push/replace the issue still persists.

I updated the Router component with the following.

  push(url, as = url, options = {}) {
    return this.change('pushState', url + '?uri=' + as, as, options)
  }

  replace(url, as = url, options = {}) {
    return this.change('replaceState', url + '?uri=' + as, as, options)
  }

and you can use it like this Router.push('/', pathname)

I'm getting same error, tried to update to 6.1.1 but won't fix.
Also tried to set useFileSystemPublicRoutes: false and didn't changed anything on the app.
I'm redirecting this way in my app but once i hit the back button the component don't fire the getInitialProps:

const cartId = 20;
Router.push(`/step2?cartId=${cartId}&hashId=asd`, `/next/${cartId}/asd`)

I've also tried with shallow but no success:

Router.push(`/step2?cartId=${cartId}&hashId=asd`, `/next/${cartId}/asd`, { shallow: true })

Hey guys

I tried to reproduce your example but could not verify it.

Example:

Got you issue resolved in the meantime or did I do something wrong?

@HaNdTriX It works well inside App routing but not with external link.
If you navigate to external url(e.g. github) and click back button, you see getInitialProps is not called.

Guys, is there any cool solution or alternatives? It's a really block at my project.

class Profile extends React.Component {
    // Update the profile avatar and show updated avatar
    // Call api to update avatar at backend database
}
Profile.getInitialProps = () => {
    // Call api to get profile information
}

When I update the profile avatar and navigate to external url(e.g. terms pdf url) and click back button, it shows the previous avatar as getInitialProps is not called.
Router.replace() after updating Avatar, didn't help me.

@OscarBengtsson I've just tested it again. And it works from my point of view.

Navigate back inside root url (internally): getInitialProps is called on the client (client render).
Navigate back from external url (external): getInitialProps is called on the server (server render).

The initial request is always rendered on the server.

This is the intended behavior.

Edit: Nevertheless on chrome it works in Safari & Firefox not.

Ok I finally could reproduce the issue.

The issue is only existent in the browsers Safari & Firefox. These browsers use a feature called "bfcache" (back-forward cache). This means the html document is cached in memory. As soon as a user uses a back button coming from an external page, no request is made on the server. Instead the html document is rendered from the cache. Since getInitialProps already ran (before it was cached) it won't run again.

@timneutkens Do you have an idea about how to solve this and get all browsers inline?

If you are experiencing issue that getInitialProps not invoked when user click back between same pages you can use next code (somewhere during bootstrap on frontend):

Router.beforePopState(({url, as}) => {
  Router.replace(url, as, {});
  return false;
});

Guys, finally i've better understood my problem. Im using Next 7.0 canary to see if latest updates work.

Im my project im using custom routing and my first URL is /start/:id/:hash
When i change route to next/:id/hash it works correct

Router.push({
    pathname: '/step2',
    query: { id: cartId, hash: 'asd' }
}, `/next/${id}/${hash}`)

Once i got back using chrome back button the route stop working as expected, the initialProps doesn't receive the params vars, They do work in step2 that i've redirect but once popstate gets called it doesnt receive any params option, because it was the first rendered page and next router didn't received any options as first render...

To solve this im splitting url, it's temporally until i found a way to solve this.

@wallynm did you try my solution above? Looks very similar to what I faced.

@DontRelaX in the end i've discovered that using custom routing with params into URL like i've used Next can't understand and read those params at first render, so when user gets back using back button all params are empty because they weren't parsed, any time the Router received these params so it cant understand once it get popped from the browser history...

Your last comment helped me understand better the problem and to solve i've implemented this on the component did mount:

componentDidMount() {
  const { store, id, hash } = this.props

  Router.replace({
    pathname: '/step1',
    query: { id, hash }
  }, `/start/${id}/${hash}`, { shallow: true})
}

This way at first render my first history gets replaced by one with custom defined params, as at first it can't understand which part of the url is what... When we redirect a user inside our app, we send those params and how the router should map the URL to the vars, but at first render... User came from an direct access.

In the end, i don't see this as a Next/Routing Bug but a misunderstood about how i was handling my custom variables...

And i think this could be very common for anyone working with these custom routes, very tricky to discover...

I'm wondering if you're using next export or not? If you're not there's definitely something else going on and we should investigate that further, as replacing on initial render is not the correct solution.

I don't know for sure what is this config. Quick search and found this:
https://github.com/zeit/next.js/issues/604
I don't using anything like that, but let me explain a little about my current config:

I'm using custom routing integrated with Express:

const app = next({ dev, dir: './src' })
const handle = app.getRequestHandler()
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const compression = require('compression');

const render = function(req, res, page) {
  const parsedUrl = parse(req.url, true)
  const { pathname, query } = parsedUrl
  return app.render(req, res, page, query)
}

app.prepare().then(() => {
  const server = express()
  if (process.env.NODE_ENV === "production") {
    server.use(compression());
  }

  server.get('/start/:id/:hash', (req, res, next) => render(req, res, '/step1'))
  server.get('/next/:id/:hash', (req, res, next) => render(req, res, '/step2'))
})

@timneutkens Sorry about worrying you. Finally found the bug in my code.

Inside render method i'm parsing the URL but turns out that Node URL module can't recognize wich part from URL should be params, as they use the ":" notation.

Well the whole problem was related with this parsed url, the only thing that i need to do was pass the req.params from express directly to the app.render this way:

const render = function(req, res, page) {
  return app.render(req, res, page, req.params)
}

Problem here was because i followed the basic home tutorial and wasn't working because i wasn't working with query params, but :id and :hash notation...

I don't know if was confuse. In the end everything it's working without bugs on Next.JS.
Sorry about this.

No worries, was just making sure people won't come here in the future and copy-paste a workaround that probably introduces performance issues 🙏

Of course @timneutkens , if you like i can remove these comments as they may not help future users!

Looks like GitHub has a hide feature to minimize the comment, just tried it on your comment and looks perfect 🙌

Hi,

I tried this and It worked for me. Let me know if this is fixing your issue.

You have to put Router.beforePopState() inside in cyclehook method componentDidMount() in _app.js file. (I'm using [email protected])
The idea is to force the browser to reload the right URL when hitting browser's back button.

import React from 'react';
import App, { Container } from 'next/app';
import Router from 'next/router';

class MyApp extends App {
  componentDidMount() {
    Router.beforePopState(({as}) => {
      location.href = as;
    });
  }

  render() {
   ...
  }
}

export default MyApp;

^ The above worked for me, however, it looks like it renders the page twice.

Better than it not rendering at all I guess.

Note*: This "back button" issue was only happening to me in Safari ([email protected]).

Hate to admit it, but I think Safari is the new IE6 🤦‍♂️

Anyways, good find @johanleroch !

Can you try next@canary, it includes bunch of fixes for back button behavior in older browsers.

Do we have any solution for this ?

My case :

  1. From http://localhost:3000 navigate to http://localhost:3000/p/hello using Link.
  2. Refresh the page.
  3. Click back button : It goes to http://localhost:3000 but still renders the content of http://localhost:3000/p/hello it is not loading the content of http://localhost:3000.

I am using "next": "^8.1.0".

@ninjawhocodes , try changing @johanleroch 's code as below

...
Router.beforePopState(({ as }) => {
      window.location.href = as;
      window.location.reload();
    });

having this issue with ^8.0.0

this work around appears to work but it does a full page reload which isn't ideal, any other solutions?

    Router.beforePopState(({ as }) => {
      window.location.href = as;
      window.location.reload();
      return false 
     // returning true or false here doesnt seem to matter, new signature requires a boolean to be returned
    });

I was having this error in my getInitialProps function at a line that used ctx.req.url

Error:
TypeError: undefined is not an object (evaluating 'ctx.req.url')

I changed this line to use a url param, ctx.query.user (I have a [user]/index.js page) and I no longer receive this error 🤷‍♂️

before:
let username = ctx.req.url.split('/')[1];

after:
let username = ctx.query.user;

I haven't been using next.js for very long and I have no idea why this is happening. Dumping the ctx object contains everything it should when hitting the back button.

I'm getting this same behavior (internal routing - within pages inside my next.js app - work, but once navigating away - or even to a static asset - breaks). I am using export.

Something to note. I also didn't have this problem when testing with github pages, but this showed up for us when we needed to move it to production in aws s3 (I pitched now, but we need to use govcloud)

If anyone runs into this issue in the future, I found a not-so-pretty solution.

I'm not sure why next export doesn't work in s3, but does work in github pages. However, if you are using aws with next export and you're running into 404 pages when either refreshing any page or navigating back from an external page to your next.js static site, I've found the following works:

If you add .html to your as= prop in any component using next/link for example:

Diff

import { withRouter } from 'next/router'
import Link from 'next/link'

const custom_link = ({ href, router, children, name, as }) => 
-   <Link prefetch href={href} as={as} passHref>
+   <Link prefetch href={href} as={as + ".html"} passHref>
      {children}
    </Link>

export const CustomLink = withRouter(custom_link)

This works as it forces the server to serve the html file for the page instead of referring to the router which has lost its context... from what I gather.

Hope this helps someone who's in a bind and needs a quick fix. Not an ideal solution, but eh...

Why is this closed ? :(

just encountered this. this should not be closed

Same issue here on Safari 13 (Technology Preview)

Also experiencing this with Safari. Chrome works as expected.

If anything the different behaviors in the two browsers should be treated as an issue in and of itself (ie: The point @HaNdTriX made about bfcache)

This issue is in Firefox, Chrome when using Next 9.1.6. This needs to be reopened.

This hack works for now, but please let me know if anyone has a good solution or cause for this problem.

static async getInitialProps(context) {

    //Hack: If this code is executing in client-side, just reload.
    if (process.browser) {
        history.go();
    }

    ...
    return { ... };
}

Is there a workaround that does NOT cause SSR? We‘d like to keep unnecessary SSR low because of backend resources.

This is still an issue for me - for all browsers.
I am using Router.push to navigate to pages, so I think thats possibly where the issue lies ?

Getting issue in Chrome and Next 9.2.1. getInitialProps does not refetch data when using the back button

Issue exists in firefox, experienced when using the forward button too, currently using the

https://github.com/zeit/next.js/issues/3065#issuecomment-569202884

hack to get around it but would like a better solution, maybe one of the Router.events can be of some use? will investigate too

Edit: used context to get around the issue completely as it fell into my use case, picking values from context while transitioning and taking value from query and setting into context to use when page is reloaded or back/forward buttons pressed

Issue in Safari still

Upgrading to 9.3.0 and changing getIntialProps to getServerSideProps solved the problem.

It really did solve it, thanks :)

Awesome, worked for me but getServerSideProps didn't do anything, but getInitialProps seems to work now with back button

I'm running a legacy project on 9.1.5, and really can't update right now, as this would break a lot. Tried the quick fix from https://github.com/zeit/next.js/issues/3065#issuecomment-569202884 but still doesn't work. He doesn't refetch the data from _app.tsx.

I'm running a legacy project on 9.1.5, and really can't update right now

There haven't been breaking changes in the latest releases.

for searchers finding this issue, here's another possible cause of the back button not working, getInitialProps or otherwise:

i was using window.history.replaceState(null, '', as); to change the url without triggering next's shallow routing (which causes a react rerender), but this apparently doesn't play well with how next manages the url. replacing that code with shallow rendering (and then refactoring my code to allow those re-renders) solved my issue and got the back button to work again

In app.ts, execute the following statement on componentDidMount.

// app.ts
this.props.router.beforePopState(({ as, options }) => {
      if (options.shallow) {
        this.props.router.replace(as)
        return false
      }
      return true
    })
Was this page helpful?
0 / 5 - 0 ratings

Related issues

wagerfield picture wagerfield  ·  3Comments

lixiaoyan picture lixiaoyan  ·  3Comments

havefive picture havefive  ·  3Comments

YarivGilad picture YarivGilad  ·  3Comments

swrdfish picture swrdfish  ·  3Comments