Hier is an example of a problem I encountered:
import ReactDOM from "react-dom/client";
import React, { useState, useEffect } from "react";
const App = () => {
  // problematic
  const [radio, setRadio] = useState(1);
  useEffect(() => {
    const localRadio = localStorage.getItem('radio');
    if (localRadio) {
      setRadio(+localRadio);
    }
  }, []);
  // My "solution" using an initializer to read from localStorage
  // const [radio, setRadio] = useState(() => {
  //   const localRadio = localStorage.getItem('radio');
  //   return localRadio ? +localRadio : 1;
  // });
  useEffect(() => {
    localStorage.setItem('radio', radio);
  }, [radio]);
  const radioChangeHandler = (event) => {
    setRadio(+event.target.value);
  };
  return (
    <div>
      <h1>useState initializer demo</h1>
      <div className="radio-group" onChange={radioChangeHandler}>
        <input
          type="radio"
          value="1"
          checked={radio === 1}
          id="language1"
          name="languageChoice"
        />
        <label htmlFor="language1">Javascript</label>
        <input
          type="radio"
          value="2"
          checked={radio === 2}
          id="language2"
          name="languageChoice"
        />
        <label htmlFor="language2">HTML</label>
        <input
          type="radio"
          value="3"
          checked={radio === 3}
          id="language3"
          name="languageChoice"
        />
        <label htmlFor="language3">CSS</label>
      </div>
    </div>
  );
};
const container = document.querySelector("#root");
const root = ReactDOM.createRoot(container);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
The idea is to write into localStorage everytime radio changes. And when I open/refresh the site, it should read from the localStorage and set the value. But then I got the problem that the useEffect with [radio] always overwrites the other one, so I always get 1 as value for radio regardless of what's written in localStorage at the beginning.
(I just found out that the code actually works if I remove StrictMode, I thought I've tested it before... but react wants that the app also works if it renders twice anyway.)
My solution was to use an initializer function to read from localStorage (the commented out code). It works fine, and I was proud of me. Then I read on https://beta.reactjs.org/apis/react/useState#parameters
If you pass a function as initialState, it will be treated as an initializer function. It should be pure, should take no arguments, and should return a value of any type.
Also on https://beta.reactjs.org/apis/react/useReducer#my-reducer-or-initializer-function-runs-twice
Only component, initializer, and reducer functions need to be pure.
Does it mean that I shouldn't read from localStorage (or fetch data from API) in initializer? And what's the right way to do it?
No. The documentation mentions that it should be pure.
If you pass a function as initialState, it will be treated as an initializer function. It should be pure, should take no arguments, and should return a value of any type.
The main idea is that with concurrent React coming into the picture, the rendering process is broken into pieces and done in parts, pausing and resuming at times. So React methods might be called more than once, and this can lead to invalid app state. (Eg: In first render localStorage has value 5, but in next it has mangoes :D). To detect these kind of issues React runs a lot of code twice: function passed to useState being one of them.
So one way to do this if you want to follow the above rule is, to read from localStorage in componentDidMount or useEffect(() => { ... },[]);. You can find relevant code in other answers.
You can have a ref variable which you update in this useEffect callback which indicates if this is the first render or not.
You can have some null value for your state variable to let you know that you do not have the correct value for radio input.
There are multiple options.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With