Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import { useState } from 'react';
// Usage
function App() {
// Similar to useState but first arg is key to the value in local storage.
const [name, setName] = useLocalStorage('name', 'Bob');
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
);
}
// Hook
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = value => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}
@vitkon

This comment has been minimized.

Copy link

commented Oct 29, 2018

Is localStorage read on every rerender?

@skiano

This comment has been minimized.

Copy link

commented Oct 29, 2018

I wonder if it would be an improvement to write to local storage inside useEffect so that it runs after the render finished?

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Oct 29, 2018

@vitkon Local storage should only be read on the first render since I'm passing a function to useState. Unless I'm missing something..

@bduimstra

This comment has been minimized.

Copy link

commented Oct 29, 2018

I would also suggest wrapping localStorage calls in a try/catch block to avoid any unwanted side effects.

@andys8

This comment has been minimized.

Copy link

commented Oct 29, 2018

useEffect was said to be for side effects. Accessing and manipulating local storage is a side effect.

Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.
https://reactjs.org/docs/hooks-reference.html#useeffect

@deomsj

This comment has been minimized.

Copy link

commented Oct 29, 2018

love what you are doing here. thank you for sharing this!

one suggestion, could we swap out this:

const [item, setValue] = useState(() =>
    window.localStorage.getItem(key) !== null
      ? window.localStorage.getItem(key)
      : initialValue);

with this, for readability:

const [item, setValue] = useState(() => window.localStorage.getItem(key) || initialValue);

Looking forward to more recipes. I'm already hooked after just one :)

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Oct 29, 2018

@skiano @andys8 Good call. Just updated it to use useEffect instead. Thanks!

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Oct 29, 2018

@deomsj Glad you're liking it and good suggestion! Updated :)

@TortleWortle

This comment has been minimized.

Copy link

commented Oct 29, 2018

I like the recipe but there is an issue when using numbers and that they will convert to strings after a refresh. Maybe it is an idea to encode and decode as JSON ?

@AndyBarron

This comment has been minimized.

Copy link

commented Oct 30, 2018

Suggestion: Return a wrapped version of setValue that updates localStorage whenever it's called. With the current setup, localStorage will be written to on every render, which I don't imagine is fast!

@AndyBarron

This comment has been minimized.

Copy link

commented Oct 30, 2018

Suggested changes:

import { useState } from 'react';

// Usage
function App() {
  // Similar to useState but we pass in a key to value in local storage
  // With useState: const [name, setName] = useState('Bob');
  const [name, setName] = useLocalStorage('name', 'Bob');

  return (
    <div>
      <input
        type="text"
        placeholder="Enter your name"
        value={name}
        onChange={e => setName(e.target.value)}
      />
    </div>
  );
}

// Hook
const useLocalStorage = (key, initialValue) => {
  // The initialValue arg is only used if there is nothing in local storage ...
  // ... otherwise we use the value in local storage so state persist through a page refresh.
  // We pass a function to useState so local storage lookup only happens once.
  const [item, setInnerValue] = useState(
    () => {
      if (window.localStorage.hasItem(key)) {
        try {
          return JSON.parse(window.localStorage.getItem(key));
        } catch (error) {
          // Do nothing
        }
      }
      // Return default value if key is absent or JSON parsing fails
      return initialValue;
    },
  );

  // Return a wrapped version of useState's setter function that persists the new value to
  // localStorage.
  const setValue = (value) => {
    setInnerValue(value);
    window.localStorage.setItem(key, JSON.stringify(item));
  };

  return [item, setValue];
};
@gragland

This comment has been minimized.

Copy link
Owner Author

commented Oct 30, 2018

@AndyBarron So originally I had a wrapped function like you suggest but was getting some feedback that useEffect was the right way to do it. Now that I think about it, the wrapped function seems fine since it's not being run during render. So the two choices I see:

Your suggestion:

const setValue = (value) => {
  setInnerValue(value);
  window.localStorage.setItem(key, JSON.stringify(item));
};

Or make it an effect but pass array of values that need to change for it to be called:

useEffect(() => {
  window.localStorage.setItem(key, JSON.stringify(item));
},[key, item]);

Any thoughts on which would be the better choice? Anyone feel free to weigh in here.

@skiano

This comment has been minimized.

Copy link

