← Back To Blog

4 Ways to Simplify React State (By Not Using React)

8 min read

React Global State

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. Checkout Part One for tips and tricks on storing state in the React lifecycle.

Types of Storage

There are four mechanisms for storing and accessing data in a React application that we are going to focus on:

  1. Web Storage: Local Storage, Session Storage & Cookies
  2. URL & Router state
  3. React Refs: State outside of React lifecycle
  4. Browser Storage

And a Bonus at the end!

1. Web Storage

Web Storage in React

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

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.

// Storing data
localStorage.setItem("lang", "en-US");
// You can also store objects in a stringiifed format:
JSON.stringify({ lang: "en-US", theme: "dark" })
// Acessing data
const lang = localStorage.getItem("lang");
// Deleting data
// Clear all

More info on local storage: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage

Session Storage

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:

// Storing data
sessionStorage.setItem("lang", "en-US");
// Accessing data
const lang = sessionStorage.getItem("lang");
// Deleting data

More info on session storage: https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage


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.

response.setHeader("Set-Cookie", ["<key>=<value>"]);

The browser can directly read and write cookies using document.cookie. It looks like a string but behaves like a getter/setter function:

document.cookie = "theme=dark";
document.cookie = "lang=en-US";
console.log(document.cookie); // "theme=dark; lang=en-US;"

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: https://developer.mozilla.org/en-US/docs/Web/HTTP/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.

2. URL & Router State

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 encodeURIComponent() and decodeURIComponent() to handle special characters.
  • query-string is a useful npm package to help manage complexities with search parameters.

URL Search Params

Using 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.

You can pass the state as a regular JavaScript object to history or when using Link:

// In the original page:
const history = useHistory();
history.push({pathname: '/my-path', state: { message: 'Hello from the other side.' }})
// or
pathname: '/my-path',
state: { message: 'Hello from the other side.' },
// In the '/my-path' page:
const { location } = useLocation();
console.log(location.state.message) // 'Hello from the other side'.

3. React Refs

Refs are better suited for getting access to a DOM element for direct manipulation like programmatic focus:

const inputEl = useRef(null);
useEffect(() => {
// Note: Be careful about setting focus programatically and make sure it does not violate accessibility of the form
}, []);
return <input ref={inputEl} type="text" />;

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.

const clicks = useRef(0);
const onClick = () => {
clicks.current += 1;
console.log("clicks: ", clicks.current);
return (
<button data-attribute-clicks={clicks.current} onClick={onClick}>
Click me!

useRef: https://reactjs.org/docs/hooks-reference.html#useref

4. Browser Storage

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.

IndexedDB is also a native browser capability but is designed to tackle storing larger datasets on the client-side. It is a transactional database system similar to SQL-based- RDBMS - but with the friendliness of JavaScript objects because you can store and access data with just a key. It is available in all modern browsers.

More info on IndexedDB: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API


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: https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage

Bonus: Don't store it in the client

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 clients 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 state 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.

Final Thoughts

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 state? What are your favorite state patterns?

If you enjoyed this article check out Part One.