Reputation: 105
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
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
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
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