recoil is there a way to do additional actions when the atom state changes?

I am managing the theme state as an atom of recoil.

If the user changes the theme through the ui, the corresponding atom state will change.

I want to save the theme value changed here to localstorage. To do that, we need to know when that state changes.

Of course, you can add localstorage storage code before calling the corresponding atom state change, but this is too inefficient if atom is used in many places.

Should I use a selector at this point? However, since a theme is a single state, it is not intuitive to treat it as a derived state to extend the state change logic.

Any help would be appreciated if you know.

example code

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { RecoilRoot, atom, useRecoilState } from "recoil";

type ThemeItem = "light" | "dark";
// 🧐: I want to do some extra work when this state changes (ex: Save the changed state to localStorage)
const themeState = atom<ThemeItem>({
  key: "themeState",
  default: "light",
});

function App() {
  const [theme, setTheme] = useRecoilState(themeState);

  function handleTheme(theme: ThemeItem) {
    return () => setTheme(theme);
  }

  return (
    <>
      <h3>{theme}</h3>
      <button onClick={handleTheme("light")}>light</button>
      <button onClick={handleTheme("dark")}>dark</button>
    </>
  );
}

ReactDOM.render(
  <StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </StrictMode>,
  document.getElementById("root")
);

Answer

The way I’ve solved this before is by implementing a ThemeProvider (or whatever you want to call it), and then using useEffect both store and load values from localStorage:

import { StrictMode, useEffect } from "react";
import ReactDOM from "react-dom";
import { RecoilRoot, atom, useRecoilState } from "recoil";

type ThemeItem = "light" | "dark";
// 🧐: I want to do some extra work when this state changes (ex: Save the changed state to localStorage)
const themeState = atom<ThemeItem>({
  key: "themeState",
  default: "light",
});

function ThemeProvider() {
  const [theme, setTheme] = useRecoilState(themeState);

  useEffect(() => {
    // Save theme in localStorage on change
    localStorage.setItem('theme', theme);
  }, [theme]);

  useEffect(() => {
    // Load theme from localStorage on mount
    if (!localStorage.getItem('theme')) return;
    setTheme(localStorage.getItem('theme') as ThemeItem);
  }, []);
}

function App() {
  const [theme, setTheme] = useRecoilState(themeState);

  function handleTheme(theme: ThemeItem) {
    return () => setTheme(theme);
  }

  return (
    <>
      <h3>{theme}</h3>
      <button onClick={handleTheme("light")}>light</button>
      <button onClick={handleTheme("dark")}>dark</button>
    </>
  );
}

ReactDOM.render(
  <StrictMode>
    <RecoilRoot>
      <ThemeProvider />
      <App />
    </RecoilRoot>
  </StrictMode>,
  document.getElementById("root")
);