Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

withRouter doesn't parse query on static website #4804

Closed
saginadir opened this issue Jul 19, 2018 · 25 comments
Closed

withRouter doesn't parse query on static website #4804

saginadir opened this issue Jul 19, 2018 · 25 comments

Comments

@saginadir
Copy link
Contributor

Bug report

Describe the bug

I'm using withRouter to get the query data from the URL, and when I export to a static website, the router.query prop is empty

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Use withRouter to wrap a component
  2. export to a static website
  3. add a query parameter to the URL
  4. router.query will not contain it

Expected behavior

for router.query to contain

Screenshots

not static export:
screen shot 2018-07-19 at 12 10 08

static export:
screen shot 2018-07-19 at 12 10 34

System information

  • OS: Mac
  • Browser Chrome
  • Version of Next.js: [e.g. 6.1.1]
@wslx520
Copy link

wslx520 commented Jul 19, 2018

I have met the same question with you. actually, after exported, you can not use dynamic query params, instead of it you need to pass the query in your next.config.js => exportPathMap, for example:

exportPathMap: function(defaultPathMap) {
    return {
      '/': { page: '/' },
      '/about': { page: '/about' },
      '/readme.md': { page: '/readme' },
      '/p/hello-nextjs': { page: '/post', query: { title: 'hello-nextjs' } },
      '/p/learn-nextjs': { page: '/post', query: { title: 'learn-nextjs' } },
      '/p/deploy-nextjs': { page: '/post', query: { title: 'deploy-nextjs' } }
    }
  }

@saginadir
Copy link
Contributor Author

@wslx520 Thanks for clarifying, I understand it better now.

I need to pass a dynamic "id" to display a different entity each time.
How did you end up solving your problem?

I can create a higher level component to wrap my layout component and parse the query params by myself, is it a valid way to go?

@wslx520
Copy link

wslx520 commented Jul 20, 2018

@saginadir I think it's valid. sometimes I just write a function for getting query from the asPath

@saginadir
Copy link
Contributor Author

@wslx520 Awesome, thanks for the help, I implemented something similar that will fetch the query params, called it {myWithRouter} and using it to wrap my component instead of {withRouter}

@eigilsagafos
Copy link

@wslx520, @saginadir I was debugging the same issue before I came across this post. Maybe someone from the Next.js team could explain if this is how it should work? Maybe there a way to force the Router to refresh so that it reads the query?

@saginadir
Copy link
Contributor Author

saginadir commented Aug 3, 2018

@eigilsagafos I am not sure how the next.js team expect to solve this, but my solution was this:

// Our router to override the query
// missing query when deploying to static export
export const ourWithRouter = (Component) => {
  return withRouter(({router, ...props}) => {
    // taking the query from router and overriding the query
    router.query = globalRouter.match(router.asPath).query

    return <Component {...props} router={router} />
  })
}

globalRouter.match function parses the router.asPath property and returns an object which contains the query. In my case I use 3rd party next-router library which gives me slugified URLs.

But if you use the regular "?key=value" query, then maybe you can just use something like:
https://www.npmjs.com/package/query-string
to parse your URL

Then you just use ourWithRouter as if you were using the regular withRouter.

@guigrpa
Copy link

guigrpa commented Oct 10, 2018

@saginadir Where do you obtain the globalRouter from?

@saginadir
Copy link
Contributor Author

saginadir commented Oct 21, 2018

@saginadir Where do you obtain the globalRouter from?

I Apologize for the late response

as I wrote globalRouter is:

globalRouter.match function parses the router.asPath property and returns an object which contains the query. In my case I use 3rd party next-router library which gives me slugified URLs.

Depends on what you are trying to do - you may need to have custom code on how you parse the "router.asPath"

@james2doyle
Copy link

james2doyle commented Jan 4, 2019

Here is what I ended up doing:

import { withRouter } from 'next/router';

/**
 * Our router to override the missing query when deploying to static export
 * This is required for page components that need access to the router
 *
 * @param {React.Component} Component
 *
 * @return {React.Component}
 */
