20

Using nextjs with next-routes, is it possible to preserve the URL's query string when navigating between pages? I have an ad-campaign running and I need to preserve it as I navigate through pages for historic reasons and tracking.

I cannot stuff this into a Redux store, or localhost, sessionstorage, etc. It must remain in the URL.

I tried something like the following:

import { Router } from 'routes';

Router.events.on('routeChangeStart', (url: string) => {
  if (Router.query && Router.router.pathname !== url) {
    const href = `${url}${Object.keys(Router.query).reduce(
      (carry: string, key: string) => {
        carry += `${carry === '' ? '?' : '&'}${key}=${Router.query[key]}`;
                return carry;
    },'')}`;

    Router.pushRoute(href, Router.router.pathname, { shallow: true });
  }
});

And the routes.js file exports next-routes:

const nextRoutes = require('next-routes');  
const routes = (module.exports = nextRoutes());

What happens here is that the URL is correctly pushed and the query string persists, but only for a momentary flash. It immediately pushes the original url back into the Router and I lose my query string arguments.

I've tried several other variations but unfortunately I cannot find the correct implementation.

Any help is appreciated.

7
  • Unfortunately, I think once routing is kicked off in next.js (which the routeChangeStart event detects) you can do some actions, but not change the route itself, which is probably why you see that brief flash before it reverts back to the original route being requested. You should consider maybe defining your own LinkWithQuery component that wraps next's Link component and appends the querystring you want to preserve to any such Link. Commented Feb 8, 2019 at 19:46
  • @Jaxx That's exactly the route that I ended up taking. It's not elegant by any means and unfortunately required some modifications, but it's the only solution I've found that's working so far. Thanks! Commented Feb 8, 2019 at 23:55
  • 1
    @Terkhos I didn't. I ended up going a completely different direction because this was unreasonably difficult . Commented Jan 14, 2020 at 18:51
  • 1
    @Terkhos Can you mention how did you solved this problem? Commented Jun 29, 2020 at 8:05
  • 1
    @invalidtoken I used the exact method described in the first comment. It wasn't elegant to replace every single link on my page with this a new component, but it proved to be less time consuming. Commented Jun 29, 2020 at 15:07

7 Answers 7

5

For those using NextJS 13+ and the new App router, you can create a custom hook that wraps the standard rooter hook. Here's how I've done it:

import { NavigateOptions } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useRouter, useSearchParams } from "next/navigation";

export interface CustomRouterOptions {
  preserveQuery: boolean;
}

export function useCustomRouter() {
  const router = useRouter();
  const searchParams = useSearchParams();

  const push = (href: string, routerOptions?: CustomRouterOptions, options?: NavigateOptions) => {
    // HACK: If relative URL given, stick the current host on the string passed to URL()
    // as the constructor throws an error if URL without a host is given
    const url = new URL(href.includes("http") ? href : window.location.host + href);
    if (routerOptions?.preserveQuery) {
      searchParams.forEach((val, key) => {
        url.searchParams.append(key, val);
      });
    }

    let urlString = url.toString();

    // If the href arg was relative, strip everything before the first '/' to
    // revert it back to a relative URL we can pass into the router.push() method
    if (!href.includes("http")) {
      urlString = urlString.substring(urlString.indexOf("/"));
    }

    router.push(urlString, options);
  };

  return { push };
}
Sign up to request clarification or add additional context in comments.

2 Comments

I've moved the accepted answer to this question as it is more time-appropriate now given the initial age of the question. Please see Ariel's excellent answer above for a more legacy solution if you're not up to date in your NextJS app.
How will this help in <Link> tags I am using
3

After some searching i managed to get the result i wanted with the following:

const customRouter = (module.exports = nextRoutes());

customRouter.Router.pushRouteWithQuery = function(route, params, options) {
    if(!params || !Object.keys(params).length) {
        const routeWithParams = QueryStringHelper.generateUrlWithQueryString(route, customRouter.Router.router.query);
        customRouter.Router.pushRoute(routeWithParams, params, options);
    } else {
        const filteredParams = QueryStringHelper.filterAllowedParams(customRouter.Router.router.query);
        const allParams = {
            ...filteredParams,
            ...params
        };

        customRouter.Router.pushRoute(route, allParams, options);
    }
}

And i would then use my newly created method to redirect to another page with the desired query strings:

import { Router } from 'my/module'

Router.pushRouteWithQuery('/home');

And finally the QueryStringHelper:

module.exports = {
    generateUrlWithQueryString,
    getAllowedParams,
    prepareParamsAsQueryString,
    filterAllowedParams
}

function getAllowedParams() {
    const allowedParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'mid', 'gclid', 'source'];

    return allowedParams;
}

function prepareParamsAsQueryString() {
    const params = getAllowedParams();
    let paramsLikeQueryString = [];

    for (let index = 0; index < params.length; index++) {
        const param = params[index];

        paramsLikeQueryString.push(`${param}=:${param}?`);
    }

    return paramsLikeQueryString.join('&');
}

function generateUrlWithQueryString(url, params) {
    if(Object.keys(params).length) {
        const filteredParams = filterAllowedParams(params);

        if (Object.keys(filteredParams).length) {
            if(url[0] != '/')
                url = `/${url}`;

            return `${url}?${serialize(filteredParams)}`;
        }

        return url;
    }

    return url;
}