commented Oct 30, 2018

This is sort of a tangent, but Philip Walton's article about idle until urgent was so inspiring that I can't help but think about it here

https://philipwalton.com/articles/idle-until-urgent/#persisting-application-state

For small cases this gist seems good, but if its being used inside a game or something that is more intensive, I wonder if it worth considering the pattern outlined under the "Persisting application state" example in the article (using an IdleQueue). I'm not suggesting that the gist change :) just thinking its worth considering before plugging this into an app.

@AndyBarron

This comment has been minimized.

Copy link

commented Oct 30, 2018

@gragland Another issue with useEffect is that it will immediately persist the default value to localStorage on mount, which might not be what you want. The wrapped version will only write to storage when setValue is called. I would consider the wrapped version better for this reason as well.

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Oct 30, 2018

@AndyBarron Yeah good point, okay going with the wrapped version. I changed your code slightly so that all calls to localStorage are within the try/catch.

  const [item, setInnerValue] = useState(() => {
    try {
      return window.localStorage.getItem(key)
        ? JSON.parse(window.localStorage.getItem(key))
        : initialValue;
    } catch (error) {
      // Return default value if JSON parsing fails
      return initialValue;
    }
  });
@ianobermiller

This comment has been minimized.

Copy link

commented Nov 9, 2018

Proposed updates:

  • You can wrap setValue with useCallback to ensure you don't return new setValue functions on every render
  • I think it should be stringifying value, not item
  • Might as well wrap this one in try-catch as well (and stringify might throw too)
  • call setInnerValue after localstorage in case persistence throws, so you will not silently think it is working
  const setValue = useCallback(value => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
      setInnerValue(value);
    } catch (e) {
      console.log(e);
    }
  }, [setInnerValue]);
@gustavoguichard

This comment has been minimized.

Copy link

commented Nov 12, 2018

Cool lil code =)
The localStorage is storing the previous state though, you must change line 42 for using value instead of item:

window.localStorage.setItem(key, JSON.stringify(value));

So it'll actually save the value that is reflected in the input ;)

@sabir-jamia

This comment has been minimized.

Copy link

commented Nov 23, 2018

Cool lil code =)
The localStorage is storing the previous state though, you must change line 42 for using value instead of item:

window.localStorage.setItem(key, JSON.stringify(value));

So it'll actually save the value that is reflected in the input ;)

I also think, it should be value instead of item. By doing this , If we refresh the page the value remains the same in the input box.

@Beraliv

This comment has been minimized.

Copy link

commented Nov 24, 2018

@sabir-jamia is right as you save previous version of a string

@rista404

This comment has been minimized.

Copy link

commented Jan 24, 2019

As @gustavoguichard said, it should be window.localStorage.setItem(key, JSON.stringify(value)); on line 42, not item

@turdiyev

This comment has been minimized.

Copy link

commented Feb 7, 2019

Yep, should be value, not item.

BTW, If you need typescript code, here you are:

import { useState, Dispatch, SetStateAction } from 'react';


export default function <S>(
    key: string,
    initialValue?: S
): [S, Dispatch<SetStateAction<S>>] {
    // The initialValue arg is only used if there is nothing in localStorage ...
    // ... otherwise we use the value in localStorage so state persist through a page refresh.
    // We pass a function to useState so localStorage lookup only happens once.
    // We wrap in try/catch in case localStorage is unavailable
    const [item, setInnerValue] = useState<S>(() => {
        try {
            const valueItem = window.localStorage.getItem(key);
            return valueItem ? JSON.parse(valueItem) : initialValue;
        } catch (error) {
            // Return default value if JSON parsing fails
            return initialValue;
        }
    });

    // Return a wrapped version of useState's setter function that ...
    // ... persists the new value to localStorage.
    const setValue = (value: SetStateAction<S>): SetStateAction<S> => {
        setInnerValue(value);
        window.localStorage.setItem(key, JSON.stringify(value));
        return value;
    };

    // Alternatively we could update localStorage inside useEffect ...
    // ... but this would run every render and it really only needs ...
    // ... to happen when the returned setValue function is called.
    /*
    useEffect(() => {
      window.localStorage.setItem(key, JSON.stringify(item));
    });
    */

    return [item, setValue];
}
@gragland

This comment has been minimized.

Copy link
Owner Author

