DEV Community

Cover image for How and why you should store React UI state in the URL
Sidney Alcantara
Sidney Alcantara

Posted on • Updated on • Originally published at

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?

⏭ Click here to skip to the solution and code snippets.

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: 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: And this paradigm is entirely natural for web users—sharing links has been part of the world wide web ever since it was invented!

Annotated screenshot of a Google search page. The search term input is highlighted and an arrow points to the corresponding part in the URL that stores the search term. The results are filtered to only show those from the past week, and another arrow points to the corresponding part in the URL that stores this data.

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:

Screen recording of a modal being opened, causing the URL to update to reflect the UI state, with #modalOpen=true being appended. When the modal is closed, it is replaced with modalOpen=false.

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:"cloudLogs"&cloudLogFilters={"type"%3A"build"%2C"timeRange"%3A{"type"%3A"days"%2C"value"%3A7}%2C"buildLogExpanded"%3A1}

Screen recording of the deep link opening the Rowy demo web app to the cloud logs modal being open.

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.

Screen recording of the user clicking the back button in the browser repeatedly, causing the UI state to change with modals being opened and closed.

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

Low-code backend platform. Manage database on spreadsheet-like UI and build cloud functions workflows in JS/TS, all in your browser.

Airtable-like UI for managing database Build any automation, with or without code

Connect to your database and create Cloud Functions in low-code - without leaving your browser.
Focus on building your apps Low-code for Firebase and Google Cloud

Live Demo 🛝

💥 Explore Rowy on live demo playground 💥



Powerful spreadsheet interface for Firestore

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

Automate with cloud functions and ready made extensions

  • Build cloud functions workflows on field level data changes
    • Use any NPM modules or APIs
  • Connect to your favourite tool with pre-built code blocks or create your own
    • SendGrid, Algolia, Twilio, Bigquery and more

Rich and flexible data fields

Top comments (9)

valeriavg profile image

Storing current open modal name in a hash is a great suggestion!
However, be very careful not to store sensitive data there as URLs are not encrypted, they are passed as is even through a secure connection.

fjones profile image

Common misconception: The URL is actually encrypted, as it is part of the TCP payload and SSL/TLS applies at that crucial lower level. What isn't encrypted is the destination address (at IP-level), which can identify the target webserver.

However that's not to say it's a good idea to add sensitive data to the URL, since the URL is indeed publicly shown in the browser (think screenshares!) and the subject at hand is sharing URLs. 😉

valeriavg profile image

I stand corrected, thank you for pointing it out!

notsidney profile image
Sidney Alcantara • Edited

This is an excellent point. Using this definitely requires the dev (and maybe even the UX designer) to consider what should belong/be sharable using the URL, especially since it’s unencrypted shareable as an unencrypted URL.

filipemerker profile image
Filipe Merker • Edited

In my experience, the location/history api is as unreliable as it gets. I'll list just the three main reasons why my team has a very strict policy to updating the URL:

  1. It is mutable by design, so forget about any "functional approach" of doing it.

  2. Listening for location events is unreliable. Using events such as back and forward navigation to update state is bad design and usually causes race conditions. React Router is horrible at that 😅

  3. Unless you have some very well engineered update queue, having multiple components reading and updating the url will introduce bugs unbelievably difficult to debug.

IMHO, the url should just be composed and updated through a single access point and it should reflect your redux store. If the store changes, the URL changes as a side-effect.

So your Store is the single source of truth and not the URL.

fernando140alto profile image

Awesome, I use query-string for params sync which is not very straight forward. Now I'm wondering if I could replace it with atom. Nice!

calag4n profile image

Really interesting . I'll definitely think about where in app it would be relevant to implement this.
Thanks for sharing.

ceoshikhar profile image

This is off topic but any idea what font is used in that one line of code in the banner image?

notsidney profile image
Sidney Alcantara

It’s JetBrains Mono!