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?
+localRadio
? Why not simply returning the radio value stored in the local storage ? Isn't this what you want ? And it will be considered pure – Brennen+localRadio
?" - becauseradio
should be a number but local storage stores strings – AlkylationuseEffect
hooks to read from localStorage and initialize state, and to persist state changes is what led the OP to think function purity was the issue so they were asking about that. I initially read this as a bit of an XY problem. Perhaps I've misunderstood what the OP's goal was here? I guess the answer is simply, "yes, they must be pure because the React docs state they must be pure." Are you suggesting we un-duplicate since it might only be closely-related? – CalculatorI wouldn't really consider reading from localStorage to be a side-effect
do we have any source for this? Looked it up cannot find anything concrete – BrembleuseEffect
, or (b) the docs use a less strict definition. – CalculatoruseEffect
hook, but to your comment about React and reusable state and theReact.StrictMode
and double-mounting I think the end result be the same using either method since the goal is to initialize the state to the "latest"/"current" value from localStorage, and this is why (IMO) it doesn't feel like an impure operation. – Calculator