February 8, 2025

Managing Persistent Browser Data with useSyncExternalStore

Ever found yourself repeatedly writing state management logic for localStorage or sessionStorage—only to discover it’s a bit more complicated than just calling getItem() or setItem() in React? You’re not alone! Tracking changes in these storages across multiple components or tabs can get messy quickly. Enter React’s useSyncExternalStore hook—a relatively new addition (as of React 18) that lets you build stable, consistent, and reactive custom storage hooks.

This blog post demonstrates how you can leverage useSyncExternalStore to manage data in both sessionStorage and localStorage with surprising ease. We’ll break down how it works, why it’s beneficial, and share a demo that showcases how you can persist React variables through tabs, sessions, and reloads.

TL;DR

  1. useSyncExternalStore is a hook that allows you to build custom subscriptions to external data sources—like local or session storage—and keep React states in sync only when these stores truly update.
  2. It’s perfect for building persistent reactive variables that live in the browser, saving you from the overhead of complex state management libraries.
  3. With minimal code, you can listen to storage events, retrieve the latest value, and trigger React re-renders whenever the stored data changes.
  4. Local Storage vs. Session Storage: Use localStorage for data that should persist across browser sessions and tabs, and sessionStorage for data that should reset when the tab or session ends.
  5. Demo: Follow along with our small Vite + React app to see these hooks in action!

Demo

We’ve built a small Vite + React app that illustrates exactly how these hooks work in practice. The project uses:

  • useLocalStorage to store user theme information (light/dark mode).
  • useSessionStorage to handle short-lived, session-scoped data like a selected account type.

You can find the project here: Demo Repo Link

Feel free to clone the repo, run it locally, and see how each storage hook performs when switching tabs or refreshing pages. This hands-on experience will help solidify your understanding of how useSyncExternalStore can simplify state persistence in your React applications.

Anatomy of useSyncExternalStore

useSyncExternalStore is designed to integrate an external data source with React’s concurrency features. It guarantees React always has the latest snapshot of that data, plus a subscription mechanism for updates. The hook involves three major parts:

  1. subscribe: Defines how React should listen for changes in the external store. Often, you’ll attach an event listener (like storage) and return a cleanup function that removes it.
  2. getSnapshot: Returns the latest value from the store. React calls this to fetch current data during renders.
  3. Server Snapshot (optional): For server-side rendering scenarios, you can provide an additional snapshot function. If you’re client-only, you can ignore it or pass null.

Why Is useSyncExternalStore Useful?

  • Concurrent-safe: Plays nicely with React’s concurrent rendering mode, avoiding update pitfalls.
  • Accurate updates: Components only re-render when the store’s data actually changes, preventing unnecessary refreshes.
  • Consistent: Helps dodge race conditions or outdated data that often arise from naive local/session storage usage.

Session Storage vs. Local Storage

Before diving into custom hooks, let’s clarify why you might want to store something in sessionStorage vs. localStorage:

  1. Local Storage
    • Persists data even after the browser or tab is closed.
    • Ideal for user preferences, theme modes, or data you want to keep until explicitly cleared.
  2. Session Storage
    • Data lives only as long as the tab or browser is open.
    • Perfect for short-lived, session-specific data—like ephemeral wizard steps or a selected account.

Why treat them similarly? In practice, reading and writing to each is nearly identical; you just switch window.localStorage for window.sessionStorage. That’s why you can build a single “storage pattern” and adapt it seamlessly for either type.

Custom Storage Hooks

Overview

We introduce a useBrowserStorage hook that consolidates all the logic for reading from and writing to web storage. Then we wrap it with two specialized hooks:

  • useLocalStorage
  • useSessionStorage

This abstraction allows you to manage both storage types with consistent logic, avoiding code duplication and ensuring reliable state synchronization across your application.

How useBrowserStorage Works

The useBrowserStorage hook serves as the foundation for managing data in either localStorage or sessionStorage. It can be found in the project at src/shared/hooks. Here’s how its key components function:

