Hone logo
Hone
Problems

React useQueryParams Hook: Mastering URL Query Parameters

This challenge involves creating a custom React hook, useQueryParams, that simplifies the process of reading and updating URL query parameters in a type-safe manner. Managing query parameters is crucial for features like pagination, filtering, and maintaining application state across page reloads, and a well-designed hook can significantly improve developer experience.

Problem Description

Your task is to implement a useQueryParams hook in TypeScript that provides a React component with a clean interface for interacting with the browser's URL query parameters. This hook should allow components to:

  1. Read current query parameters: Retrieve all existing query parameters from the URL as an object.
  2. Update query parameters: Provide a function to update one or more query parameters, persisting the changes to the URL without a full page reload.
  3. Type safety: Ensure that the query parameters are handled in a type-safe manner, allowing for explicit typing of expected parameters.
  4. React to changes: Automatically update the component when query parameters in the URL change (e.g., via browser back/forward buttons).

Key Requirements:

  • The hook should return an object containing:
    • queryParams: An object representing the current query parameters.
    • setQueryParams: A function to update the query parameters.
  • setQueryParams should accept either an object of new/updated parameters or a callback function that receives the current parameters and returns the updated ones.
  • The hook should utilize the browser's history.pushState or history.replaceState API to update the URL.
  • The hook should listen for popstate events to react to changes initiated by the browser's navigation.
  • The hook should be generic, allowing users to define the expected shape of their query parameters.

Expected Behavior:

When the useQueryParams hook is used in a component, accessing queryParams should yield an object reflecting the current URL's query string. Calling setQueryParams should modify the URL and trigger a re-render of the component.

Edge Cases to Consider:

  • Empty query string: The hook should gracefully handle URLs with no query parameters.
  • URL decoding/encoding: Query parameters should be correctly decoded when read and encoded when set.
  • Multiple values for the same key: While typically query parameters are treated as single values, consider how your hook might handle or simplify this if needed (for this challenge, assume single values per key is sufficient).
  • Initial render: Ensure the hook correctly parses parameters on the initial mount.

Examples

Example 1: Basic Usage

// Assuming your app is at '/dashboard?page=2&sort=asc'
import React from 'react';
import { useQueryParams } from './useQueryParams'; // Assuming your hook is in this file

interface DashboardQueryParams {
  page?: string;
  sort?: 'asc' | 'desc';
}

function Dashboard() {
  const { queryParams, setQueryParams } = useQueryParams<DashboardQueryParams>();

  const nextPage = () => {
    setQueryParams({ page: String(Number(queryParams.page || 0) + 1) });
  };

  return (
    <div>
      <p>Current Page: {queryParams.page || '1'}</p>
      <p>Sort Order: {queryParams.sort || 'none'}</p>
      <button onClick={nextPage}>Next Page</button>
    </div>
  );
}

Input (URL): /dashboard?page=2&sort=asc

Output (after component mounts):

  • queryParams object: { page: '2', sort: 'asc' }
  • On clicking "Next Page": The URL updates to /dashboard?page=3&sort=asc, and the component re-renders with queryParams.page as '3'.

Explanation: The hook parses the URL, making page and sort available. The nextPage function updates the page parameter and the URL, triggering a re-render.

Example 2: Setting Multiple Parameters and Using Callback

// Assuming your app is at '/products?category=electronics'
import React from 'react';
import { useQueryParams } from './useQueryParams';

interface ProductQueryParams {
  category?: string;
  search?: string;
  filterId?: string;
}

function ProductList() {
  const { queryParams, setQueryParams } = useQueryParams<ProductQueryParams>();

  const applyFilter = () => {
    setQueryParams(prev => ({
      ...prev,
      search: 'new_search_term',
      filterId: 'filter_123',
    }));
  };

  return (
    <div>
      <p>Category: {queryParams.category || 'All'}</p>
      <p>Search: {queryParams.search || 'None'}</p>
      <p>Filter ID: {queryParams.filterId || 'None'}</p>
      <button onClick={applyFilter}>Apply Filter</button>
    </div>
  );
}

Input (URL): /products?category=electronics

Output (after component mounts):

  • queryParams object: { category: 'electronics' }
  • On clicking "Apply Filter": The URL updates to /products?category=electronics&search=new_search_term&filterId=filter_123, and the component re-renders with updated queryParams.

Explanation: The setQueryParams callback allows updating parameters based on their previous state, ensuring that existing parameters (category in this case) are preserved.

Example 3: Empty Query String and Type Safety

// Assuming your app is at '/'
import React from 'react';
import { useQueryParams } from './useQueryParams';

interface HomeQueryParams {
  userId?: string;
  showWelcome?: 'true' | 'false';
}

function HomePage() {
  const { queryParams } = useQueryParams<HomeQueryParams>();

  return (
    <div>
      <p>User ID: {queryParams.userId || 'Not provided'}</p>
      <p>Show Welcome: {queryParams.showWelcome === 'true' ? 'Yes' : 'No'}</p>
    </div>
  );
}

Input (URL): /

Output (after component mounts):

  • queryParams object: {} (an empty object)

Explanation: The hook correctly handles URLs without query parameters, returning an empty object. The generic type HomeQueryParams ensures that queryParams.userId is treated as a string | undefined and queryParams.showWelcome is constrained to 'true' | 'false' | undefined.

Constraints

  • The hook must be implemented in TypeScript.
  • The hook should not rely on any external libraries for parsing or manipulating URL query parameters. Standard browser APIs are expected.
  • The hook should be efficient and avoid unnecessary re-renders.
  • The setQueryParams function should use history.pushState to add new entries to the history and history.replaceState to modify the current entry. You can choose one or allow a configuration option. For simplicity, pushState is often preferred for user-initiated changes that navigate to a new state.
  • The hook should handle cases where query parameters are not present by returning undefined for their values.

Notes

  • Consider how you will parse the query string from window.location.search.
  • Think about how to serialize the queryParams object back into a query string for the URL.
  • The popstate event fires when the active history entry changes. This is key to updating your hook's state when users navigate using the browser's back/forward buttons.
  • Your generic type T should represent the expected shape of your query parameters. This allows users to define the types of their parameters, promoting type safety. For example, if a parameter is expected to be a number, you might want to handle its parsing from a string. For this challenge, string values are acceptable, but think about how you might extend it.
Loading editor...
typescript