use-query-params icon indicating copy to clipboard operation
use-query-params copied to clipboard

can this works with next.js ?

Open dfang opened this issue 5 years ago • 35 comments

hi, can this lib works with next.js ?

i got this error on page when refresh (ssr mode) ....

TypeError: Cannot read property 'search' of undefined

dfang avatar Apr 28 '19 05:04 dfang

Hmm, I suppose this is due to window.location not being a thing. I've never used next.js though, would you be able to reproduce this in an example repo I could use to debug?

pbeshai avatar Apr 29 '19 16:04 pbeshai

I've ran into similar issue with Gatsby: error Building static HTML failed for path "/" (...) WebpackError: TypeError: Cannot read property 'search' of undefined I suppose that this issue for @reach/router could point us in the right direction with preventing this from happening.

I've added PR #25 that should enable use-query-params to work with Gatsby/NextJS.

zielinsm avatar Jun 21 '19 12:06 zielinsm

Just wanted to add a +1 to this issue. It would be nice to use this with SSR applications and @zielinsm seems to have a working fix ready.

EDIT: I tested out PR #25 locally and found it to be non-functional. I'll look into submitting an alternate PR.

nickhavenly avatar Aug 29 '19 04:08 nickhavenly

Would also love to usue this SSR (nextjs in particular), have you made any progress @nickhavenly ?

gcloeval avatar Sep 09 '19 03:09 gcloeval

@nickhavenly, I have made some changes to the PR I submitted after finding out I wasn't returning a value adjusted to the environment. This should be working now. I've re-requested a review from @pbeshai.

zielinsm avatar Sep 09 '19 07:09 zielinsm

From what I understand so far, it seems to me that this would allow it to just perhaps pass/build successfully. I am wondering if @nickhavenly actually wanted to look into building something like currently supported ReactRouter / ReachRouter providers, i.e. NextRouterProvider?

For example, if I click on a "filter" component and update a value, I would be getting a new request on the next SSR server with new value of the filter, so I can fetch different data on the servers getInitilProps? I am also first time working with nextjs so I am not 100% sure how involved that is.

gcloeval avatar Sep 09 '19 21:09 gcloeval

@gcloeval @zielinsm,

I was able to get it working in a similar fashion to PR #25 but after getting to that point realized that I needed to integrate with NextRouter for my use case. My deadline was too tight to spend time on this so I ended up rolling my own rudimentary solution which interfaced directly with next router.

For non-package specific SSR @zielinsm's PR should be sufficient but I think the correct long term solution here is to integrate a NextRouterProvider as @gcloeval suggested. Unfortunately I'm still pushing against tight deadlines and don't have the extra bandwidth to pursue this as an option at the moment. Hopefully someone else can carry the torch as this is a really neat package and I'd love to refactor for it in the future.

nickhavenly avatar Sep 09 '19 22:09 nickhavenly

@nickhavenly - yeah same here, bulding a proper NextRouterProvider seems a bit too over my head at the moment. Do you mind sharing your rudimentary soltion, or at least some snippets what you ended up using - maybe a) I can also use it for my app and b) it can be a good starting point if somebody wants to pick it up here?

gcloeval avatar Sep 09 '19 22:09 gcloeval

/**
 * Next.js query param provider.
 */
const QueryProvider: React.FC = ({ children }) => {
  const router = useRouter()

  // eslint-disable-next-line no-shadow
  const history = {
    push: ({ search }: Location) =>
      router.push({ search, pathname: router.pathname }),

    replace: ({ search }: Location) =>
      router.replace({ search, pathname: router.pathname }),
  }

  // eslint-disable-next-line no-shadow
  const location = {
    search: router.asPath.replace(/[^?]+/u, ''),
  } as Location

  return (
    <QueryParamProvider history={history} location={location}>
      {children}
    </QueryParamProvider>
  )
}

lucasconstantino avatar Sep 23 '19 02:09 lucasconstantino

If you use Reach router, see https://github.com/pbeshai/use-query-params/issues/51#issuecomment-546393827. It could help :)

Romcol avatar Oct 28 '19 13:10 Romcol

Just wanted to add some small updates (useMemo, really) onto @lucasconstantino's work. (Thanks for doing the brunt of the job!)

// import

import React, {memo, useMemo} from 'react';
import pt from 'prop-types';
import {useRouter} from 'next/router';
import {QueryParamProvider as ContextProvider} from 'use-query-params';

// component

export function QueryParamProvider(props) {
  const {children, ...rest} = props;
  const router = useRouter();

  const location = useMemo(() => (process.browser ? window.location : {
    search: router.asPath.replace(/[^?]+/u, ''),
  }), [router.asPath]);

  const history = useMemo(() => ({
    push: ({search}) => router.push({search, pathname: router.pathname}),
    replace: ({search}) => router.replace({search, pathname: router.pathname}),
  }), [router.pathname]);

  return (
    <ContextProvider {...rest} history={history} location={location}>
      {children}
    </ContextProvider>
  );
}

QueryParamProvider.propTypes = {
  children: pt.node,
};

// export

export default memo(QueryParamProvider);

mikestopcontinues avatar Dec 15 '19 16:12 mikestopcontinues