export const withPageRouter = (Component) => {
    return withRouter(({ router, ...props }) => {
        // split at first `?`
        const searchParams = new URLSearchParams(router.asPath.split(/\?/)[1]);

        const query = {};
        for (const [key, value] of searchParams) {
            query[key] = value;
        }

        // replace the empty query
        router.query = query;

        return (<Component {...props} router={router} />);
    });
};

Very similar to what @saginadir did but without the external dependency. This works identically to withRouter from next/router

RobinCsl added a commit to kiwicom/margarita that referenced this issue Feb 4, 2019
Summary: When going to /results page from a built nextjs application, getInitialProps and the likes are ignored. That meant that refreshing /results would return Error because the query params were not taken into account.

I used the solution from this comment vercel/next.js#4804 (comment), and it works pretty well.

Caveat is that we show for a brief moment a sort of broken state. That should be improved.
RobinCsl added a commit to kiwicom/margarita that referenced this issue Feb 4, 2019
Summary: When going to /results page from a built nextjs application, getInitialProps and the likes are ignored. That meant that refreshing /results would return Error because the query params were not taken into account.

I used the solution from this comment vercel/next.js#4804 (comment), and it works pretty well.

Caveat is that we show for a brief moment a sort of broken state. That should be improved.
RobinCsl added a commit to kiwicom/margarita that referenced this issue Feb 4, 2019
Summary: When going to /results page from a built nextjs application, getInitialProps and the likes are ignored. That meant that refreshing /results would return Error because the query params were not taken into account.

I used the solution from this comment vercel/next.js#4804 (comment), and it works pretty well.

Caveat is that we show for a brief moment a sort of broken state. That should be improved.
RobinCsl added a commit to kiwicom/margarita that referenced this issue Feb 4, 2019
Summary: When going to /results page from a built nextjs application, getInitialProps and the likes are ignored. That meant that refreshing /results would return Error because the query params were not taken into account.

I used the solution from this comment vercel/next.js#4804 (comment), and it works pretty well.

Caveat is that we show for a brief moment a sort of broken state. That should be improved.
RobinCsl added a commit to kiwicom/margarita that referenced this issue Feb 4, 2019
Summary: When going to /results page from a built nextjs application, getInitialProps and the likes are ignored. That meant that refreshing /results would return Error because the query params were not taken into account.

I used the solution from this comment vercel/next.js#4804 (comment), and it works pretty well.

Caveat is that we show for a brief moment a sort of broken state. That should be improved.
RobinCsl added a commit to kiwicom/margarita that referenced this issue Feb 4, 2019
Summary: When going to /results page from a built nextjs application, getInitialProps and the likes are ignored. That meant that refreshing /results would return Error because the query params were not taken into account.

I used the solution from this comment vercel/next.js#4804 (comment), and it works pretty well.

Caveat is that we show for a brief moment a sort of broken state. That should be improved.
@chromale
Copy link

chromale commented Feb 5, 2019

Above solution is really good but I’ve had some issues URLSearchParams() during SSR (in development on localhost).

So there is another (and shorter) version using library query-string:

import { withRouter } from "next/router";
import queryString from "query-string";

export const withPageRouter = Component => {
  return withRouter(({ router, ...props }) => {
    router.query = queryString.parse(router.asPath.split(/\?/)[1]);

    return <Component {...props} router={router} />;
  });
};

@james2doyle
Copy link

@chromale hmm the URLSearchParams global object was added in Node 10 and should be available in all browsers. Not sure why it would be causing issues. Anyway, what you have is also a nice option

@RobAWilkinson
Copy link

Thanks for your snippet @james2doyle it would be awesome if that little example was in the docs

@loganpowell
Copy link

Here is what I ended up doing:

import { withRouter } from 'next/router';

/**
 * Our router to override the missing query when deploying to static export
 * This is required for page components that need access to the router
 *
 * @param {React.Component} Component
 *
 * @return {React.Component}
 */
export const withPageRouter = (Component) => {
    return withRouter(({ router, ...props }) => {
        // split at first `?`
        const searchParams = new URLSearchParams(router.asPath.split(/\?/)[1]);

        const query = {};
        for (const [key, value] of searchParams) {
            query[key] = value;
        }

        // replace the empty query
        router.query = query;

        return (<Component {...props} router={router} />);
    });
};

