Storing state in the URL with React
Deep linking is a fundamental part of the web. It makes it possible to share links to pages with specific information. Take this link for instance: https://www.google.com/search?q=react. It contains a query parameter, that contains the key q, and value react. Upon entering the page, a google search page will appear with results for the given keyword.
You can also think of a UI that can contain several filters. After you have spent time setting up your desired filter, you might accidentally close the window. Then re-opening the site, you have to go trough the hassle of setting up the filter all over again 😬. This happens because the site doesn't save the filter anywhere. If the site did put the all filter settings in the URL, this wouldn't be a problem.
Try changing the inputs below, and look at the URL.
Now refresh the page. 😃
A tweet by Kent C. Doods highlights this type of issue, and propose that you should treat the url as the source of state, and ditching the local state in return.
If you find yourself synchronizing some local component state with the URL (for example, query parameters), you'd be better off treating the URL as the source of truth and ditching your local state in favor of getting the value from the URL (or your router) and updating the URL.
— Kent C. Dodds 💿 (@kentcdodds) January 13, 2021
I have found several hooks that makes it easy to implement this. Alibaba has a collection of React hooks, one called useUrlState which you start using directly here. I decided to use these as inspiration for a hook I made myself. It looks like this.
function useStateParams<T>(
initialState: T,
paramsName: string,
serialize: (state: T) => string,
deserialize: (state: string) => T
): [T, (state: T) => void] {
const history = useHistory();
const search = new URLSearchParams(history.location.search);
const existingValue = search.get(paramsName);
const [state, setState] = useState<T>(
existingValue ? deserialize(existingValue) : initialState
);
useEffect(() => {
// Updates state when user navigates backwards or forwards in browser history
if (existingValue && deserialize(existingValue) !== state) {
setState(deserialize(existingValue));
}
}, [existingValue]);
const onChange = (s: T) => {
setState(s);
const searchParams = new URLSearchParams(history.location.search);
searchParams.set(paramsName, serialize(s));
const pathname = history.location.pathname;
history.push({ pathname, search: searchParams.toString() });
};
return [state, onChange];
}
It has four parameters. The first is the initial state of the hook. The second parameter is the name of the query param that will be shown in the url. The third function is a serialization function, that will take the state and turn it into a url friendly string. The fourth de-serialization function, will take the string and transform it back to the state. In the fourth step, its a good practice to make sure that the value is of correct type, since you don't want the rest of you application to fail because of invalid query params. You can extend the above hook to have more complicated types of query parameters that fit your needs. The one above is designed using react-router. But it could be used with other routing libraries as well.
The slider and the checkbox can then be used in the following way.
const [bool, setBool] = useStateParams(
false,
'boolean',
(s) => (s ? 'true' : 'false'),
(s) => s === 'true'
);
const [slider, setSlider] = useStateParams(
10,
'slider',
(s) => s.toString(),
(s) => (Number(s) !== Number.NaN ? Number(s) : 10)
);