TLDR;
- embrace the URL and put part of your state into the query string of the URL (?simple=true)
- automatically support bookmarking, back, forward & refresh actions in the browser by doing so
-
useQueryState
fromuse-location-state
helps to put state into the query string -
useQueryState(itemName, defaultValue)
works likeuseState()
, but persists the state in the query string
Intro: Benefits of good URLs
URLs are a fundamental technology of the web since their definition in the mid-90s. Being able to deep-link from one document to another was such a great idea that mobile operating systems even copied the principle for their app platforms. Apple calls them Universal Links, and Google calls them App Links. While native apps only surface a URL from time to time via the share sheet, URLs are always visible and accessible in the browser. So websites should maintain useful URLs.
Good URLs enable users to keep a reference to web applications in a specific state or to share them with other people. While the path of the URL usually defines the page the user is on, eg. a search results page (/search), the query string is often used to encode a custom state of that page, eg. a search query for "shoes" with a set of filters for colors and size (/search?q=shoes&color=blue&color=black&size=44). A user could now bookmark this URL to come back later or share it with a friend or click on one of the products to check it out in detail, and if they want to return to the results page, they could just use the back functionality to go back to the same search results and select another product.
Challenges: Maintaining the URL is hard... so we rarely do it
While the benefits of good URLs are apparent to most people, many modern SPAs build with frameworks like React still struggle to provide good URLs, because updating the URL and query string is harder than updating a local state or the redux store. I've been guilty of this myself, and I think the leading cause for this was the lack of an easy-to-use API.
An important design goal was to enable independent components on the page to take advantage of the query string and history.state
, without them needing to know about each other. So a component only concerned about a specific part of the state, for example, the size filter parameter (?...&size=44), could read and update that state without having to deal with any other information stored in the query string.
Introducing: useQueryState()
I went ahead to create a simple, yet powerful hook for React that works like useState()
, but persists state in the query string of the URL. All that you need to use it is to choose a parameter name and pass a default value. The API looks like this:
const [currentValue, updateValueFunction] = useQueryState(paramName, defaultValue)
The default value will be returned as the current value, as long as the value was not updated and the query string does not include a value for that parameter yet. In case this syntax (array destructuring) is new to you, I recommend reading about it in the React Docs.
function Search() {
const [queryString, setQueryString] = useQueryState("queryString", "");
return (
<label>
What are you looking for?
<input
value={queryString}
onChange={e => setQueryString(e.target.value)}
placeholder="Shoes, Sunglasses, ..."
/>
</label>
);
}
When a user now types a search term "shoes" into the text field, the query string of the URL will be updated to /?queryString=shoes
. And you can reload, or go another page and return and the state will be restored correctly.
You can of course also use multiple useQueryState()
hooks in a single component (or in separate components). Each useQueryState()
automatically merges its updates with the currently encoded state in the query string.
const [queryString, setQueryString] = useQueryState("queryString", "");
const [colors, setColors] = useQueryState("colors", []);
const toggleColor = e => {
const color = e.target.value;
setColors(
colors.includes(color)
? colors.filter(t => t !== color)
: [...colors, color]
);
};
return (
<form>
...
<Color
name="red"
active={colors.includes("red")}
onChange={toggleColor}
/>
<Color
name="blue"
active={colors.includes("blue")}
onChange={toggleColor}
/>
...
</form>
)
useQueryState()
currently supports following value types: string | number | boolean | Date | string[]
.
The query string is a global state, so choose the parameter names wisely to prevent accidental clashes. But it is of course allowed to use the same parameter name on purpose when you want access to the same state in multiple places.
If not safe for the URL: useLocationState()
In some cases, you might not want to store the state in the query string of the URL, but still, want the user to be able to restore a previous state using the back/forward actions of the browser. To enable this useLocationState()
persists state in the history state instead.
The API works the same. You provide a name and a default value and get the current value, and the update function returned as a pair.
const [currentValue, updateValueFunction] = useLocationState(paramName, defaultValue)
For persisting complex or more sensitive state useLocationState()
is more suitable, for example, the state of a comment form. Also, state based on data that frequently changes is better suited to be stored in history.state
. This way, you can avoid offering URLs that only work for a short time.
Installation / Usage
You can install these hooks using yarn or npm:
yarn add use-location-state
Import the hooks where you want to use them:
import { useLocationState, useQueryState } from 'use-location-state'
Are you using react-router
or another popular router?
For the best experience install one of the router integrations.
yarn add react-router-use-location-state
And use these imports:
import { useLocationState, useQueryState } from 'react-router-use-location-state'
Thanks!
I hope you find this introduction and the library is useful! Happy to discuss enhancements and answer questions π
Top comments (0)