Skip to content

Example of Using useSyncExternalStore with LocalStorage

Published: at 06:26 PM

In React development, it’s often necessary to synchronize the application state with an external data source, particularly with solutions like LocalStorage. Since React 18, a new hook called useSyncExternalStore has been introduced to facilitate this synchronization. I’ll walk you through a concrete example of using this hook.

Why use useSyncExternalStore?

useSyncExternalStore is designed to read and synchronize data from external sources that may be shared across multiple component instances. This enables the application to efficiently react to changes in these data sources, ensuring state consistency and providing a smooth user experience.

Concrete example: Synchronizing an application theme with LocalStorage

In this example, we will create a React application that allows the user to toggle between light and dark themes. The chosen theme will be stored in LocalStorage to persist between sessions, and we will use useSyncExternalStore to manage the theme synchronization across the entire application, even across different browser tabs.

useThemeStore

The first step is to create our custom hook that will handle listening to and updating our LocalStorage.

import { useSyncExternalStore } from "react";

type Theme = "light" | "dark";

const THEME_STORAGE_KEY = "app-theme";

const getThemeFromLocalStorage = (): Theme => {
  return (localStorage.getItem(THEME_STORAGE_KEY) as Theme) || "light";
};

const subscribe = (callback: () => void): (() => void) => {
  window.addEventListener("storage", callback);
  return () => {
    window.removeEventListener("storage", callback);
  };
};

const useThemeStore = (): [Theme, (newTheme: Theme) => void] => {
  const theme = useSyncExternalStore(subscribe, getThemeFromLocalStorage);

  const setTheme = (newTheme: Theme) => {
    localStorage.setItem(THEME_STORAGE_KEY, newTheme);
    window.dispatchEvent(new Event("storage"));
  };

  return [theme, setTheme];
};

export default useThemeStore;

Application

Here’s a concrete example of how we can use our useThemeStore hook to toggle the theme via our ThemeToggler component. The advantage of this solution is that the theme will be changed across all browser tabs.

// Header.tsx
import React from "react";
import useThemeStore from "./useThemeStore";
import styled from "styled-components";

const StyledHeader = styled.header<{ themeType: "light" | "dark" }>`
  padding: 1rem;
  text-align: center;
  background-color: ${({ themeType }) =>
    themeType === "light" ? "#f0f0f0" : "#222"};
  color: ${({ themeType }) => (themeType === "light" ? "#000" : "#fff")};
`;

const Header: React.FC = () => {
  const [theme] = useThemeStore();

  return (
    <StyledHeader themeType={theme}>
      <h1>Current Theme: {theme.charAt(0).toUpperCase() + theme.slice(1)}</h1>
    </StyledHeader>
  );
};

export default Header;
//  Footer.tsx
import React from "react";
import useThemeStore from "./useThemeStore";
import styled from "styled-components";

const StyledFooter = styled.footer<{ themeType: "light" | "dark" }>`
  padding: 1rem;
  text-align: center;
  background-color: ${({ themeType }) =>
    themeType === "light" ? "#e0e0e0" : "#111"};
  color: ${({ themeType }) => (themeType === "light" ? "#000" : "#fff")};
  position: absolute;
  bottom: 0;
  width: 100%;
`;

const Footer: React.FC = () => {
  const [theme] = useThemeStore();

  return (
    <StyledFooter themeType={theme}>
      <p>Footer Content - Theme is {theme}</p>
    </StyledFooter>
  );
};

export default Footer;
// ThemeToggler.tsx
import React from "react";
import useThemeStore from "./useThemeStore";
import styled from "styled-components";

const ToggleButton = styled.button<{ themeType: "light" | "dark" }>`
  padding: 0.5rem 1rem;
  font-size: 1rem;
  cursor: pointer;
  background-color: ${({ themeType }) =>
    themeType === "light" ? "#000" : "#fff"};
  color: ${({ themeType }) => (themeType === "light" ? "#fff" : "#000")};
  border: none;
  border-radius: 4px;
  transition:
    background-color 0.3s ease,
    color 0.3s ease;

  &:hover {
    background-color: ${({ themeType }) =>
      themeType === "light" ? "#333" : "#ddd"};
  }
`;

const ThemeToggler: React.FC = () => {
  const [theme, setTheme] = useThemeStore();

  const toggleTheme = () => {
    setTheme(theme === "light" ? "dark" : "light");
  };

  return (
    <ToggleButton themeType={theme} onClick={toggleTheme}>
      Switch to {theme === "light" ? "Dark" : "Light"} Theme
    </ToggleButton>
  );
};

export default ThemeToggler;
// App.tsx
import React from "react";
import Header from "./Header";
import ThemeToggler from "./ThemeToggler";
import Footer from "./Footer";
import useThemeStore from "./useThemeStore";
import styled from "styled-components";

const AppContainer = styled.div<{ themeType: "light" | "dark" }>`
  min-height: 100vh;
  background-color: ${({ themeType }) =>
    themeType === "light" ? "#ffffff" : "#333333"};
  color: ${({ themeType }) => (themeType === "light" ? "#000000" : "#ffffff")};
  display: flex;
  flex-direction: column;
  align-items: center;
  transition: all 0.3s ease;
  position: relative;
  padding-bottom: 3rem; // To ensure footer is visible
`;

const App: React.FC = () => {
  const [theme] = useThemeStore();

  return (
    <AppContainer themeType={theme}>
      <Header />
      <ThemeToggler />
      <Footer />
    </AppContainer>
  );
};

export default App;

Other Concrete Use Cases for useSyncExternalStore

Conclusion

useSyncExternalStore is a powerful tool for synchronizing your application state with external data sources like LocalStorage. With this hook, you can create React applications capable of handling shared data across multiple components or even multiple instances of your application.

By using useThemeStore, we demonstrated how to store and synchronize an application theme with LocalStorage, ensuring a smooth and consistent user experience, even when reloading the page or switching between tabs.

Sources and References