Edit: This article is outdated. I wrote an updated version of this article for Next 15: Synchronous and asynchronous searchParams in Next 15
A while back I wrote a fairly popular article on dev.to: How to mock Next router with Jest. Since then, Next 13 has arrived and brought us a new router and new router hooks.
In this first part we will build a little project using all these new hooks. In the second and third parts we will learn how to test these new hooks and functions. All the files we use are available on github.
The plan
We are building a project where you can sort a list of fruits by clicking sort buttons.
Clicking a button pushes a new route with a sortOrder parameter to the router: /sortList?sortOrder=asc or /sortList?sortOrder=desc. The list of fruits is sorted accordingly.
I used a fresh create-next-app (13.4.8) install with typescript and eslint. Cleaned out the boilerplate, added all Jest packages, added eslint packages for Jest and configured them.
Note: I originally tried building this with radio inputs but due to a bug in Next 13, it didn't work properly. I left the files in the repo in case you are interested.
Get query strings
There are 2 ways to get access to url parameters:
-
searchParamspage prop in the root of a route (the page file) returns an object with the parameters: f.e.{ foo: 'bar' } - The
useSearchParamshook returns a readonlyURLSearchParamsinterface.
The URLSearchParams interface defines utility methods to work with the query string of a URL.
source: MDN
Among others, it allow you to get or set a parameter, to check if a parameter is present, to get the keys and values of all the parameters,... In short, it is a handy set of tools to read and update query strings.
Project outline
There are 3 things we want to work with:
-
searchParamspage prop -
useSearchParamshook -
useRouterhook
We will make a <List /> component that is responsible for sorting our list of fruits and rendering it. To read the sortOrder parameter from the url, we use searchParams in the page root and pass it to <List />.
function Page({ searchParams }) {
return <List {...pass searchParams...} />;
}
Our second main component <ListControlesButtons /> will hold our sort buttons. In this component we will use the useSearchParams hook to get access to the query string and also use useRouter to push new routes to the router.
function Page({ searchParams }) {
return (
<>
<ListControlesButtons />
<List {...pass searchParams...} />
</>
);
}
searchParams prop
What do we expect from searchParams? There are a couple of options:
| sortorder | route | searchParams |
|---|---|---|
| none | /sortList |
{} |
| valid | /sortList?sortOrder=asc |
{ sortOrder: 'asc' } |
/sortList?sortOrder=desc |
{ sortOrder: 'desc' } | |
| invalid | /sortList?sortOrder=foobar |
{ sortOrder: 'foobar' } |
/sortList?sortOrder |
{ sortOrder: '' } |
This means we will have to validate searchParams. We create a type for represent searchParams:
// types/index.d.ts
export type SearchParams = {
sortOrder?: string;
};
page.js
Here is our full Page component:
// app/sortList/page.tsx
import { SearchParams } from '@/types';
import getSortOrderFromSearchParams from '@/lib/getSortOrderFromSearchParams';
import ListControlesButtons from '@/components/ListControlesButtons';
import List from '@/components/List';
type Props = {
searchParams: SearchParams;
};
export default function Page({ searchParams }: Props) {
const sortOrder = getSortOrderFromSearchParams(searchParams);
return (
<>
<ListControlesButtons />
<List sortOrder={sortOrder} />
</>
);
}
getSortOrderFromSearchParams.ts
getSortOrderFromSearchParams is the function that validates searchParams. It returns either asc or desc. It will return asc as the default value when:
- There is no
sortOrderparam. -
sortOrdervalue is empty. -
sortOrdervalue is invalid (notascordesc).
When none of the above conditions were fulfilled, the value of sortOrder will be valid (asc or desc) and we can use it, else the default asc is passed. We also created a type for this:
// types/index.d.ts
...
export type SortOrder = 'asc' | 'desc';
// lib/getSortOrderFromSearchParams.ts
import { SearchParams, SortOrder } from '@/types';
import validateSortOrder from './validateSortOrder';
export default function getSortOrderFromSearchParams(
searchParams: SearchParams
): SortOrder {
const sortOrderParam = searchParams.sortOrder;
let sortOrder: SortOrder = 'asc';
if ('sortOrder' in searchParams && sortOrderParam) {
if (validateSortOrder(sortOrderParam)) {
sortOrder = sortOrderParam;
}
}
return sortOrder;
}
validateSortOrder.ts
The validateSortOrder is a type predicate. It checks if the value is either asc or desc and returns a boolean.
// lib/validateSortOrder.ts
import { SortOrder } from '@/types';
export default function validateSortOrder(value: string): value is SortOrder {
const validOptions: [SortOrder[0], SortOrder[1]] = ['asc', 'desc'];
return validOptions.includes(value as SortOrder);
}
To recap: in page.js we validate searchParams and we then pass it to <List sortOrder={sortOrder} />. When sortOrder is passed, it is validated, meaning it will always be asc or desc.
List.js
Our <List /> component is simple. It just sorts a list of fruits along the sortOrder prop:
// components/List.tsx
import { SortOrder } from '@/types';
type Props = {
sortOrder: SortOrder;
};
export default function List({ sortOrder }: Props) {
const list = ['Banana', 'Apple', 'Lemon', 'Cherry'];
const sortedList = [...list].sort((a, b) => {
if (sortOrder === 'asc') {
return a > b ? 1 : -1;
}
return a < b ? 1 : -1;
});
return (
<ul>
{sortedList.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
ListControlesButtons.tsx
This is our second main component. We factored away all the hooks inside a custom hook useSort that returns sortOrder (asc or desc) and an event handler handleSort.
// components/ListControlesButtons.tsx
'use client';
import useSort from '@/hooks/useSort';
export default function ListControlesButtons() {
const { sortOrder, handleSort } = useSort();
return (
<div>
<div>sort order: {sortOrder === 'asc' ? 'ascending' : 'descending'}</div>
<button onClick={() => handleSort('asc')}>sort ascending</button>
<button onClick={() => handleSort('desc')}>sort descending</button>
</div>
);
}
useSort.ts
useSort is a custom hook. It return 2 things:
-
sortOrder: validated fromuseSearchParams()hook -
handleSort: an event handler that pushes routes to the router
// hooks/useSort.ts
import { SortOrder } from '@/types';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import getSortOrderFromUseSearchParams from '../lib/getSortOrderFromUseSearchParams';
export default function useSort() {
const params = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const sortOrder = getSortOrderFromUseSearchParams(params);
const handleSort = (value: SortOrder) => {
const newParams = new URLSearchParams(params.toString());
newParams.set('sortOrder', value);
router.push(`${pathname}?${newParams.toString()}`);
};
return { sortOrder, handleSort };
}
sortOrder
In useSort we use the useSearchParams hook. As mentioned above, this returns a readonly URLSearchParams interface. Similar to what we did in page.js, we need to validate: check if there is a sortOrder parameter and that its value is valid.
But we can't reuse our getSortOrderFromSearchParams function because that only accepts objects and we now have a URLSearchParams interface. So, we wrote a new function getSortOrderFromUseSearchParams. It does the exact same checks but uses the URLSearchParams methods like .has() and .get().
// lib/getSortOrderFromUseSearchParams.ts
import { SortOrder } from '@/types';
import { ReadonlyURLSearchParams } from 'next/navigation';
import validateSortOrder from '../lib/validateSortOrder';
export default function getSortOrderFromUseSearchParams(
params: ReadonlyURLSearchParams
) {
const sortOrderParam = params.get('sortOrder');
let sortOrder: SortOrder = 'asc';
if (params.has('sortOrder') && sortOrderParam) {
if (validateSortOrder(sortOrderParam)) {
sortOrder = sortOrderParam;
}
}
return sortOrder;
}
handleSort()
Finally, our handleSort function is an event listener that gets called when clicking the sort buttons. What does this function need to do?
First, we have our query string that we got from useSearchParams. We want to append or overwrite to this string the following: sortOrder=asc or sortOrder=desc.
But, useSearchParams returns a readonly URLSearchParams interface. We can't change it. So we create a new URLSearchParams and pass into that the URLSearchParams we got from useSearchParams. Why do we pass in the old URLSearchParams? Because there may be other parameters and we don't want to discard them.
const newParams = new URLSearchParams(params.toString());
The URLSearchParams constructor accepts a string so we use the build-in toString method. If we were on route /sortList?sortOrder=desc&foo=bar then params.toString() would equal sortOrder=desc&foo=bar.
Our newParams variable now has access to these parameters:
-
newParams.get('sortOrder')->desc. -
newParams.get('foo')->bar.
newParams is also not readonly. We can make changes. How? By using the set() method:
newParams.set('sortOrder', value);
value is the value that handleSort got called with inside our buttons: either 'asc' and 'desc'. We don't need to validate this because they are hardcoded inside our buttons.
The last step we need is to construct a route and push this route to router:
router.push(`${pathname}?${newParams.toString()}`);
We got pathname from the usePathname hook and it would be localhost/sortList. We then add the ? and our updated query string (newParams.toString()) and push the route. And we are done. Our project is fully functioning.
Recap
You may be thinking this was a lot of code for such a little project. But we were quite thorough:
- We used Typescript.
- We validated our query strings.
- We split our code into functions and custom hooks to facilitate testing. (see next part)
- We used both
searchParamspage prop anduseSearchParamshook to access our query string.
In the end, the project is quite simple:
- In our route root (page.js) we validate
searchParamsand passsortOrderto<List />. -
<List />sorts our fruits along thissortOrderparameter. -
<ListControlesButtons />renders our buttons. It gets access tosortOrderandhandleSortvia a custom hookuseSort. - The custom
useSorthook:- Gets the query string via
useSearchParamshook. It validates this string usinggetSortOrderFromUseSearchParamsand returnssortOrder(asc|desc). - Creates an event handler for the buttons:
handleSort. When called, this function creates a newURLSearchParamsfrom the readonly one thatuseSearchParamsreturns. It adds (or overwrites)sortOrderwith a new value. It then constructs a new route fromusePathnameand our newURLSearchParamsand pushes that to the router.
- Gets the query string via
In the next parts we are going to test all of these components and functions with Jest and rtl.

Top comments (2)
Created reusable hook to simplify boilerplate github.com/asmyshlyaev177/state-in...
Thanks for this, I was looking for a neat way to deal with updating search params without discarding other search params.