vercel / next.js

The React Framework
https://nextjs.org
MIT License
124.22k stars 26.48k forks source link

(Shallow routing) updating search params causes server code to rerun. #49668

Open Tomekmularczyk opened 1 year ago

Tomekmularczyk commented 1 year ago

Verify canary release

Provide environment information

{
  name: 'my-app',
  description: 'my-app',
  'dist-tags': {
    latest: '0.1.0'
  },
  versions: [
    '0.1.0'
  ],
  maintainers: [
    {
      name: 'mdathrika',
      email: 'mdathrika@ebay.com'
    }
  ],
  time: {
    modified: '2022-06-20T15:34:47.301Z',
    created: '2013-05-30T19:30:56.012Z',
    '0.1.0': '2013-05-30T19:30:58.974Z'
  },
  users: {
    'enas-443': true
  },
  version: '0.1.0',
  scripts: {
    start: 'node server.js'
  },
  license: 'Apache License 2.0',
  dependencies: {
    raptor: '>=2.3.0',
    optimist: '~0.3.5',
    'express-raptor': '~0.1.2',
    express: '~3.1.1',
    request: '~2.16.6',
    'ebay-global-header': 'git://github.scm.corp.ebay.com/cubejs-ebay/ebay-global-header-node.git',
    'raptor-optimizer-ready-plugin': 'git://github.scm.corp.ebay.com/cubejs-ebay/raptor-optimizer-ready-plugin.git',
    'ebay-ui-components': 'git://github.scm.corp.ebay.com/Raptor/RaptorUIComponents.git',
    'raptor-hot-reload': '~0.1.3',
    'socket.io': '~0.9.14',
    cluster2: 'git://github.scm.corp.ebay.com/cubejs/cluster2.git',
    'context-config': 'git://github.scm.corp.ebay.com/cubejs/context-config-node.git',
    'raptor-config': 'git://github.scm.corp.ebay.com/cubejs-ebay/raptor-config-node.git'
  },
  devDependencies: {},
  readmeFilename: 'README.md',
  dist: {
    shasum: '495332e31a4fa9bca5e100798f7cfb30663c73ad',
    tarball: 'https://registry.npmjs.org/my-app/-/my-app-0.1.0.tgz',
    integrity: 'sha512-CLIDEWQdnm3x96rWAQgm9u+jorK1KdGN9SC1qoyHuEc3KyKIw1NJ6fQ3p1NzDMPSpVt01ojiUbtdsZuIlQ4nPQ==',
    signatures: [
      {
        keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA',
        sig: 'MEUCIE2UwWOiqF7h+/wy7couMhgLBPBNm3dMhi3qyVUt+MtjAiEAlMyO12Ros0SDJiQRteOCMr+mPm8mtDGDmAn662Nkzr4='
      }
    ]
  },
  directories: {}
}

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true), Routing (next/router, next/navigation, next/link)

Link to the code that reproduces this issue

https://github.com/Tomekmularczyk/next-shallow-routing

To Reproduce

The app has 3 links that change the searchParam to one of: /?step=1, /?step=2 or /?step=3. Notice that initially when you click on the link and change the search param, the server code reruns and causes the all the data fetching logic to rerun. Once the link has been visited at least once, you can navigate without causing any server code to run again (thanks to in-memory client-side cache). See the video:

https://github.com/vercel/next.js/assets/12544704/14220251-c551-452a-b39a-f16e8853018a

Describe the Bug

Updating search params causes all the data on the server to be refetched when it's often a case where search params are used only to store some meta data or UI state.

Expected Behavior

I want to update the URL with a search param without rerunning the server code and triggering data fetching all over again. This used to be something called a shallow routing in the previous versions of NEXT.

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

Vercel

Related discussion https://github.com/vercel/next.js/discussions/48110#discussioncomment-5862857

Tomekmularczyk commented 1 year ago

Sorry if that is not a bug but a feature request. Seems like this is a very common use case that is missing from the new NEXTjs or is now any other way to achieve what I want?

izica commented 1 year ago

@Tomekmularczyk try to use history.replaceState(null, "", url);

fernandojbf commented 12 months ago

shallow is a feature present in the Pages Router. To be honest i believe will be difficult to create some features without it for App Router.

Probably one good way to communicate between server and client components is query parameters using shallow. without the prevention of not calling the server again makes this approach impossible

franky47 commented 11 months ago

@Tomekmularczyk try to use history.replaceState(null, "", url);

Thanks for this tip! While it helps a lot with performance (not triggering a cascade of requests), I found there's a lack of sync between the history state and Next's router, meaning that clicking Links and using the imperative router.replace methods won't always trigger a page render.

My use-case here is bringing app router support for next-usequerystate. The initial implementation (going through the router) ended up triggering a lot of network requests and blocking the UI (see discussion). Switching to the history API helps a lot with performance, but brings all kinds of sync issues with normal Next.js routing.

Edit: next-usequerystate now supports shallow updates of search params, and all issues above have been resolved. Check it out!

BenMcD0nald commented 11 months ago

Just to let anyone finding this topic know that there is a more comprehensive discussion on this issue here #48110 where the Devs are saying a solution is on the roadmap....

baunov commented 9 months ago

For anyone experiencing similar issue, I've also used @izica solution for pure client-side navigation. I have a case where I also need to watch the query params client-side. And to make it work I've created this custom hook:

import {useState, useEffect, useCallback} from 'react';
import {usePathname, useSearchParams} from "next/navigation";

function patchHistory(history: History){
    if ((history as any)['patched']) {
        return;
    }
    const pushState = history.pushState;
    const replaceState = history.replaceState;

    history.pushState = function(state) {
        pushState.apply(history, arguments as any);
        window.dispatchEvent(new Event('pushstate'));
    };

    history.replaceState = function(state) {
        replaceState.apply(history, arguments as any);
        window.dispatchEvent(new Event('replacestate'));
    };

    (history as any)['patched'] = true;
}
export function useCurrentUrl(): URL | null {
    // Helper function to get the current URL

    const pathname = usePathname();
    const query = useSearchParams();
    const [currentUrl, setCurrentUrl] = useState<string | null>(
        typeof window === "undefined" ? null : window.location?.href
    );

    const handleUrlChange = useCallback(() => {
        setCurrentUrl(window.location.href);
    }, []);

    useEffect(() => {
        if (typeof window === "undefined") {
            return;
        }

        handleUrlChange();
        patchHistory(window.history);

        // Add event listener for URL changes
        window.addEventListener('popstate', handleUrlChange);
        window.addEventListener('pushstate', handleUrlChange); // Custom event for pushState
        window.addEventListener('replacestate', handleUrlChange); // Custom event for replaceState

        // Clean up the event listener on component unmount
        return () => {
            window.removeEventListener('popstate', handleUrlChange);
            window.removeEventListener('pushstate', handleUrlChange);
            window.removeEventListener('replacestate', handleUrlChange);
        };
    }, []);

    if (!currentUrl) {
        // just use your hostname instead of process.env.VERCEL_URL
        return new URL(`https://${process.env.VERCEL_URL}${pathname}?${query.toString()}`);
    }
    return new URL(currentUrl);
}

Hope we'll get the official solution soon. Nextjs 14.0.3 - still same issue.