Cached Value

  • Purpose: The hook keeps an in-memory cache of the current storage value using a React ref. This ensures that the most recent data is accessible without triggering unwanted re-renders.
  • Behavior: Whenever the hook reads from storage, it compares the stored JSON data to the cached ref. If they differ, it updates the ref to match the new data, maintaining consistency between the internal state and the actual storage.

getSnapshot

  • Purpose: The getSnapshot function synchronously returns the current value from the browser storage. It’s essential for useSyncExternalStore, allowing React to retrieve the freshest data during renders.
  • Behavior: Each time getSnapshot runs, the hook parses the stored JSON. If the parsed data differs from the cached ref, the ref is updated. This ensures the snapshot reflects the latest state of localStorage or sessionStorage.

subscribe

  • Purpose: This function instructs useSyncExternalStore on how to listen for external changes. In this context, an “external change” occurs if another tab or a different hook instance updates the same storage key.
  • Behavior: Subscribing involves attaching a storage event listener to the window. When a storage change event fires, the listener invokes a callback provided by React, signaling that an update has occurred. React then calls getSnapshot to fetch the new data and re-renders components if necessary.

setValue

  • Purpose: The setValue function updates the browser storage and the internal cache. This enables components to write data back into localStorage or sessionStorage.
  • Behavior: When setValue is invoked, the hook:
    1. Retrieves the current cached value.
    2. Determines the new value (either directly or via a function).
    3. Updates the appropriate storage (localStorage or sessionStorage).
    4. Updates the cached ref.
    5. Dispatches a synthetic storage event to notify all subscribers of the change, ensuring synchronization across tabs and components.

How useLocalStorage and useSessionStorage Build on useBrowserStorage

Both useLocalStorage and useSessionStorage are lightweight wrappers that utilize useBrowserStorage. Essentially, they invoke the same core hook—useBrowserStorage—but specify the storage type (localStorage or sessionStorage) to use. This separation of concerns allows you to create specialized hooks without duplicating the underlying logic.

Usage in the App

In our demo app:

  • useLocalStorage is utilized for persistent data such as theme preferences (via a useThemeMode hook). This ensures that the user’s dark or light mode setting is remembered across browser sessions and tabs.
  • useSessionStorage manages data that only needs to exist for the duration of the current browser tab or session, such as a selected account type. This selection persists through page reloads but resets when the tab or session ends.

This approach neatly separates long-term data from session-only data without duplicating the underlying storage logic, making your codebase cleaner and more maintainable.

Conclusion

Persisting React state variables across reloads and tabs doesn’t have to be a tangled mess of stateful code and useEffects. With useSyncExternalStore, you gain a robust, concurrency-safe tool to:

  • Subscribe to storage events efficiently.
  • Re-render components only when relevant data changes.
  • Maintain consistent state across multiple tabs and browser sessions.

By centralizing storage logic in a versatile useBrowserStorage hook and creating specialized hooks like useLocalStorage and useSessionStorage, you streamline your state management approach. This not only enhances code readability and maintainability but also ensures a seamless user experience as data persists reliably across various user actions.

Ready to elevate your React state management? Clone, edit, and play around with our Demo Repo. See firsthand how useSyncExternalStore can simplify persisting state across tabs and reloads. Embrace this powerful hook to build more resilient and user-friendly applications!

Happy Coding!
If you have questions or want to share how you’re using these hooks, feel free to reach out or open an issue in the demo repository. useSyncExternalStore is still a relatively new concept in React, so there’s plenty of room for the community to discover even more amazing use cases.

Yeti designs and develops innovative digital products. If you have a project you'd like to get started on, we'd love to chat! Tell us a bit about what you're working on and we'll get back to you immediately!

A yeti leads 2 hikers across a surreal, otherworldly landscape formed of smooth twisting rocks covered in moss and vegetation.
Let's Talk

The Tech Landscape Is Changing. Let's Explore What's Next.

See how an end-to-end partnership with Yeti can help you build, fund, and grow a software business rooted in your unique industry expertise.

Book a Meeting
Let's Talk