Has anyone gotten the above provider to work with Next? I'm getting a Cannot read 'split' of undefined when trying to use useQueryParam.

stevewillard avatar Dec 31 '19 17:12 stevewillard

Having the same issue above.

Cannot read 'split' of undefined

fccoelho7 avatar Jan 16 '20 13:01 fccoelho7

Currently implemented this (a typescript adapted version of @mikestopcontinues ) in my next.js and it's working like a charm.

Only thing I'm not really a fan of is how we are supposed to handle the "initial state" by having to set it manually in componentDidMount but that's not a problem with the next.js/ssr it's part of the library logic itself.

the above split of undefined problem are most probably due to the fact that the pages reads the query on load and if no query is found your state is indeed undefined, and you can go around that with something like this:

   // create your defaultState variable
    const defaultQuery = {
        filters: {
            tags: [],
            category: undefined,
            textSearch: undefined,
        }
    }
 // on mount smart merge the existing query with your default values
    useEffect(
        () => {
            setQuery({
                ...defaultQuery,
                ...(query.filters && { filters: { ...query.filters } }) // if a value is present in the first load query url, it overwrite the default values
            }, 'replace'); // we want to replace because we don't want unexpected states, if you need something else feel free to change this
        },
        []
    )

yuri-scarbaci-lenio avatar Mar 05 '20 14:03 yuri-scarbaci-lenio

If you use https://nextjs.org/docs/routing/dynamic-routes you should modify @mikestopcontinues example

import React, { memo, useMemo } from 'react';
import { useRouter } from 'next/router';
import { QueryParamProvider as ContextProvider, QueryParamContextValue } from 'use-query-params';

export const QueryParamProviderComponent = (props: { children?: React.ReactNode }) => {
  const { children, ...rest } = props;
  const router = useRouter();
  const match = router.asPath.match(/[^?]+/);
  const pathname = match ? match[0] : router.asPath;

  const location = useMemo(
    () =>
      process.browser
        ? window.location
        : ({
            search: router.asPath.replace(/[^?]+/u, ''),
          } as QueryParamContextValue['location']),
    [router.asPath],
  );

  const history = useMemo(
    () => ({
      push: ({ search }: QueryParamContextValue['location']) =>
        router.push(router.pathname, { search, pathname }, { shallow: true }),
      replace: ({ search }: QueryParamContextValue['location']) =>
        router.replace(router.pathname, { search, pathname }, { shallow: true }),
    }),
    [pathname, router],
  );

  return (
    <ContextProvider {...rest} history={history} location={location}>
      {children}
    </ContextProvider>
  );
};

export const QueryParamProvider = memo(QueryParamProviderComponent);

baybara-pavel avatar Apr 02 '20 11:04 baybara-pavel

@baybara-pavel what should pathNameMatch be in your example?

brycereynolds avatar Apr 14 '20 00:04 brycereynolds

Actually, it looks like maybe just a check on match?

brycereynolds avatar Apr 14 '20 00:04 brycereynolds

@brycereynolds, yep you right, thank you, just my typo. Fixed my original comment.

baybara-pavel avatar Apr 14 '20 11:04 baybara-pavel

@baybara-pavel thanks for this example. We ended up changing the push/replace functions to:

router.push(`${router.pathname}${search}`, { search, pathname }, { shallow: true });
// and
router.replace(`${router.pathname}${search}`, { search, pathname }, { shallow: true });

In order to avoid this bug in next.js's router when doing client-side navigation:

https://github.com/zeit/next.js/issues/9473

matt-dutchie avatar May 19 '20 01:05 matt-dutchie

@matt-dutchie I used a different approach to solve the same issue.

router.push({ pathname: router.pathname, query: router.query }, { search, pathname }, { shallow: true });
// and
router.replace({ pathname: router.pathname, query: router.query }, { search, pathname }, { shallow: true });

STEVEOO6 avatar Jun 29 '20 15:06 STEVEOO6

@baybara-pavel Thanks for your example! This is great!

I had to change it slightly, since QueryParamContextValue doesn't seem to be exported anymore:

import React, { memo, useMemo } from 'react';
import { useRouter } from 'next/router';
import { QueryParamProvider as ContextProvider } from 'use-query-params';

export const QueryParamProviderComponent = (props: {
  children?: React.ReactNode;
}) => {
  const { children, ...rest } = props;
  const router = useRouter();
  const match = router.asPath.match(/[^?]+/);
  const pathname = match ? match[0] : router.asPath;

  const location = useMemo(
    () =>
      process.browser
        ? window.location
        : ({
            search: router.asPath.replace(/[^?]+/u, ''),
          } as Location),
    [router.asPath]
  );

  const history = useMemo(
    () => ({
      push: ({ search }: Location) =>
        router.push(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true }
        ),
      replace: ({ search }: Location) =>
        router.replace(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true }
        ),
    }),
    [pathname, router]
  );

  return (
    <ContextProvider {...rest} history={history} location={location}>
      {children}
    </ContextProvider>
  );
};

export const QueryParamProvider = memo(QueryParamProviderComponent);

