Why is localStorage getting cleared whenever I refresh the page?
Asked Answered
H

6

11

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]);
Hedvig answered 12/5, 2022 at 22:34 Comment(2)
What does it say when you inspect your localStorage in your browser? ie going inspect > Application > Local StorageWray
wouldn't it better to do directly const [todoList, setTodoList] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || []); ?Treulich
E
22

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]);

  ...
Effeminize answered 12/5, 2022 at 23:21 Comment(2)
This works, and is a method I learned in a react course, however the react docs for 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 for discussion on whether or not this requirement is important.Libretto
+1 for anyone else who lands here even if not using React, the key words are processed asynchronously...if you are clearing/setting localStorage in your code after some asynchronous operation (in my case it was after an async fetch()) then DON'T assume the browser will cancel that operation just because you refreshed the page. It appears the operation runs "in the background" and will clear localStorage such that your newly loaded page finds it empty (you can confirm this by disabling that code and refreshing to see the state still there this time round).Barkeeper
G
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])
Grimy answered 1/11, 2022 at 1:7 Comment(1)
this isn't great because an empty array is probably valid state e.g. once you remove all your todosLibretto
B
2

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

Builtin answered 9/2, 2023 at 15:58 Comment(2)
no quite on the topicEvangelicalism
worked for me since I am currently in development. ThanksMastrianni
L
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]);
}
Libretto answered 24/8, 2023 at 2:0 Comment(4)
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.Effeminize
so, anti-pattern on the one hand, breaking a rule in the react docs on the other - is there another better way?Libretto
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.Effeminize
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!Libretto
C
0

To ensure that your data is not removed from local storage when you refresh the page, you can use the following code:

const LOCAL_STORAGE_KEY = "task-list"

function TodoList() {
    const [todoList, setTodoList] = useState(()=>{
    const storedList = localStorage.getItem(LOCAL_STORAGE_KEY);
    return storedList ? JSON.parse(storedList) : []
    });

    useEffect(() => {
        localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList));
    }, [todoList]);
e
Conspecific answered 8/10, 2023 at 9:48 Comment(0)
B
0

I am using RTK for a React project. My Redux store was cleared when I manually refresh pages. I found these 2 pages helpful to resolve the "Redux state not persisting" issues. Persisting State Between Page Reloads in React RTK Store Setup

Here is my store setup,

## Store.ts file
import {combineReducers, configureStore} from '@reduxjs/toolkit';
import agentSlice from "../Reducers/agentSlice";

import operatorSlice from "../Reducers/operatorSlice";
import registrationSlice from "../Reducers/registrationSlice";
import driverSlice from '../Reducers/driver_Slice/driverSlice';
import configureAppStore_2 from './configureAppStore_2';



const saveToLocalStorage = (state:any) => {
    try {
        localStorage.setItem('state', JSON.stringify(state));
    } catch (e) {
        console.error(e);
    }
};

const loadFromLocalStorage = () => {
    try {
        const stateStr = localStorage.getItem('state');
        return stateStr ? JSON.parse(stateStr) : undefined;
    } catch (e) {
        console.error(e);
        return undefined;
    }
};


export const combinedReducer = combineReducers({
    agent: agentSlice,
    register: registrationSlice,
    operator:operatorSlice,
    driver:driverSlice,

});
const persistedStore = loadFromLocalStorage();

export const store =configureAppStore_2(persistedStore);


store.subscribe(() => {
    saveToLocalStorage(store.getState());
});


// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch


## configureAppStore_2.ts File:

import { configureStore } from '@reduxjs/toolkit'
import {combinedReducer} from './store';


const configureAppStore_2=(preloadedState:any) =>{
  const store = configureStore({
    reducer: combinedReducer,
    preloadedState,
  })

  //@ts-ignore
  if (process.env.NODE_ENV !== 'production' && module.hot) {
    //@ts-ignore
    module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
  }

  return store
}

export default configureAppStore_2;
Bad answered 27/12, 2023 at 9:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.