How and why you should store React UI state in the URL

Deep linking in React, as simple as useState

Have you ever used a complex web app with many features, modal windows, or side panels? Where you get to the perfect state with just the right information on the screen after a few clicks through different screens, but then you accidentally close the tab? (Or Windows decides to update?)

It would be great if there were a way to return to this state without going through the same tedious process. Or be able to share that state so a teammate can work on the same thing you are.

This problem could be solved with deep linking, which is used today in mobile apps to open the app to a specific page or UI state. But why does this not exist in many web apps?

Bring back deep linking on the web

The emergence of single-page applications (SPAs) has allowed us to craft new user experiences that are instantly interactive on the web. By doing more on the client side using JavaScript, we can respond to user events immediately, from opening custom dialog windows to live text editors like Google Docs.

Traditional server-rendered websites send a request to get a new HTML page every single time. An excellent example is Google, which sends a request to its servers with the user’s search query in the URL: https://www.google.com/search?q=your+query+here. What’s great about this model is that if I filter by results from the past week, I can share the same search query by simply sharing the URL: https://www.google.com/search?q=react+js&tbs=qdr:w. And this paradigm is entirely natural for web users—sharing links has been part of the world wide web ever since it was invented!

When SPAs came along, we didn’t need to store this data in the URL since we no longer need to make a server request to change what is displayed on the screen (hence single-page). But this made it easy to lose a unique experience of the web, the shareable link.

Desktop and mobile apps never really had a standardized way to link to specific parts of the app, and modern implementations of deep linking rely on URLs on the web. So when we build web apps that function more like native apps, why would we throw away the deep linking functionality of URLs that we’ve had for decades?

Dead-simple deep linking

When building a web app that has multiple pages, the minimum you should do is change the URL when a different page is displayed, such as /login and /home. In the React ecosystem, React Router is perfect for client-side routing like this, and Next.js is an excellent fully-featured React framework that also supports server-side rendering.

But I’m talking about deep linking, right down to the UI state after a few clicks and keyboard inputs. This is a killer feature for productivity-focused web apps, as it allows users to return right to the exact spot they were at even after closing the app or sharing it with someone else so they can start work without any friction.

Screen recording of a modal window being opened, causing the URL to update to add `#modal="webhooks"`, which is the internal state that triggers the modal to open.

Notice how the URL updates to add `#modal="webhooks"` as the modal opens.

You could use npm packages like query-string and write a basic React Hook to sync URL query parameters to your state, and there are plenty of tutorials for this, but there’s a more straightforward solution.

While exploring modern state management libraries for React for an architecture rewrite of our React app Rowy, I came across Jotai, a tiny atom-based state library inspired by the React team’s Recoil library.

The main benefit of this model is that state atoms are declared independent from the component hierarchy and can be manipulated from anywhere in the app. This solves the issue with React Context causing unnecessary re-renders, which I previously worked around with useRef. You can read more about the atomic state concept in Jotai’s docs and a more technical version in Recoil’s.

The code

Jotai has a type of atom called atomWithHash, which syncs the state atom to the URL hash.

Suppose we want a modal’s open state stored in the URL. Let’s start by creating an atom:

Then in the modal component itself, we can use this atom just like useState:

And here’s how it looks:

And that’s it! It’s that simple.

What’s fantastic about Jotai’s atomWithHash is that it can store any data that useState can, and it automatically stringifies objects to be stored in the URL. So I can store a more complex state in the URL, making it sharable.

In Rowy, we used this technique to implement a UI for cloud logs. We’re building an open-source platform that makes backend development easier and eliminates friction for common workflows. So, reducing friction for sharing logs was perfect for us. You can see this in action on our demo, where I can link you to a specific deploy log: https://demo.rowy.io/table/roadmap#modal="cloudLogs"&cloudLogFilters={"type"%3A"build"%2C"timeRange"%3A{"type"%3A"days"%2C"value"%3A7}%2C"buildLogExpanded"%3A1}

Decoding the URL component reveals the exact state used in React:

A side effect of atomWithHash is that it pushes the state to the browser history by default, so the user can click the back and forward buttons to go between UI states.

This behavior is optional and can be disabled using the replaceState option:

Thanks for reading! I hope this has convinced you to expose more of your UI state in the URL, making it easily shareable and reducing friction for your users—especially since it’s effortless to implement.

You can follow me on Twitter @nots_dney for more articles and Tweet threads about front-end engineering.

GitHub logo rowyio / rowy

Open-source Airtable-like experience for your database (Firestore) with GCP's scalability. Build any automation or cloud functions for your product. ⚡️✨

Rowy

Modern Backend Stack

Build prototypes that scale on Google Cloud Platform in minutes

Manage Firestore data in a spreadsheet-like UI, write Cloud Functions effortlessly in the browser, and connect to your favorite third party platforms such as SendGrid, Twilio, Algolia, Slack and more

Live Demo

💥 Check out the live demo of Rowy 💥

Quick Deploy

Set up Rowy on your Google Cloud Platform project with this one-click deploy button.

Run on Google Cloud

https://deploy.cloud.run/?git_repo=https://github.com/rowyio/rowyRun.git

Documentation

You can find the full documentation with how-to guides here.

Features

Powerful spreadsheet interface for Firestore

  • CRUD operations
  • Sort and filter by row values
  • Lock, Freeze, Resize, Hide and Rename columns
  • Multiple views for the same collection
  • Bulk import or export data - csv, json

Automate with cloud functions and ready made extensions

  • Effortlessly build cloud functions on field level triggers right in the browser
    • Use any NPM…

3