Add use-url-state Hook to Sync State with URL Query Params #6843
Replies: 3 comments
-
Everything that is related to url must be handled by a router package, it is not planned to introduce such hooks as router implementations are very different depending on the package. |
Beta Was this translation helpful? Give feedback.
-
Thank you for your feedback. I understand the concern about router packages varying across different frameworks, and the desire to keep the library unopinionated regarding routing solutions. However, I believe it's possible to implement a useUrlState hook in a framework-agnostic way by leveraging the native browser history API. This approach avoids dependencies on any specific router implementation while still providing the benefits of synchronizing state with URL query parameters. Here's how the useUrlState hook can be implemented using native browser APIs: import { useCallback, useEffect, useRef, useState } from "react";
const getUrlParams = () => {
const searchParams = new URLSearchParams(window.location.search);
const params: Record<string, any> = {};
searchParams.forEach((value, key) => {
try {
// Decode the parameter value and parse it from JSON
params[key] = JSON.parse(decodeURIComponent(value));
} catch {
// If parsing fails, assign the decoded value directly
params[key] = decodeURIComponent(value);
}
});
return params;
};
const useUrlState = <T extends Record<string, any>>(initialState: T) => {
const [state, setState] = useState<T>(() => {
// On initial render, merge initial state with URL parameters
const urlParams = getUrlParams();
return { ...initialState, ...urlParams };
});
// Ref to track if the state update is from a popstate event
const isPopState = useRef(false);
useEffect(() => {
const handlePopState = () => {
isPopState.current = true; // Set flag to prevent URL update in the next effect
const urlParams = getUrlParams();
// Update state with parameters from the URL when navigating back/forward
setState((prevState) => ({ ...prevState, ...urlParams }));
};
// Listen for browser navigation events (back/forward)
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);
useEffect(() => {
if (isPopState.current) {
// Skip updating the URL if the state change originated from a popstate event
isPopState.current = false;
return;
}
const searchParams = new URLSearchParams();
Object.entries(state).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
// Stringify and encode each state value before adding it to the URL
searchParams.set(key, encodeURIComponent(JSON.stringify(value)));
}
});
// Construct the new URL with updated query parameters
const newUrl = `${window.location.pathname}?${searchParams.toString()}${window.location.hash}`;
// Update the browser's history without reloading the page
window.history.pushState(null, "", newUrl);
}, [state]);
const updateState = useCallback((updater: (current: T) => T) => {
// Update the state using the provided updater function
setState((current) => updater(current));
}, []);
return [state, updateState] as const;
};
export default useUrlState; Key Points:
Advantages of Using Native Browser APIs:
Addressing Potential Concerns:
Next Steps:I welcome any feedback and are open to integrating this functionality in a way that aligns with your library's vision and standards. I'm eager to discuss how this hook can add value to your project. |
Beta Was this translation helpful? Give feedback.
-
Wrote a similar hook for react-router and next.js https://github.com/asmyshlyaev177/state-in-url , very simple and well tested, check it out. import React from 'react';
//import { useUrlState } from 'state-in-url/next';
import { useUrlState } from 'state-in-url/react-router';
const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: {id: string; value: {text: string; time: Date } }[];
};
export const useFormState = () => {
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form);
// first navigation will push new history entry
// all following will just replace that entry
// this way will have history with only 2 entries - ['/url', '/url?key=param']
const replace = React.useRef(false);
const setUrl = React.useCallback((
state: Parameters<typeof setUrlBase>[0],
opts?: Parameters<typeof setUrlBase>[1]
) => {
setUrlBase(state, { replace: replace.current, ...opts });
replace.current = true;
}, [setUrlBase]);
return { urlState, setUrl, resetUrl: reset };
}; |
Beta Was this translation helpful? Give feedback.
-
One of the common needs in modern web applications is the ability to persist state via URL query parameters, especially for use cases like filters, configurations, or even dynamic content. In a scenario where users might want to share a particular view, configuration, or selection, having a hook like
useUrlState
to manage URL state seamlessly can provide a powerful and user-friendly experience.Why useUrlState is valuable:
The addition of a
useUrlState
hook would offer the following advantages:Proposed API:
This API solves the issue of multiple hook instances overwriting each other by combining all state into one object. The state is fully type-safe, allowing developers to define the structure of urlState. This ensures the state is consistent and avoids conflicts when multiple query parameters need to be managed.
The hook would:
Example Usage:
By initializing the hook with a single object containing all the state variables, this approach prevents overwriting instances of useUrlState while maintaining full type safety. Each key-value pair in the object corresponds to a query parameter, allowing easy management and synchronization between the URL and component state.
Beta Was this translation helpful? Give feedback.
All reactions