Before you useState
Tags: react, webdev
Published: 2021-06-11 08:14:00

---
title: Before you useState
published: true
description: Imagine, you are on this amazing website that has lots of filters and you just nailed down a combination that yields perfect results and you would like to share it with your partner. You hit the "Share" button and send it over. Then the other person opens it only to see... the default page instead of filtered results. *Everyone hates it!* Yet we, Frontend developers, are the ones who screwed up by treating our applications' state as something that only belongs to the application and burring it in `useState` calls or in the Redux store. Luckily, we are the ones who can fix it!
tags: React, WebDev
cover_image: https://images.unsplash.com/photo-1494961104209-3c223057bd26?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&w=3600
canonical_url: https://www.component-driven.dev/articles/before-you-use-state
---
Imagine, you are on this amazing website that has lots of filters and you just nailed down a combination that yields perfect results and you would like to share it with your partner. You hit the "Share" button and send it over. Then the other person opens it only to see... the default page instead of filtered results. *Everyone hates it!* Yet we, Frontend developers, are the ones who screwed up by treating our applications' state as something that only belongs to the application and burring it in `useState` calls or in the Redux store. Luckily, we are the ones who can fix it!

## Simply `useState`

While implementing a filter for a list of items based on a user input, most of you would probably do something like without even thinking twice (at least I did so many times!):

```jsx
import * as React from "react";

const options = ["Apple", "Banana", "Cherry"];

export default function App() {
  const [query, setQuery] = React.useState(""); // set the initial state
  const results = options.filter((option) => option.includes(query)); // filter results using the state
  const handleChange = (e) => setQuery(e.target.value); // update the state based on the new value
  return (
    <>
      <input type="search" value={query} onChange={handleChange} />
      <ul>
        {results.map((result) => (
          <li key={result}>{result}</li>
        ))}
      </ul>
    </>
  );
}
```

