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:
- Read current query parameters: Retrieve all existing query parameters from the URL as an object.
- Update query parameters: Provide a function to update one or more query parameters, persisting the changes to the URL without a full page reload.
- Type safety: Ensure that the query parameters are handled in a type-safe manner, allowing for explicit typing of expected parameters.
- 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.
setQueryParamsshould 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.pushStateorhistory.replaceStateAPI to update the URL. - The hook should listen for
popstateevents 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):
queryParamsobject:{ page: '2', sort: 'asc' }- On clicking "Next Page": The URL updates to
/dashboard?page=3&sort=asc, and the component re-renders withqueryParams.pageas'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):
queryParamsobject:{ 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 updatedqueryParams.
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):
queryParamsobject:{}(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
setQueryParamsfunction should usehistory.pushStateto add new entries to the history andhistory.replaceStateto modify the current entry. You can choose one or allow a configuration option. For simplicity,pushStateis 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
undefinedfor their values.
Notes
- Consider how you will parse the query string from
window.location.search. - Think about how to serialize the
queryParamsobject back into a query string for the URL. - The
popstateevent 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
Tshould 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.