Also included the changes as proposed by @STEVEOO6, thanks for that, too!

Regaddi avatar Sep 01 '20 11:09 Regaddi

@Regaddi thanks for your solution, it worked for me ❤️

Just a few things my typescript was complaining about:

  • should use u flag. I'm not good at regexp, so not sure if it's necessary.

  const match = router.asPath.match(/[^?]+/);
  • push and replace return promises and should use async & await
    () => ({
      push: ({ search }: Location) =>
        router.push(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true }
        ),
      replace: ({ search }: Location) =>
        router.replace(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true }
        ),
    }),
    [pathname, router]
  );

tbntdima avatar Sep 14 '20 21:09 tbntdima

Was having one small problem with @Regaddi 's solution (which, this aside, works perfectly): history itself needs to also export location, so:

 const history = useMemo(
    () => ({
      push: ({ search }: Location) =>
        router.push(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true }
        ),
      replace: ({ search }: Location) => {
        router.replace(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true }
        );
      },
      location,
    }),
    [pathname, router.pathname, router.query, location.pathname]
  );

without this, LocationProvider.js would be using its most recently stored locationRef rather than the present location in the browser, which causes it to use an outdated location if you're updating the query params as you're moving away from a page (basically in the hooks version of "componentWillUnmount").

essential-randomness avatar Dec 26 '20 06:12 essential-randomness

Here's a complete example code based on all the previous answers:

import React, { memo, useMemo } from 'react'
import { useRouter } from 'next/router'
import { QueryParamProvider as ContextProvider } from 'use-query-params'

export const QueryParamProviderComponent = (props: {
  children?: React.ReactNode
}) => {
  const { children, ...rest } = props
  const router = useRouter()
  const match = router.asPath.match(/[^?]+/)
  const pathname = match ? match[0] : router.asPath

  const location = useMemo(
    () =>
      typeof window !== 'undefined'
        ? window.location
        : ({
            search: router.asPath.replace(/[^?]+/u, ''),
          } as Location),
    [router.asPath]
  )

  const history = useMemo(
    () => ({
      push: ({ search }: Location) =>
        router.push(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true }
        ),
      replace: ({ search }: Location) => {
        router.replace(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true }
        )
      },
      location,
    }),
    [pathname, router.pathname, router.query, location.pathname]
  )

  return (
    <ContextProvider {...rest} history={history} location={location}>
      {children}
    </ContextProvider>
  )
}

export const QueryParamProvider = memo(QueryParamProviderComponent)

Also replaced process.browser check with typeof window !== 'undefined' which is the right way of doing this (officially recommended).

yantakus avatar Apr 06 '21 10:04 yantakus

Edition for Next 10.1 https://nextjs.org/blog/next-10-1#router-methods-scroll-to-top. Disable auto scroll to top on each query-string changing.

import React, { memo, useMemo } from 'react'
import { useRouter } from 'next/router'
import { QueryParamProvider as ContextProvider } from 'use-query-params'

export const QueryParamProviderComponent = (props: {
  children?: React.ReactNode
}) => {
  const { children, ...rest } = props
  const router = useRouter()
  const match = router.asPath.match(/[^?]+/)
  const pathname = match ? match[0] : router.asPath

  const location = useMemo(
    () =>
      typeof window !== 'undefined'
        ? window.location
        : ({
            search: router.asPath.replace(/[^?]+/u, ''),
          } as Location),
    [router.asPath]
  )

  const history = useMemo(
    () => ({
      push: ({ search }: Location) =>
        router.push(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true, scroll: false }
        ),
      replace: ({ search }: Location) => {
        router.replace(
          { pathname: router.pathname, query: router.query },
          { search, pathname },
          { shallow: true, scroll: false }
        )
      },
      location,
    }),
    [pathname, router.pathname, router.query, location.pathname]
  )

  return (
    <ContextProvider {...rest} history={history} location={location}>
      {children}
    </ContextProvider>
  )
}

export const QueryParamProvider = memo(QueryParamProviderComponent)

baybara-pavel avatar Apr 08 '21 08:04 baybara-pavel

@baybara-pavel Dependency array on history object should be [pathname, location, router].

fpolic-profico avatar May 12 '21 11:05 fpolic-profico

I've published an adapter for Next.js: next-query-params

It was originally based on the code that is discussed in this issue but also contains a few bugfixes that popped up over time as well as support for the App Router. Thanks everyone for the collaboration here!

amannn avatar Sep 06 '21 06:09 amannn

@pbeshai any comment on including this in the package? Would be neat to have built-in nextjs support. I don't think you would even have to maintain it, I'm sure people would be happy to help out with updating it/ensuring that it works.

fgblomqvist avatar Jan 05 '22 10:01 fgblomqvist

A note for the people following along here: I've just published [email protected] which is based on use-query-params@2 and the introduced concept of adapters.

This removes the necessity for a wrapper and integrates much better now, including being able to make use of use-query-params directly.

amannn avatar Aug 30 '22 06:08 amannn

If the adapter is working well, you're certainly welcome to PR it into this repo if you'd like. Thanks for taking the initiative on this work! 🙇

pbeshai avatar Aug 31 '22 16:08 pbeshai