Building a Multi-Page SPA with React RouterEntry 10 of 20

Query Params with useSearchParams

useSearchParams() reads and writes the URL's query string (?role=admin), and re-renders the component when it changes.

Notes

  • Returns [searchParams, setSearchParams], like useState for the URL.
  • searchParams is a standard URLSearchParams: .get('role'), .getAll('tag'), .has('page').
  • setSearchParams({ role: 'admin' }) updates the URL with client-side navigation (no reload).
  • In loaders, read query params from the request: new URL(request.url).searchParams.
  • Query params suit shareable UI state: filters, sorting, pagination, search terms.

What / Why / When

What

URL-backed state.

Why

Filters survive refresh and can be shared or bookmarked.

When

Any state the user expects to keep when copying the URL.

Code compare

React
import { useSearchParams } from "react-router";​const ROLES = ["admin", "manager", "user"] as const;​export default function UsersPage() {  const [searchParams, setSearchParams] = useSearchParams();  const role = searchParams.get("role") ?? "all";​  return (    <>      <p>Filter: {role}</p>      {ROLES.map((r) => (        <button key={r} onClick={() => setSearchParams({ role: r })}>          {r}        </button>      ))}    </>  );}// Clicking "admin" → /users?role=admin, component re-renders
React
// Reading query params in a loaderexport async function loader({ request }: LoaderFunctionArgs) {  const role = new URL(request.url).searchParams.get("role");  return getUsers(role);}

With withComponentInputBinding(), query params also bind to inputs.

Angular
@Component({  selector: "app-users",  template: `    <p>Filter: {{ role() ?? "all" }}</p>    @for (r of roles; track r) {      <button (click)="setRole(r)">{{ r }}</button>    }  `,})export class UsersComponent {  private router = inject(Router);  private route = inject(ActivatedRoute);​  role = input<string>(); // bound from ?role=  protected roles = ["admin", "manager", "user"];​  setRole(role: string) {    this.router.navigate([], {      relativeTo: this.route,      queryParams: { role },      queryParamsHandling: "merge",    });  }}

Differences and gotchas

  • setSearchParams({ role }) replaces all query params. Use the function form to keep others: setSearchParams((prev) => { prev.set('role', r); return prev; }).
  • Angular's queryParamsHandling: 'merge' keeps existing params.

Quick reference

TaskReactAngular
ReadsearchParams.get('role')role = input<string>() / ActivatedRoute.queryParamMap
WritesetSearchParams({ role })router.navigate([], { queryParams: { role } })
Keep othersFunction form of setSearchParamsqueryParamsHandling: 'merge'
In a loader/resolvernew URL(request.url).searchParamsroute.queryParamMap