useEffect does not listen for localStorage
Asked Answered
U

6

22

I'm making an authentication system and after backend redirects me to the frontend page I'm making API request for userData and I'm saving that data to localStorage. Then I'm trying to load Spinner or UserInfo.

I'm trying to listen for the localStorage value with useEffect, but after login I'm getting 'undefined'. When the localStorage value is updated useEffect does not run again and Spinner keeps spinning forever.

I have tried to do: JSON.parse(localStorage.getItem('userData')), but then I got a useEffect infinite loop.

Only when I'm refreshing the page does my localStorage value appear and I can display it instead of Spinner.

What I'm doing wrong?

Maybe there is a better way to load userData when it's ready?

I'm trying to update DOM in correct way?

Thanks for answers ;)

import React, { useState, useEffect } from 'react';
import { Spinner } from '../../atoms';
import { Navbar } from '../../organisms/';
import { getUserData } from '../../../helpers/functions';

const Main = () => {
  const [userData, setUserData] = useState();
  useEffect(() => {
    setUserData(localStorage.getItem('userData'));
  }, [localStorage.getItem('userData')]);

  return <>{userData ? <Navbar /> : <Spinner />}</>;
};

export default Main;
Ursuline answered 12/4, 2020 at 21:13 Comment(1)
You are trying to update the DOM when the local storage is updated with user info. And you are updating the local storage when the api response comes. So the right approach is to change the DOM when the api resolves, not when the local storage changes. So this is the correct flow: api success -> (update local storage AND update user info global context) *in your useEffect dependency look for user info global context If it's not clear tell me to provide a full answer.Rennin
D
48

It would be better to add an event listener for localstorage here.

useEffect(() => {
  function checkUserData() {
    const item = localStorage.getItem('userData')

    if (item) {
      setUserData(item)
    }
  }

  window.addEventListener('storage', checkUserData)

  return () => {
    window.removeEventListener('storage', checkUserData)
  }
}, [])
Dalhousie answered 12/4, 2020 at 21:24 Comment(11)
To extend on this answer: The reason OPs example doesn't work is because the dependency array passed to useEffect determines whether or not to re-run the effect when the component is rendered, which means that while the effect would re-run if localStorage changed, it would have to render first. The way around this is to set up a subscription to localStorage, as Minan does, to watch for changes and notify the component to re-render.Nylanylghau
hi, can you please help me with this? #62907423Belated
What about this? Doesn't that mean that the eventListener wouldn't be triggered in the tab changing the storage?Unwise
Actually, it doesn't work like that. As you can see in this example demo, all values continue to work synchronously. Demo => mq4uu.csb.app Code => codesandbox.io/s/bold-rain-mq4uuDalhousie
@Dalhousie I am having a hard time reproducing this logic in a project, not sure why the event listener on storage doesn't react to removeItem event, added a second button to your sandbox: codesandbox.io/s/elegant-star-yw1z5Chequered
@Chequered I could not see the second button in the example you sent. But I think I updated this what you wanted to do. codesandbox.io/s/bold-rain-mq4uuDalhousie
@Dalhousie ah, just overwriting it, yea that could work. I must have forgotten to save on my link, I updated it but was trying to onClick={() => window.localStorage.removeItem("userData")} -- ended up just tossing the token in redux to get the behavior I was looking forChequered
its not workingMorbid
You can check this example. codesandbox.io/s/fervent-perlman-yu3oknDalhousie
Note that window.addEventListener('storage', handler) only listens for events in other pages, from the MDN docs This won't work on the same page that is making the changes, instead you can use document.addEventListener(eventName, eventHandler, false); as shown in this answerCoquillage
First Apply your handler and then when you store the new Values publish the Event. window.dispatchEvent(new Event('storage')); or create a LocalStorage Wrapper to apply optionally. This triggers the event on the same window.Wadewadell
K
12

Event listener to 'storage' event won't work in the same page

The storage event of the Window interface fires when a storage area (localStorage) has been modified in the context of another document.

https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event

Knowable answered 15/7, 2021 at 19:37 Comment(0)
A
4

The solution is to use this structure:

useEffect(() => {
    window.addEventListener("storage", () => {
      // When storage changes refetch
      refetch();
    });

    return () => {
      // When the component unmounts remove the event listener
      window.removeEventListener("storage");
    };
}, []);
Airless answered 28/5, 2021 at 20:54 Comment(0)
B
0

With React 18's useSyncExternalStore, something like:

const EVENT_TYPE = "localUpdated";
class LocalStorageEvent extends Event {
  constructor(readonly key: string) {
    super(EVENT_TYPE, { bubbles: true });
  }
}

export function useLocalStorage() {
  // ...
  const data = useSyncExternalStore(subscribe, getSnapshot);
  return data;
}

function subscribe(onStorageChange: () => void) {
  function listener(event: Event) {
    if (event instanceof LocalStorageEvent && event.key === "mykey") {
      onStorageChange();
    }
  }

  window.addEventListener(EVENT_TYPE, listener);
  return () => window.removeEventListener(EVENT_TYPE, listener);
}

function getSnapshot() {
  const value = localStorage.getItem("mykey");
  return value ? JSON.parse(value) : undefined;
}

Note useSyncExternalStore compares snapshot by Object.is, according to doc.

Bloemfontein answered 4/2 at 12:16 Comment(1)
According to https://mcmap.net/q/496373/-observe-localstorage-changes-in-js, event 'storage' may not work.Bloemfontein
H
0

this should be working:


  const [mode, setMode] = useState<'light' | 'dark'>('light');

  useEffect(() => {
    function checkModeChange() {
      const item = window.localStorage.getItem('theme');

      if (item) {
        setMode(item as 'light' | 'dark');
      }
    }

    checkModeChange();
    window.addEventListener('storage', checkModeChange);

    return () => {
      window.removeEventListener('storage', checkModeChange);
    };
  });
Homeric answered 18/6 at 9:58 Comment(0)
V
-2
  • "Maybe there is a better way to load userData when it's ready?"

You could evaluate the value into localStorage directly instead passing to state.

const Main = () => {
  if (localStage.getItem('userData')) {
    return (<Navbar />);
  }
  else {
    return (<Spinner />);
  }
};

If there is a need to retrieve the userData in more components, evaluate the implementation of Redux to your application, this could eliminate the usage of localStorage, but of course, depends of your needs.

Vas answered 12/4, 2020 at 21:39 Comment(4)
In that way it catches "undefined" value(and the spinner will spin forever). When after few seconds value "undefined" is updated, it will not update DOM.Ursuline
Interesting... Try to pass the userData as a props to the Main component.Vas
This makes a thread-blocking read into localstorage on every re-render, so beware of thatKasher
@Kasher Using IndexedDB could solve this? Knowing that works asynchronously...Vas

© 2022 - 2024 — McMap. All rights reserved.