matt1331
matt1331

Reputation: 105

NextJS how to make router.push not a shallow route

I have a search page, and when a user searches for something, for example, chairs, it will push to the route /search-page/chairs using router.push("/search-page/" + searchQuery);

However, a problem that is arising is that when a user makes another search while already inside the search-page, the query in the URL, in the address bar, updates, but the page doesn't refresh, thus not updating products. I have tried router.push("/search-page/" + searchQuery, undefined, {shallow: false}); to try to force the router.push to not be shallow, but that didn't work. I have also tried

componentDidUpdate(prevProps){
  if(this.state.router.asPath != prevProps.router.asPath){
   updateProducts()
 }
}

to check if when the component updates, that the previous URL is not equal to the URL currently. However, this if statement doesn't seem to work too. Something isn't being updated properly. Since i'm using NextJS, perhaps there is something I can do regarding the getServerSideProps? But I'm not too familiar with how getServerSideProps or getIntialProps work. Or maybe there is a way to update the query in the URL, and then force refresh the page afterwards, like a callback function. thanks

Upvotes: 1

Views: 3060

Answers (2)

Andrew Ross
Andrew Ross

Reputation: 1158

One approach I've used is wrapping the returned search query JSX in a memo, or memoizing it to update state on change. Here is code from a subreddit search project I built several months ago on the side

@/components/Searchbar.tsx
import { FC, useEffect, useMemo, useState } from 'react';
import cn from 'classnames';
import css from './Searchbar.module.css';
import { useRouter } from 'next/router';
import { Input } from '../UI';
import { filterQuery } from '@/lib/helpers';

interface Props {
    className?: string;
    id?: string;
}

const Searchbar: FC<Props> = ({ className, id = 'r/' }) => {
    const router = useRouter();
    const [value, setValue] = useState('');
    useEffect(() => {
        // router.prefetch(url, as)
        router.prefetch('/r/[display_name]', `/r/${router.query}`, {
            priority: true
        });
    }, [value]);

    return useMemo(
        () => (
            <div
                className={cn(
                    'relative bg-accents-1 text-base w-full transition-colors duration-150',
                    className
                )}
            >
                <label className='sr-only' htmlFor={id}>
                    /r/ - search by subreddit name
                </label>
                <div className='absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none'>
                    <span className='text-gray-100 font-semibold sm:text-base'>
                        /r/
                    </span>
                </div>
                <Input
                    id={id}
                    name={id}
                    onChange={setValue}
                    className={css.input}
                    defaultValue={
                        router && router.query ? (router.query.q as string) : ''
                    }
                    onKeyUp={e => {
                        e.preventDefault();

                        if (e.key === 'Enter') {
                            const q = e.currentTarget.value;

                            router.push(
                                {
                                    pathname: `/r/${q}`,
                                    query: q ? filterQuery({ q }) : {}
                                },
                                undefined,
                                { shallow: true }
                            );
                        }
                    }}
                />
                <div className={css.iconContainer}>
                    <svg
                        className={css.icon}
                        fill='rgb(229, 231, 235)'
                        viewBox='0 0 20 20'
                    >
                        <path
                            fillRule='evenodd'
                            clipRule='evenodd'
                            d='M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z'
                        />
                    </svg>
                </div>
            </div>
        ),
        []
    );
};

export default Searchbar;
Corresponding @/components/Searchbar.module.css file
.input {
    @apply bg-redditSearch px-3 pl-7 py-2 appearance-none w-full transition duration-150 ease-in-out pr-10 text-gray-100 font-semibold;

    @screen sm {
        min-width: 300px;
        @apply text-lg;
    }
    @screen md {
        min-width: 600px;
        @apply text-lg;
    }
}

.input:focus {
    @apply outline-none text-gray-100;
}

.iconContainer {
    @apply absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none;
}

.icon {
    @apply h-5 w-5;
}

I prefetch the targeted rout inside of a useEffect hook to enhance UX in a production environment; it would enhance UX in development too but prefetching only works in prod currently.

Then, there is an @/pages/api route handling the user-input value as follows:

@/pages/api/snoosearch.ts
import { Subreddit, Listing } from 'snoowrap';
import { NextApiRequest, NextApiResponse } from 'next';
import { r } from '@/lib/snoo-config';

export type SearchSubreddits = {
    subreddit: Listing<Subreddit> | never[];
    found: boolean;
};

export default async function (
    req: NextApiRequest,
    res: NextApiResponse<SearchSubreddits>
) {
    const { q } = req.query;
    console.log(q);
    const data = q
        ? await r.searchSubreddits({
                query: (q as string) ?? 'snowboarding',
                count: 10,
                limit: 3
          })
        : [];
    res.statusCode = 200;
    res.setHeader(
        'Cache-Control',
        'public, s-maxage=1200, stale-while-revalidate=600'
    );

    return res.status(200).json({
        subreddit: data,
        found: true
    });
};

So, this lambda injects getStaticPaths of a dynamic subdirectory to handle real-time static path generation (as well as optional localization if using internationalized routing) and populating the content of any given subreddit via ISR.

If you've had to write tests in Nextjs using Jest in a typescript environment, you may be familiar with having to mock the next router by replicating it in your testing environment. This helped me learn a lot about the internals of the router:

TLDR -- Memoization of the searchbar where user search is submitted updates state which forces a non-shallow render which should solve your problem

Mock Nextjs router
// Mocks useRouter
type PrefetchOptions = {
    priority?: boolean;
    locale?: string | false;
};

const useRouter = jest.spyOn(
    require('next/router'),
    'useRouter'
);

/**
 * mockNextUseRouter
 * Mocks the useRouter React hook from Next.js on a test-case by test-case basis
 */
export function mockNextUseRouter(props: {
    route: string;
    prefetch(
        url: string,
        asPath?: string,
        options?: PrefetchOptions
    ): Promise<void>;
    pathname: string;
    query: string;
    asPath: string;
}) {
    useRouter.mockImplementationOnce(() => ({
        route: props.route,
        prefetch: props.prefetch,
        pathname: props.pathname,
        query: props.query,
        asPath: props.asPath
    }));
}

Upvotes: 0

matt1331
matt1331

Reputation: 105

Incase anyone was wondering, I managed to figure it out by using window.location

componentDidUpdate() {

  var str = window.location.pathname;
  var n = str.lastIndexOf('/');
  var result = str.substring(n + 1);
  if(result != this.state.searchQuery){//if the current URL doesn't match the URL stored in the state (which is the previous url before making a new search)
    //grab the query from the current URL, and update the searchQuery state with that query from the current URL
    this.updateProducts(result);
  }
}

In the following code, when the componentUpdates, it grabs the query param from window.location.pathname using substring. Then it compares that query param from the window.location to query param from this.state.searchQuery, if they aren't the same, then update this.state.searchQuery with the param extracted from window.location.pathname, and call the function to update the products based on the users input.

Upvotes: 2

Related Questions