This is part 2 of a series on searchParams
in Next 15
. In the first part we talked about synchronous and asynchronous searchParams
. Later, we want to run some tests on this but this means we first need some code to run tests on. This will be our focus in this chapter.
Note: this code is available in a github repo.
SPOILER: Right now, @testing-library/react
doesn't work for Next 15
. This means that we cannot write these tests yet. See part 3.
List
We will make a little app where we can sort a list of fruits ascending or descending. Preview:
This app consists of 2 components: the actual list and the sort buttons. For the list, we will pass the searchParams
prop from page to <List />
. So route list?sortOrder=asc
will pass asc
: <List sortOrder='asc' />
.
For the sort buttons we will use the useSearchParams
hook. This gives me the opportunity to demonstrate how to mock next/navigation
hooks in Next 15
. Pushing the buttons calls push
function on useRouter
, f.e. router.push('/list?sortOrder=desc')
.
/list/page.tsx
Our first component is the route root page.tsx:
// src/app/list/page.tsx
import List from '@/components/List';
import ListControles from '@/components/ListControles';
import validateSortOrder from '@/lib/validateSortOrder';
type Props = {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
const ITEMS = ['apple', 'banana', 'cherry', 'lemon'];
export default async function ListPage({ searchParams }: Props) {
const currSearchParams = await searchParams;
const sortOrder = validateSortOrder(currSearchParams.sortOrder);
return (
<>
<h2 className='text-2xl font-bold mb-2'>List</h2>
<ListControles />
<List items={ITEMS} sortOrder={sortOrder} />
</>
);
}
In this component we use the asynchronous searchParams
request. We extract a sortOrder value from searchParams
using the validateSortOrder
function:
// src/lib/validateSortOrder.ts
import isSortOrderT from '@/types/isSortOrderT';
import { SortOrderT } from '@/types/SortOrderT';
const DEFAULT_SORT_ORDER: SortOrderT = 'asc';
export default function validateSortOrder(
value: string | string[] | undefined | null
) {
if (!value) return DEFAULT_SORT_ORDER;
if (Array.isArray(value)) return DEFAULT_SORT_ORDER;
if (!isSortOrderT(value)) return DEFAULT_SORT_ORDER;
return value;
}
This function checks if searchParams.sortOrder
is either asc
or desc
and returns the default asc
when it's not.
List
Our <List />
component receives the validated sortOrder value ('asc' | 'desc'
) and simply sorts the fruits accordingly. Nothing new here:
// scr/components/List.tsx
import { SortOrderT } from '@/types/SortOrderT';
type Props = {
items: string[];
sortOrder: SortOrderT;
};
const SORT_CALLBACKS = {
asc: (a: string, b: string) => (a > b ? 1 : -1),
desc: (a: string, b: string) => (a < b ? 1 : -1),
};
export default function List({ items, sortOrder }: Props) {
const sortedItems = items.sort(SORT_CALLBACKS[sortOrder]);
return (
<ul className='list-disc'>
{sortedItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
ListControles
Our final component holds our sort buttons and uses the useSearchParams
hook.
// src/components/ListControles.tsx
'use client';
import validateSortOrder from '@/lib/validateSortOrder';
import { SortOrderT } from '@/types/SortOrderT';
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
export default function ListControles() {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
// get validated sortOrder value
const sortOrder = validateSortOrder(searchParams.get('sortOrder'));
function handleSort(val: SortOrderT) {
const newParams = new URLSearchParams(searchParams);
newParams.set('sortOrder', val);
newParams.set('ha ha ha ha', '123 456 789');
router.push(`${pathname}?${newParams.toString()}`);
}
return (
<div>
<div className='mb-2'>current sort order: {sortOrder}</div>
<div className='flex gap-1'>
<button
className='bg-blue-700 text-white py-1 px-4 rounded-sm'
onClick={() => handleSort('asc')}
>
sort ascending
</button>
<button
className='bg-blue-700 text-white py-1 px-4 rounded-sm'
onClick={() => handleSort('desc')}
>
sort descending
</button>
</div>
</div>
);
}
useSearchParams
The useSearchParams
hook returns a readonly URLSearchParams
interface: ReadonlyURLSearchParams
.
The URLSearchParams interface defines utility methods to work with the query string of a URL.
source: MDN
These utility methods includes things like: has()
, get()
and set()
. So, for example, on url /list?foo=bar
, we can do this:
const searchParams = useSearchParams();
searchParams.has('foo'); // true
searchParams.has('mooooo'); // false
searchParams.get('foo'); // 'bar'
searchParams.get('mooooo'); // null
But, in this case, we can't use .set
. Why not? Because useSearchParams
returns a readonly ReadonlyURLSearchParams
interface.
searchParams.set('foo', bar); // Unhandled Runtime Error (crash)
Given the simplicity of this app we could just manually write our new search params:
router.push(`/list?sortOrder=${value}`);
But that has 2 potential problems. Firstly, value isn't url encoded (we could again do this manually). Secondly, we lose other potential search params. For example if we are on url /list?sortOrder=desc&color=red
, we would want to keep the color parameter and not just delete it.
That is why we use URLSearchParams
. But, we need to go from a readonly to a read and write URLSearchParams
interface. Luckily, this is quite easy. Here is our handleSort function from our <ListControles />
component
function handleSort(val: SortOrderT) {
const newParams = new URLSearchParams(searchParams);
newParams.set('sortOrder', val);
router.push(`${pathname}?${newParams.toString()}`);
}
- We create a new
URLSeachParams
interface and pass in the old one. This way we don't lose any search params. The newURLSeachParams
is not readonly. - Next, we write a new sortOrder value using the
.set
method. This will also url encode both the key and the value. - Finally, we need to go from a
URLSearchParams
interface to an actual url search params string which we do by simply calling the.toString()
method on the interface.
By doing it this way we preserve existing search params and we also get some free url encoding. All and all a handy API. I like it.
Recap
We just build a simple fruit sorting app so we have some code to test and mock with Jest
and React Testing Library
.
We have our page component that uses asynchronous searchParams
prop and then our <ListControles />
component were we will have to mock usePathname
, useSearchParams
and useRouter
.
We will do this in the next chapters.
If you want to support my writing, you can donate with paypal.