Hone logo
Hone
Problems

Replicate React Router's useSearchParams Hook

This challenge requires you to build a custom React hook, useSearchParams, that mimics the functionality of React Router's built-in hook. This hook will allow you to read and update URL search parameters within your React components, enabling dynamic filtering, pagination, and other URL-driven features.

Problem Description

Your task is to create a custom React hook named useSearchParams that provides a simple interface for interacting with the browser's URL search parameters. This hook should allow components to:

  1. Read existing search parameters: Retrieve the current values of search parameters from the URL.
  2. Update search parameters: Modify the search parameters in the URL, triggering a re-render of the component.

Key Requirements:

  • The hook should return a tuple: [searchParams, setSearchParams].
  • searchParams should be an object representing the current search parameters (e.g., { page: '1', query: 'react' }).
  • setSearchParams should be a function that accepts either:
    • An object of key-value pairs to update or add search parameters.
    • A string representing the new search query.
  • When setSearchParams is called, the browser's URL should be updated using history.pushState or history.replaceState (your choice, but pushState is generally preferred for navigation).
  • The hook should listen for URL changes (e.g., browser back/forward buttons) and update the searchParams accordingly.
  • The hook should be implemented in TypeScript.

Expected Behavior:

When useSearchParams is called within a functional component, it should provide access to the current search parameters and a function to modify them. Any changes made via setSearchParams should update the URL in the address bar and cause the component to re-render with the new searchParams.

Edge Cases to Consider:

  • No search parameters: The hook should gracefully handle URLs with no search parameters.
  • Empty parameter values: How should empty parameter values (e.g., ?key=&other=value) be represented?
  • URL encoding/decoding: Ensure proper handling of URL-encoded characters.
  • Initial render: The hook should correctly read parameters on the initial render.

Examples

Example 1:

import React from 'react';
import { useSearchParams } from './useSearchParams'; // Assuming your hook is in this file

function MyComponent() {
  const [searchParams, setSearchParams] = useSearchParams();

  const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSearchParams({ query: event.target.value });
  };

  return (
    <div>
      <input
        type="text"
        value={searchParams.query || ''}
        onChange={handleQueryChange}
        placeholder="Search..."
      />
      <p>Current Query: {searchParams.query || 'None'}</p>
    </div>
  );
}

// Initial URL: http://localhost:3000/?query=react
// After typing "hooks" in the input: http://localhost:3000/?query=hooks

Output (in the browser):

When the component mounts with the URL http://localhost:3000/?query=react:

  • searchParams will be { query: 'react' }.
  • The input will display "react".
  • The paragraph will display "Current Query: react".

When the user types "hooks" into the input:

  • setSearchParams({ query: 'hooks' }) is called.
  • The URL changes to http://localhost:3000/?query=hooks.
  • The component re-renders.
  • searchParams is now { query: 'hooks' }.
  • The input displays "hooks".
  • The paragraph displays "Current Query: hooks".

Example 2:

import React from 'react';
import { useSearchParams } from './useSearchParams';

function PaginationComponent() {
  const [searchParams, setSearchParams] = useSearchParams();

  const nextPage = () => {
    const currentPage = parseInt(searchParams.page || '1', 10);
    setSearchParams({ page: (currentPage + 1).toString() });
  };

  const prevPage = () => {
    const currentPage = parseInt(searchParams.page || '1', 10);
    if (currentPage > 1) {
      setSearchParams({ page: (currentPage - 1).toString() });
    }
  };

  return (
    <div>
      <p>Current Page: {searchParams.page || '1'}</p>
      <button onClick={prevPage}>Previous</button>
      <button onClick={nextPage}>Next</button>
    </div>
  );
}

// Initial URL: http://localhost:3000/items?page=2
// After clicking "Next": http://localhost:3000/items?page=3

Output (in the browser):

When the component mounts with the URL http://localhost:3000/items?page=2:

  • searchParams will be { page: '2' }.
  • The paragraph will display "Current Page: 2".

After clicking the "Next" button:

  • setSearchParams({ page: '3' }) is called.
  • The URL changes to http://localhost:3000/items?page=3.
  • The component re-renders.
  • searchParams is now { page: '3' }.
  • The paragraph displays "Current Page: 3".

Example 3 (Setting multiple parameters):

import React from 'react';
import { useSearchParams } from './useSearchParams';

function FilterComponent() {
  const [searchParams, setSearchParams] = useSearchParams();

  const toggleStatus = () => {
    const currentStatus = searchParams.status === 'active' ? 'inactive' : 'active';
    setSearchParams({ status: currentStatus, page: '1' }); // Also reset page when status changes
  };

  return (
    <div>
      <p>Current Status: {searchParams.status || 'All'}</p>
      <button onClick={toggleStatus}>Toggle Status</button>
    </div>
  );
}

// Initial URL: http://localhost:3000/products
// After clicking "Toggle Status" the first time: http://localhost:3000/products?status=active&page=1
// After clicking "Toggle Status" the second time: http://localhost:3000/products?status=inactive&page=1

Output (in the browser):

When the component mounts with the URL http://localhost:3000/products:

  • searchParams will be {}.
  • The paragraph displays "Current Status: All".

After clicking "Toggle Status" the first time:

  • setSearchParams({ status: 'active', page: '1' }) is called.
  • The URL changes to http://localhost:3000/products?status=active&page=1.
  • The component re-renders.
  • searchParams is now { status: 'active', page: '1' }.
  • The paragraph displays "Current Status: active".

Constraints

  • The hook must be implemented in TypeScript.
  • It should not rely on any external libraries for URL manipulation beyond native browser APIs.
  • The hook should be efficient and not cause unnecessary re-renders.

Notes

  • Consider using URLSearchParams API available in modern browsers for parsing and manipulating search parameters.
  • Think about how to handle updates to the URL. history.pushState will add a new entry to the browser's history, while history.replaceState will replace the current entry. For a typical search parameter update, pushState is often more appropriate.
  • To make your hook resilient, you'll need to set up an event listener for popstate to detect when the user navigates using the back/forward buttons.
  • Remember to clean up any event listeners when the component unmounts to prevent memory leaks.
Loading editor...
typescript