Very similar to what @saginadir did but without the external dependency. This works identically to withRouter from next/router

This doesn't work on IE11

@Timer
Copy link
Member

Timer commented Jul 8, 2019

This feature is supported out-of-the-box in Next.js 9! Please read the blog post to learn more.

tl;dr after hydration, Next.js will trigger an update with the query string parameters.

@Timer Timer closed this as completed Jul 8, 2019
@frontendtony
Copy link
Contributor

Migrated to Next.js 9 to solve this problem, but that brought it's own problem to sour my joy!

Deciding to export pages as HTML files in the root of the /out directory like /out/page.html instead of the usual /out/pagefolder/index.html and no clear way to go back to the previous behavior is a bummer. I need to deploy with IIS and that returns not found when you visit a page directly because it interprets example.com/page as "Hey, give me the index file in the /page directory"

@timneutkens
Copy link
Member

@Tonerolima https://github.com/zeit/next.js/blob/canary/UPGRADING.md#next-export-no-longer-exports-pages-as-indexhtml

@frontendtony
Copy link
Contributor

Thank you @timneutkens, you are a life saver! I need to start contributing to Next.js soonest, even though I have no idea where to start

@cansin
Copy link

cansin commented Oct 13, 2019

@Timer I am using next@9.1.1 with next export. But when I try to read query at pages/index.js as:

import { withRouter } from 'next/router';
import React from 'react';

class Home extends React.Component {
  componentDidMount() {
    const { router } = this.props;
    const { query } = router;
    const searchParams = [
      ...new URLSearchParams(window.location.search).entries()
    ].reduce((q, [k, v]) => Object.assign(q, { [k]: v }), {});

    console.log(JSON.stringify(query), JSON.stringify(searchParams));
  }

  render() {
    return null;
  }
}

export default withRouter(Home);

for a given /?some_key=someValue, the result for query is {}, yet searchParams is {"some_key": "someValue"}. So I do not think this is supported out-of-the-box. Or am I making a mistake?

@cansin
Copy link

cansin commented Oct 13, 2019

In the meantime, using the above ideas here is what I came up with:

import { withRouter } from 'next/router';
import React from 'react';

export const withPageRouter = ComposedComponent => {
  const WithPageRouteWrapper = withRouter(({ router, ...props }) => {
    router.query = [
      ...new URLSearchParams((router.asPath || '').split(/\?/)[1]).entries()
    ].reduce((q, [k, v]) => Object.assign(q, { [k]: v }), {});

    return <ComposedComponent {...props} router={router} />;
  });

  WithPageRouteWrapper.getInitialProps = ComposedComponent.getInitialProps;
  WithPageRouteWrapper.origGetInitialProps =
    WithPageRouteWrapper.origGetInitialProps;
  if (process.env.NODE_ENV !== 'production') {
    const name =
      ComposedComponent.displayName || ComposedComponent.name || 'Unknown';
    WithPageRouteWrapper.displayName = `withPageRouter(${name})`;
  }

  return WithPageRouteWrapper;
};

@Rameshv
Copy link

Rameshv commented Oct 14, 2019

This wont work if your component uses getInitialProps() and depends on the props set by this. It will throw all kinds of errors.

You can fix that by

import { withRouter } from 'next/router';
export const withPageRouter = Component => {
  const routerWrapper =  withRouter(({ router, ...props }) => {
    router.query = [
      ...new URLSearchParams((router.asPath || '').split(/\?/)[1]).entries()
    ].reduce((q, [k, v]) => Object.assign(q, { [k]: v }), {});

    return <Component {...props} router={router} />;
  });
  routerWrapper.getInitialProps = Component.getInitialProps;
  return routerWrapper;
};

@cansin
Copy link

cansin commented Oct 14, 2019

Ah @Rameshv you are right. Thanks for the heads up!

@cansin
Copy link

cansin commented Oct 14, 2019