function filterAllowedParams(params) {
    if(Object.keys(params).length) {
        const filteredParams = Object.keys(params)
            .filter(key => getAllowedParams().includes(key.toLowerCase()))
            .reduce((obj, key) => {
                obj[key] = params[key];
                return obj;
            }, {});

        return filteredParams;
    }

    return params;
}

// INTERNAL
function serialize(obj) {
    var str = [];
    for (var p in obj) {
        if (obj.hasOwnProperty(p)) {
            str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
        }
    }

    return str.join("&");
}

2 Comments

Hi Ariel, how do you manage the file structure with this, I guess the "module.exports" are inside from "next.config.js" but to be honest I don't understand where to locate the other code snippets? Would you mind providing me some guidance with this? - Thanks.
@KennethBregat i'm really sorry but i do not have access to this project anymore. What i recall from this is that all i had to do was add this code to the project and create the customRoute. Have you tried to achieve this in the newer version of NextJS?
1

What helped us was the URL and URLSearchParams APIs.

We created a few small methods that we use them when using router.push, here is an example of changing routes but persisting the query params:

/**
 *
 * @param currentPath the current asPath the user is on, eg /stuff/example/more-stuff?utm-campaing=test
 * @param newPath the new path we want to push to the router, eg /stuff/example/even-more-stuff
 * @returns the newPath with the query params from the existing path, eg /stuff/example/even-more-stuff?utm-campaing=test
 */
export const changeRouteKeepParams = (
  currentPath: string,
  newPath: string
): string => {
  const hasQueries = currentPath.indexOf('?');
  const searchQuery = hasQueries >= 0 ? currentPath.substring(hasQueries) : '';
  const params = new URLSearchParams(searchQuery);
  const paramsStr = params.toString() !== '' ? `?${params.toString()}` : '';
  return `${newPath}${paramsStr}`;
};

1 Comment

URLSearchParams are very useful. it allows you to use existing query props without triggering a re-render because you no longer need router.query as a useEffect dependency
1
npm install query-string
import Link from 'next/link'
import {useRouter} from 'next/router'
import queryString from 'query-string'

export const Example = () => {
  const router = useRouter()
  const query = queryString.stringify(router.query)
  const url = query ? `${router.pathname}?${query}` : router.pathname

  return <Link href={url}></Link>
}

Comments

1

If you want to use the Link component then you can create a custom link wrapper of next link. Use this wrapper in all the places where you want the Query params to forward with the link -

'use client';

import Link, { LinkProps } from 'next/link';
import { useSearchParams } from 'next/navigation';
import { ReactNode } from 'react';

interface LinkWithQPProps extends LinkProps {
  children?: ReactNode;
  className?: string;
  ref?: any;
  href: any;
}
export function LinkWithQP({
  ref,
  className,
  href,
  children,
}: LinkWithQPProps) {
  const searchParams = useSearchParams();

  const search = `?${searchParams.toString()}`;

  // handle and ids (#contact, #aboutUs) in the href
  const customHref = href.includes('#')
    ? href.split('#')[0] + search + '#' + href.split('#')[1]
    : href + search;
  return (
    <Link ref={ref} className={className} href={customHref}>
      {children}
    </Link>
  );
}

Comments

0

This was very tricky to solve. The other answers on this question above helped me enormously, but many are overly complex, and others introduce bugs around edge cases, e.g. non-http URLs, SSR, etc. Use this instead.


export const preserveParams = (
  dest: string,
  preservedParams?: URLSearchParams,
) => {
  const [destHref, destSearch] = dest.split('?');
  if (!preservedParams) return dest;
  if (!destSearch) return `${destHref}?${preservedParams.toString()}`;
  const destParams = new URLSearchParams([
    ...Array.from(preservedParams.entries()),
    ...Array.from((new URLSearchParams(destSearch)).entries()),
  ]);

  return `${destHref}?${destParams.toString()}`;
};

export function useRouter() {
  const nextRouter = useNextRouter();
  const params = useSearchParams();
  type Push = typeof nextRouter.push;
  const push = (...[href, options]: Parameters<Push>): ReturnType<Push> =>
    nextRouter.push(preserveParams(href, params), options);

  return { ...nextRouter, push };
}

Advantages:

  • For Next 13+ ('app' router)
  • parsimonious ('DRY')
  • robust. e.g. does not depend on magic strings, e.g. 'http', as some solutions above
  • performant. Never does unnecessary computation or object instantiation.
  • minimal freehand string munging, aside from the split on '?', but the URL RFC guarantees that this is sane and reliable
  • preserveParams() function can be used with e.g. a custom Link component, and does not rely on Hooks (and therefore is not itself a Hook)
  • if you want to get really fancy you can use a Proxy object instead of just assigning your new push() to the existing object, for perfect compat
  • does not depend on window.location, in contrast to solutions above. window is often unavailable in NextJS execution context (e.g. SSR)
  • I use the pattern from the moz docs of URLSearchParams so I have high confidence that edge cases are covered

Comments

-2

I got the query string from the URL and passed it long wherever I was using the component.

import { useRouter } from "next/router";
import { useEffect, useState } from "react";

export const getUrlPathParams = (): string => {
  const [pathParams, setPathParams] = useState("");
  useEffect(() => {
    setPathParams(router?.asPath?.slice(router?.pathname?.length));
  });
  const router = useRouter();
  return pathParams;
};

And an example of using it in a link.

<Link href={"/" + getUrlPathParams()}> Home </Link>

I'm doing the same thing for my external links so that my ad campaign works.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.