commented Feb 7, 2019

Thanks for the feedback everyone! Just updated the recipe to fix the usage of item instead of value and now wrapping the second local storage call in a try/catch.

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Feb 11, 2019

Just updated so value passed to setValue can also be a function that gets passed current state (just like useState).

@oleg-am

This comment has been minimized.

Copy link

commented Mar 28, 2019

We have wrong behavior, if in localStorage saved string value without JSON.stringify
Example: window.locslStorage.setItem('someKey', 'stringValue')

This code will return initial value, because JSON.parse(item) will caused error.
But we need item.

try {
      // Get from local storage by key
      const item = window.localStorage.getItem(key);
      // Parse stored json or if none return initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      // If error also return initialValue
      console.log(error);
      return initialValue;
    }

We need return item from catch block, and change area of try block:

      // Get from local storage by key
      const item = window.localStorage.getItem(key);
try {
      // Parse stored json or if none return initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      // If error return item
      console.log(error);
      return item;
    }
@beausmith

This comment has been minimized.

Copy link

commented May 17, 2019

I wish I had seen @turdiyev's post here before I had written my own TypeScript version of this useLocalStorage hook.

This version is similar, with exceptions:

  • doesn't have try/catch blocks (simple to add back in if needed).
  • removes the localStorage if value to store is "falsey".
  • named useStateWithLocalStorage to be more explicit.
// useStateWithLocalStorage.ts

import { useState, Dispatch, SetStateAction } from 'react'

const useStateWithLocalStorage = <S>(
  key: string,
  initialValue?: S
): [S, Dispatch<SetStateAction<S>>] => {
  const [item, setInnerValue] = useState<S>(() => {
    const valueItem = window.localStorage.getItem(key)
    return valueItem ? JSON.parse(valueItem) : initialValue
  })

  const setValue = (value: SetStateAction<S>): SetStateAction<S> => {
    const valueToStore = value instanceof Function ? value(item) : value
    setInnerValue(valueToStore)
    if (valueToStore) {
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } else {
      window.localStorage.removeItem(key)
    }
    return value
  }

  return [item, setValue]
}

export default useStateWithLocalStorage

PS - @turdiyev - Thanks for your example above; I learned a couple things!

@turdiyev

This comment has been minimized.

Copy link

commented May 18, 2019

@beausmith. You are welcome. I'm glad to help you

@jildertvenema

This comment has been minimized.

Copy link

commented Jul 12, 2019

Suggested change:

if value is null or undefined remove it from local storage.

import { useState } from 'react'

// Hook
function useLocalStorage (key, initialValue) {
  // State to store our value
  // Pass initial state function to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState(() => {
    try {
      // Get from local storage by key
      const item = window.localStorage.getItem(key)
      // Parse stored json or if none return initialValue
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      // If error also return initialValue
      console.log(error)
      return initialValue
    }
  })

  // Return a wrapped version of useState's setter function that ...
  // ... persists the new value to localStorage.
  const setValue = value => {
    try {
      // Allow value to be a function so we have same API as useState
      const valueToStore =
        value instanceof Function ? value(storedValue) : value
      // Save state
      setStoredValue(valueToStore)
      // Save to local storage or remove if value is null or undefined
      if (valueToStore === null || valueToStore === undefined) {
        window.localStorage.removeItem(key)
      } else {
        window.localStorage.setItem(key, JSON.stringify(valueToStore))
      }
    } catch (error) {
      // A more advanced implementation would handle the error case
      console.log(error)
    }
  }

  return [storedValue, setValue]
}

export default useLocalStorage
@vinicius98s

This comment has been minimized.

Copy link

commented Aug 8, 2019

As said by @oleg-am when you try to parse a string from localStorage you get an error, but we still want to return the initalValue if it doesn't exist so we can call an external function to verify this

The same occurs if you try to stringify a string, so when try to save it on localStorage verify if it already is a string

const useLocalStorage = (key, initialValue) => {
  const parseJsonItem = item => {
    try {
      return JSON.parse(item);
    } catch (e) {
      return item;
    }
  };

  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);

      if (item) return parseJsonItem(item);

      return initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = value => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(
        key,
        typeof valueToStore === 'string'
          ? valueToStore
          : JSON.stringify(valueToStore),
      );
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.