Alright following @Rameshv advice, plus looking at the source code at https://github.com/zeit/next.js/blob/canary/packages/next/client/with-router.tsx , I've updated the code above (at #4804 (comment)) to something that I think should be bug-free now.

@jesstelford
Copy link
Contributor

jesstelford commented Mar 31, 2020

Extending on @cansin's example from #4804 (comment), I ended up adding a couple of enhancements:

  • Injects a query key to context within getInitialProps if it exists
  • Adds support for useRouter too
  • Ensures any existing query values that next may have set don't get wiped out
  • Supports query strings with a question mark in them: http://example.com/?foo=bar?
import React from 'react';
import { withRouter as withNextRouter, useRouter as useNextRouter } from 'next/router';

/**
 * Given a string such as:
 *
 * https://example.com/foo?bar=zip&name=Sam
 *
 * Will return:
 *
 * {
 *   bar: 'zip',
 *   name: 'Sam',
 * }
 */
const queryFromUrl = url => {
  const [, ...queryStrings] = url.split('?');
  const queryString = queryStrings.join('?');
  const query = {};

  for (let [key, value] of new URLSearchParams(queryString).entries()) {
    query[key] = value;
  }

  return query;
};

const extractQueryFromRouter = router => ({
  ...queryFromUrl(router.asPath),
  ...router.query,
});

/**
 * Provide a router provider which ensures router.query is always correctly
 * hydrated on the first render even when statically optimised to avoid [this
 * caveat from the docs](https://nextjs.org/docs/routing/dynamic-routes):
 *
 * > Pages that are statically optimized by Automatic Static Optimization will
 * > be hydrated without their route parameters provided, i.e `query` will be an
 * > empty object (`{}`).
 *
 * Also injects a `.query` paramter to the `context` in `getInitialProps` making
 * it more useful than the existing `.asPath`
 *
 * Usage is identical to `import { withRouter } from 'next/router';
 *
 * Modified from https://github.com/zeit/next.js/issues/4804#issuecomment-541420735
 */
export const withRouter = ComposedComponent => {
  const WithPageRouteWrapper = withNextRouter(({ router, ...props }) => {
    router.query = extractQueryFromRouter(router);

    return <ComposedComponent {...props} router={router} />;
  });

  if (ComposedComponent.getInitialProps) {
    WithPageRouteWrapper.getInitialProps = (context, ...args) => {
      context.query = extractQueryFromRouter(context);
      return ComposedComponent.getInitialProps(context, ...args);
    };
  }

  if (process.env.NODE_ENV !== 'production') {
    const name =
      ComposedComponent.displayName || ComposedComponent.name || 'Unknown';
    WithPageRouteWrapper.displayName = `withPageRouter(${name})`;
  }

  return WithPageRouteWrapper;
};

/**
 * Provide a router hook which ensures router.query is always correctly hydrated
 * on the first render even when statically optimised to avoid [this caveat from
 * the docs](https://nextjs.org/docs/routing/dynamic-routes):
 *
 * > Pages that are statically optimized by Automatic Static Optimization will
 * > be hydrated without their route parameters provided, i.e `query` will be an
 * > empty object (`{}`).
 *
 * Usage is identical to `import { useRouter } from 'next/router';
 */
export const useRouter = () => {
  const router = useNextRouter();
  router.query = extractQueryFromRouter(router);
  return router;
};

@softmarshmallow

This comment has been minimized.

jonathansick added a commit to lsst-sqre/times-square-ui that referenced this issue Dec 14, 2021
This demonstrates a dynamically router page for individual notebooks.
The notebook name is a variable that is part of the URL. The page also
takes the query parameters as initialStaticProps.

To read the query parameters, we use a custom version of the useRouter
hook; see vercel/next.js#4804 (comment)
jonathansick added a commit to lsst-sqre/times-square-ui that referenced this issue Dec 18, 2021
This demonstrates a dynamically router page for individual notebooks.
The notebook name is a variable that is part of the URL. The page also
takes the query parameters as initialStaticProps.

To read the query parameters, we use a custom version of the useRouter
hook; see vercel/next.js#4804 (comment)
@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.