6

Like the title says, the localStorage I set registers the changes made to the todoList array and JSON.stringifys it; however, whenever I refresh the page the array returns to the default [] state.

const LOCAL_STORAGE_KEY = "task-list"

function TodoList() {
    const [todoList, setTodoList] = useState([]);

    useEffect(() => {
        const storedList = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
        if (storedList) {
            setTodoList(storedList);
        }
    }, []);
    
    useEffect(() => {
        localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList));
    }, [todoList]);
TheCryptoChad
  • 85
  • 1
  • 1
  • 4
  • What does it say when you inspect your localStorage in your browser? ie going inspect > Application > Local Storage – Richard Hpa May 12 '22 at 22:37
  • 1
    wouldn't it better to do directly ```const [todoList, setTodoList] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || []);``` ? – VersifiXion May 12 '22 at 22:44

4 Answers4

16

When you reload the app/component both effects will run, and React state updates are processed asynchronously, so it's picking up the empty array state persisted to localStorage before the state update is processed. Just read from localStorage directly when setting the initial todoList state value.

Example:

const LOCAL_STORAGE_KEY = "task-list"

function TodoList() {
  const [todoList, setTodoList] = useState(() => {
    return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || []
  });
    
  useEffect(() => {
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList));
  }, [todoList]);

  ...

Edit why-is-localstorage-getting-cleared-whenever-i-refresh-the-page

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • This works, and is a method I learned in a react course, however the [react docs for `useState`](https://react.dev/reference/react/useState) indicate that the initializer function should be pure (only depends on its parameters and produces no side-effects). See my answer below for a method that does not violate this rule. [see this question](https://stackoverflow.com/q/73940073/1402511) for discussion on whether or not this requirement is important. – jameh Aug 24 '23 at 02:05
3

The above solution does not work in all cases. Instead add a conditional in front of the localStorage.setItem line in order to prevent the [] case.

//does not work in all cases (such as localhost)
function TodoList() {
  const [todoList, setTodoList] = useState(() => {
    return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || []
  });
//use conditional instead
 useEffect(() => {
    if (todoList.length > 0) {localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList))}
  }, [todoList])
  • this isn't great because an empty array is probably valid state e.g. once you remove all your todos – jameh Aug 24 '23 at 13:12
1

Your React version is above v18 which implemented the <React.StrictMode>. If this is enabled in the index.js this code

useEffect(() => {
    const storedList = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
    if (storedList) {
        setTodoList(storedList);
    }
}, []);

Won't work because it detects potential problems. If you removed <React.StrictMode> it will work but I won't recommend it. The best solution is the first two answers

Azuren
  • 57
  • 7
0

This article demonstrates how to avoid running a useEffect hook on first render.

Here is your modified code that will not overwrite localStorage on the initial render:

import { useState, useEffect, useRef } from "react";

const LOCAL_STORAGE_KEY = "task-list"
const isMounted = useRef(false);

function TodoList() {
  const [todoList, setTodoList] = useState([]);

  useEffect(() => {
    const storedList = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
    if (storedList) {
        setTodoList(storedList);
    }
  }, []);

  useEffect(() => {
    // don't store on initial render
    if (isMounted.current) {
      localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList));
    } else {
      isMounted.current = true;
    }
  }, [todoList]);
}
jameh
  • 1,149
  • 3
  • 12
  • 20
  • This also would work, but just FYI, the article you link is from 2021 and "isMounted" logic/checks have since become generally considered to be a bit of a React anti-pattern. – Drew Reese Aug 24 '23 at 03:17
  • so, anti-pattern on the one hand, breaking a rule in the react docs on the other - is there another better way? – jameh Aug 24 '23 at 13:06
  • While I'm inclined to agree that, from a pure CS point, reading localStorage in the initializer function is *technically* impure, but also point out that in this case it's completely harmless. I think React applies the "pure function" label too rigidly. IMO the obvious intent there is that the initializer function should not be making *external* side-effects like fetching data or updating DBs, etc. Using a second effect to initialize the state and an "isMounted" variable are more moving parts and possibly gets weird with React 18's `StrictMode` double-mounting to ensure reusable state. – Drew Reese Aug 24 '23 at 16:48
  • My understanding is that `StrictMode` is meant to catch side-effects and impure functions at development time by running things twice, but side-effects are expected in `useEffect` hooks so it shouldn't cause a problem to have an impure function there (that's their reason to exist). While having an impure function as an initializer doesn't cause a problem now, it could in the future (perhaps react will rely on the cached output of the initializer function at some point). Then again perhaps they'll just change the docs and soften the requirement! – jameh Aug 24 '23 at 21:17