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.
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) {
    // this is incorrect, it should be
    // const newParams = new URLSearchParams(searchParams.toString());
    // we fix this later on
    const newParams = new URLSearchParams(searchParams);
    newParams.set('sortOrder', val);
    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) {
  // this is incorrect, it should be
  // const newParams = new URLSearchParams(searchParams.toString());
  // we fix this later on
  const newParams = new URLSearchParams(searchParams);
  newParams.set('sortOrder', val);
  router.push(`${pathname}?${newParams.toString()}`);
}
- We create a new 
URLSeachParamsinterface and pass in the old one. This way we don't lose any search params. The newURLSeachParamsis not readonly. - Note: pass it like this: 
new URLSearchParams(searchParams.toString()). Above example is incorrect. - Next, we write a new sortOrder value using the 
.setmethod. This will also url encode both the key and the value. - Finally, we need to go from a 
URLSearchParamsinterface 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, useSearchParamsand useRouter.
We will do this in the next chapters.
If you want to support my writing, you can donate with paypal.

    
Top comments (0)