How to simplify your application state with native browser implementations.
What if I told you, you should not be storing all of your data in a React lifecycle? There are many tools using the built-in browser APIs and non-lifecycle methods in React that can be very powerful for the right use cases.
This is the second part in a series where I want to show you ways to simplify state in React without needing “global state” tools. Check out the previous part for tips on storing state in the React lifecycle
There are four mechanisms for storing and accessing data in a React application that we are going to focus on:
- Web Storage: Local Storage, Session Storage & Cookies
- URL & Router state
- React Refs: State outside of React lifecycle
- Browser Storage
And a Bonus at the end!
Leveraging native browser mechanisms like local storage, session storage, and cookies is an excellent option for persisting and sharing state because they work across all modern browsers, and you don’t need to install any new dependencies to use them.
Local storage persists across browser sessions for a given origin — “origin” is the combination of hostname, protocol, and port.
Because of its persistence across browser sessions, local storage is a great way to persist data across page renders when the data does not need to be stored in a database. It could be used for keeping track of JWT authentication tokens, whether a notification has been opened, etc.
It follows a key-value format and only supports string values. You can still store complex objects if you stringify the data using
JSON.stringify or some other way.
More info on local storage: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
Session storage is very similar to local storage — the main difference is it only persists for the same browser session. It persists across page refreshes for the same session but not across sessions (new tabs, windows). This method can be useful for less persistent storage, such as tracking metrics for a user session.
The API of session storage is almost identical to local storage:
More info on session storage
Cookies are data stored in text files on the client’s computer. Typically they are used to maintain stateful data between stateless HTTP calls. For example, CSRF tokens, login info, preferences, theme, language, etc. The server can set cookies using a
Set-Cookie instruction in HTTP responses.
The browser can directly read and write cookies using
document.cookie. It looks like a string but behaves like a getter/setter function:
Cookies are not as popular in the age of modern SPAs and Jamstack applications but they are battle tested and extremely useful in the right scenarios.
More info on cookies.
Note: To comply with GDPR you want to be careful about storing non-essential data in cookies and ask your users for consent first. Strictly necessary cookies required for your website to function are exempt — such as session ids, CSRF tokens, etc.
You can store a lot of information in the URL of your page. This is great when you want your users to be sharing URLs with others. The URL can know the exact state to render your application each time you open the same link. This is often why you see long URLs when you try to copy them into some applications.
- You can store URLSearchParams in a key-value format by appending a
?to the end of your URL.
- You can include multiple params by separating them using
- Depending on your application, you may need to encode and decode the params using
decodeURIComponent()to handle special characters.
query-stringis a useful npm package to help manage complexities with search parameters.
react-router-dom you can also pass state when navigating throughout your application. This is helpful when a page needs to know something about the previous page or you want to pre-populate it with fetched data – such as when navigating from a listing page to a detail view.
history or when using
Refs are better suited for getting access to a DOM element for direct manipulation like programmatic focus:
But you can also store data in a ref and have it live outside of the render lifecycle for a component. Meaning updates to it will not trigger re-renders.
Note: Before React, it was common to store some data in custom attributes directly on DOM elements — which is still possible today.
Refs are practical for mutable values that persist across renders like keeping track of click counts, performance metrics, or interval/timeout ids.
These are not specific to any framework and are may be more advanced for most typical use cases, but they offer very powerful mechanisms for managing large data problems on the client side. And a peek into a potential future of native browser API-based development.
Web storage is an excellent way to persist state outside of React using browser native capabilities, but it is limited in the amount of data that can be stored. The primary use case is for small amounts of session-related data.
key. It is available in all modern browsers.
More info on IndexedDB
CacheStorage is another native browser API for storing and retrieving network requests and responses. As the name implies, CacheStorage is an excellent choice for caching data fetched from your backend services. It uses service workers under the hood and is available in all modern browsers.
More info on CacheStorage
We’ve talked about mechanisms for storing data in the browser and in Part One we covered tips for storing data in the React lifecycle.
Both solutions are still a subset of the bigger picture. Often our applications rely on a Database to store persistent state across browser sessions. The Database is the source of truth — so why not store all of your state there?
Before SPAs and modern browsers this is how you stored state of your web applications. Any changes in the browser would be sent back to the server and persisted in memory or in the Database. This greatly simplified the client code making page loads quick and responsive for users.
However, it required the HTML to be generated for each change so applications quickly started to feel clunky when a lot of interactivity was required. This is the power of SPAs and modern web techniques — creating complex interactions in the browser performantly without regenerating the HTML.
You can still store states outside of the client in a Database today in conjunction with modern SPA patterns. In fact, this might greatly simplify some of your state and data needs provided you have a smooth API experience. There are many tools and patterns out there to help you shift some of the complexity of the client back to the server. Techniques like server-side rendering, or a Backend For Frontend (BFF) server.
GraphQL implementations are great for this type of use case because it provides your client a way to query and alter data using a query language without needing to create new REST APIs each time.
State management is a hard problem — but it is crucial to create smooth web applications that best serve your end users. Always be critical of how and where you are storing state of your application. And keep in mind, nothing is ever set in stone. Constantly refactoring and re-architecting is healthy for the longevity of your software project.
I hope this has helped you think more critically about how to best store data in your web applications.
I’d love to hear from you. Let me know what you think in the comments. What type of challenges have you run into with the state? What are your favorite state patterns?