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:
- Read existing search parameters: Retrieve the current values of search parameters from the URL.
- 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]. searchParamsshould be an object representing the current search parameters (e.g.,{ page: '1', query: 'react' }).setSearchParamsshould 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
setSearchParamsis called, the browser's URL should be updated usinghistory.pushStateorhistory.replaceState(your choice, butpushStateis generally preferred for navigation). - The hook should listen for URL changes (e.g., browser back/forward buttons) and update the
searchParamsaccordingly. - 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:
searchParamswill 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.
searchParamsis 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:
searchParamswill 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.
searchParamsis 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:
searchParamswill 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.
searchParamsis 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
URLSearchParamsAPI available in modern browsers for parsing and manipulating search parameters. - Think about how to handle updates to the URL.
history.pushStatewill add a new entry to the browser's history, whilehistory.replaceStatewill replace the current entry. For a typical search parameter update,pushStateis often more appropriate. - To make your hook resilient, you'll need to set up an event listener for
popstateto 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.