[Open on CodeSandbox](https://codesandbox.io/s/before-usestate-1-i7sys)

This does the job and gives you the user interface we wanted but *the state is now not accessible by the user*. You can't share the URL with another person and they won't be able to see what you saw. 

## Sharing the state with the user

People start using your UI and get annoyed by the fact links aren't sharable. So you decide to implement this feature on top of the existing code base. It will probably will look like this:

```jsx
import * as React from "react";

const options = ["Apple", "Banana", "Cherry"];

export default function App() {
  const queryParams = new URLSearchParams(window.location.search); // get query string from the location
  const [query, setQuery] = React.useState(queryParams.get("query") ?? ""); // set the initial state to it
  const results = options.filter((option) => option.includes(query)); // filter results using the state
  const handleChange = (e) => setQuery(e.target.value); // update the state based on the new value
  return (
    <>
      <input type="search" value={query} onChange={handleChange} />
      <ul>
        {results.map((result) => (
          <li key={result}>{result}</li>
        ))}
      </ul>
    </>
  );
}
```

[Open on CodeSandbox](https://codesandbox.io/s/before-usestate-2-k7non)

Better! We can parse the URL and set the application state to reflect it but it actually doesn't update the URL as you change the input's value while typing. Let's fix it!

## Reacting to the user input

```jsx
import * as React from "react";

const options = ["Apple", "Banana", "Cherry"];

export default function App() {
  const queryParams = new URLSearchParams(window.location.search); // get query string from the location
  const [query, setQuery] = React.useState(queryParams.get("query") ?? ""); // set the initial state to it
  const results = options.filter((option) => option.includes(query)); // filter results using the state
  const handleChange = (e) => setQuery(e.target.value); // update the state based on the new value

  // Now that we have the new state, let's sync it with location
  React.useEffect(() => {
    // Calculate new URL based on the state
    queryParams.set("query", query);
    const newURL = "?" + queryParams.toString();
    // Update the URL in the location
    window.history.pushState({}, undefined, newURL);
  }, [queryParams, query]);

  return (
    <>
      <input type="search" value={query} onChange={handleChange} />
      <ul>
        {results.map((result) => (
          <li key={result}>{result}</li>
        ))}
      </ul>
    </>
  );
}
```

[Open on CodeSandbox](https://codesandbox.io/s/before-usestate-3-jv118)

Phew! It works but look at that code! But wait, maybe *we could do better*? 

## Use URL instead of `useState`

Think about what we're trying to do:

1. Derive the state from the URL
2. Set it as a default value of the `useState` call
3. Update the React's state in the `onChange` event handler using the setter function
4. Derive the new URL in the `useEffect` 
5. Set the `location` to the new URL so it's in sync with the UI

What if we would treat the URL as *our state container?* This way we could bypass the local state completely. Here is the updated algorithm.

1. Derive the state from the URL
2. Update the `location` in `onChange` callback to keep it in sync with the UI

```jsx
import * as React from "react";
import { navigate } from "@reach/router";

const options = ["Apple", "Banana", "Cherry"];

export default function App() {
  const queryParams = new URLSearchParams(window.location.search); // get query string from the location
  const query = queryParams.get("query") ?? ""; // get the query value
  const results = options.filter((option) => option.includes(query)); // filter results using the state
  const handleChange = (e) => {
    queryParams.set("query", e.target.value); // update the state based on the new value
    // Calculate new URL based on the state
    const newURL = "?" + queryParams.toString();
    // Update the URL in the location
    navigate(newURL);
  };

  return (
    <>
      <input type="search" value={query} onChange={handleChange} />
      <ul>
        {results.map((result) => (
          <li key={result}>{result}</li>
        ))}
      </ul>
    </>
  );
}
```

[Open on CodeSandbox](https://codesandbox.io/s/before-usestate-4-zu9ph)

Much simpler! But most importantly, not only we improved our code, but also we made our application more accessible: each filter result is now sharable using a simple link!

## Final result

https://codesandbox.io/embed/before-usestate-3-jv118?autoresize=1&codemirror=1&fontsize=16&module=%2Fsrc%2FApp.js&theme=dark

## Gotchas

There are a few gotchas I've discovered while implementing my state this way:

1. Although browsers' [native History API](https://developer.mozilla.org/en-US/docs/Web/API/History) gives a simple way of modifying the state [using `pushState()`](https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API), it won't trigger a re-render of the React app. That's why in my last example I use [@reach/router](https://reach.tech/router/). Since most popular React frameworks like [Next.js](https://nextjs.org/docs/api-reference/next/router#userouter) or [Gatsby](https://www.gatsbyjs.com/docs/reach-router-and-gatsby/) already have a router as a dependency, I don't consider this a problem.
2. Second problem is: updating the `location` via a router will scroll the page to the top by default in most browsers. Most of the time it shouldn't be a problem since this is desired to see top results. Depending on the layout of the page and device's resolution, though, it can be annoying. In this case there are ways of disabling it:

### Prevent scroll in Next.js

```javascript
const handleChange = (event) => {
  router.replace(
    {
      query: {
        q: event.target.value,
      },
    },
    undefined,
    {
      scroll: false,
      shallow: true,
    }
  )
}
```

### Prevent scroll in Gatsby

```javascript
export const shouldUpdateScroll = ({ routerProps: { location } }) => {
 return location.search === "" // Scroll to top only when we're not searching
}
```

3. Last but not least: changing the URL will re-render the whole application which can cause some performance problems for bigger and more complex applications. In this case, synchronizing the URL with the state is the only solution for now. With the concurrent mode, though, it might become much less of a problem and that's how I think frameworks should deal with complexity: developers should write the most idiomatic code and let frameworks do optimizations under the hood.

## Conclusion

Next time, before you `useState`, stop for a second and think about your state. Most applications' state deserves to be shared with your users, so treat it as something public and put it into URL from the beginning. It will make your app more accessible and easy to use and it will make the code much simpler. A win-win situation! 

P.S.: Even if you can't implement the final solution for some reason now, I still encourage you to think about the URL as an additional user interface to interact with the website and expose as much as possible to allow such interactions.