-
-
Save gragland/2970ae543df237a07be1dbbf810f23fe to your computer and use it in GitHub Desktop.
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]; | |
} |
Hi @gragland - Have you ran into any scenarios where local-storage is needed on more than one element?
I am running into the state being updated on two elements and they both need to be isolated of each other. The error is both counters keep duplicating each others values. That shouldn't be the intended behavior
@jephjohnson
I have made a solution for you using react's context.
Only thing you have to add is the LocalStorageProvider in your app.js
Here is a working example: https://codesandbox.io/s/zealous-bell-o6lh9
This is my use-local-storage.js:
import React, { useState, createContext, useContext } from "react";
export const localStorageContext = createContext();
export const useLocalStorageContext = () => useContext(localStorageContext);
export const LocalStorageProvider = ({ children }) => {
const [state, setState] = useState({});
return (
<localStorageContext.Provider value={{ state, setState }}>
{children}
</localStorageContext.Provider>
);
};
export default function useLocalStorage(key, initialValue) {
const { state, setState } = useLocalStorageContext();
let value = state[key];
if (!value) {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
value = 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(state[key]) : value;
// Save state
setState(prev => ({ ...prev, [key]: 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 [value, setValue];
}
And this is your app.js:
import React, { useState, useEffect } from "react";
import useLocalStorage from "./use-local-storage";
import { LocalStorageProvider } from "./use-local-storage";
const navLinks = [{ text: "Cars" }, { text: "Buses" }];
const Items = ({ text, id, activeTab }) => {
const [count, setCount] = useLocalStorage("count", 0);
const plus = () => {
setCount(count + 1);
};
const minus = () => {
setCount(count - 1);
};
return (
<div id={id} className={activeTab === id ? "active" : null}>
{text}
<h2>You clicked {count} times!</h2>
<button onClick={plus}>+</button>
<button onClick={minus}>-</button>
</div>
);
};
const Nav = ({ list, activeTab, onClick }) => {
return list ? (
list.map((item, index) => (
<Items
key={index}
text={item.text}
onClick={onClick}
activeTab={activeTab}
/>
))
) : (
<div>No Data</div>
);
};
function App() {
const [items, setlist] = useState([]);
useEffect(() => {
setlist(navLinks);
}, []);
return (
<LocalStorageProvider>
<Nav list={items} />
</LocalStorageProvider>
);
}
export default App;
Hi @gragland - Have you ran into any scenarios where local-storage is needed on more than one element?
I am running into the state being updated on two elements.
@jildertvenema - Doh! Looks like both counters are being updated at the same time. They should both be isolated. So if Increment the counter on the first one, refresh the value should persist. The second one should not duplicate the same value. Make sense? Both are independent of each other
@jephjohnson Then why not use count1 and count2?
@jephjohnson Then why not use count1 and count2?
In the state? Not following...
@jephjohnson Maybe i'm missing your point but why don't u use useLocalStorage("bussesCount", 0);
and useLocalStorage("carCount", 0);
@jephjohnson Maybe i'm missing your point but why don't u use
useLocalStorage("bussesCount", 0);
anduseLocalStorage("carCount", 0);
Codesandbox on what your thinking would be helpful
@jildertvenema - I am duplicating the component. What are you thinking exactly?
@jephjohnson i've added a stateName property to your tabs
@jephjohnson i've added a stateName property to your tabs
@jildertvenema - Epic! Solid idea :)
There is a flaw in the way this hook tries to emulate useState
behaviour.
React batches calls to setState
that happen in the same callback. Because of that you should never rely on the state returned by useState
to update the state. This is why you can also pass a function to setState
that receives the latest state as an argument. But this is actually broken here because the hook invokes the function with the state returned by useState
which may already be outdated from a previous call. Instead it should simply pass the function provided to setStoredValue
and update the local storage with the resulting state when it has been updated using useEffect
.
Also see Why is setState giving me the wrong value?.
Have a look at this example to see the problem:
const BrokenCounter = () => {
const [count, setCount] = useLocalStorage("count", 0);
const handleIncrement = () => {
// count should be increased by 2
// but it will only be increased by 1 because react batches those updates
// https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value
setCount(current => current + 1);
setCount(current => current + 1);
};
return (
<div>
<p>{`The count is: ${count}`}</p>
<button onClick={handleIncrement}>Increment by 2</button>
</div>
);
}
This is my suggestion for a fix:
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;
}
});
useEffect(() => {
try {
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
if you want this link
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 {
// if you want the real function
let valueToStore
if(value instanceof Function){
setStoredValue((v) => {
valueToStore = value(v)
return valueToStore
})
}
else{
valueToStore = value
setStoredValue(value)
}
// 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];
}
@gragland Would be great if this could be updated to sync across tabs. It's pretty much window.addEventListener("storage", updateState)
where updateState
is a memoized function to handle the StorageEvent
and set the state with the new value.
Inspired by donavon/use-persisted-state and useLocalStorage: hooks are nice.
I used this code and then had setValue
returned from this hook in a dependency array of a useEffect as I wanted to set a value in local storage from inside that effect. This creates an endless loop since you are returning a new setValue
on every render, each time your useLocalStorage is called so the effect this hook is used in will execute on every render.
I moved to this implementation instead which does not have the issue as it is using the setter returned from useState
which will be the same across renders:
https://bit.dev/giladshoham/react-hooks/use/use-local-storage/
I would suggest to modify your code or at least list a caveat so others don't have the same issue when they stumble upon this code from a web search.
@gragland Would be great if this could be updated to sync across tabs. It's pretty much
window.addEventListener("storage", updateState)
whereupdateState
is a memoized function to handle theStorageEvent
and set the state with the new value.
Here's what I came up with for that in case anyone wants to do the same:
import { useState, useEffect, useCallback } from 'react';
export function useLocalStorage(key, initialValue) {
// 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) {
console.warn(error);
// If error also return initialValue
return initialValue;
}
});
// Return a wrapped version of useState's setter function that persists the new value to localStorage
const setValue = useCallback(
(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.warn(error);
}
},
[storedValue, setStoredValue, key],
);
useEffect(
function setUpSyncOnMount() {
function storageWatcher(ev) {
// Update our state with new value from other tab
if (!document.hasFocus() && key === ev.key && ev.oldValue !== ev.newValue) {
try {
const newValue = JSON.parse(ev.newValue);
setStoredValue(newValue);
} catch (error) {
// A more advanced implementation would handle the error case
console.warn(error);
}
}
}
window.addEventListener('storage', storageWatcher);
return function removeOnUnmount() {
window.removeEventListener('storage', storageWatcher);
};
},
[key, setStoredValue],
);
return [storedValue, setValue];
}
export default useLocalStorage;
As bayareacoder suggested, if you're updating local state and not just using useLocalStorage
then make sure you're only setting with useLocalStorage => [, setValue]
if the values are different, or it will endlessly loop. For my use case I had sortBy
and setSortBy
that I was syncing with useLocalStorage
, so I kept a reference to the last stored sortBy
(storedSortBy
) with documentSortBy
in useRef
:
const [storedSortBy, setStoredSortBy] = useLocalStorage(`${id}-sort`, defaultSorted);
const documentSortBy = useRef(storedSortBy);
// ...
useEffect(
function syncSortStorage() {
// If the sort options changed on another tab/window
if (storedSortBy !== documentSortBy.current) {
setSortBy(storedSortBy);
documentSortBy.current = storedSortBy;
}
// If the sort options were changed directly by user
else if (sortBy !== storedSortBy) {
setStoredSortBy(sortBy);
documentSortBy.current = sortBy;
}
},
[sortBy, storedSortBy, setStoredSortBy, documentSortBy, setSortBy],
);
Came accros this issue
setValue((state) => "");
/*^^^^^^
This expression is not callable.
Not all constituents of type 'string | ((value: string | ((oldValue: string) => string)) => void)' are callable.
Type 'string' has no call signatures.(2349)
*/
I had to add in as const
on the returns values for it to comply without errors. I'm not sure what i have in my confguration for this to occur.
return [storedValue, setValue] as const;
I'm also rendering out from NextJS and it failed horribly attempting to access window, so had to add in
if (typeof window !== 'undefined') {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// Get from local storage by key
if (typeof window !== 'undefined') {
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
}else{
return initialValue;
}
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
I also wanted to add that the current implementation doesn't work even in the TypeScript playground, adding as const
to hook return type fixes this error
add typescript type in setValue
export function useLocalStorage<T>(key: string, initialValue: T) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
try {
if (typeof window !== 'undefined') {
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} else {
return initialValue;
}
} catch (error) {
// If error also return initialValue
console.error('localstorage', error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: T | ((val: T) => T)) => {
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] as [T, (value: T | ((val: T) => T)) => void];
}
Hi first of all thanks to everyone for the work.
I would like to suggest some improvements in the implementation:
- initialValue as a function - the
useState
hook let you to initiate the value with a function that execute once,useLocalStorage
should preserve this logic. - don`t mute errors - the
try/catch
block in the hook should surround only thelocalStorage
logic, and not the evaluation and storage of the state toreact
, to not hide error that been thrown insetState
callback evaluation. - memorized
setValue
- theuseState
hook return memorizedsetValue
callback to not trigger other hooks that use thesetState
as a dependency ,useLocalStorage
should preserve this logic. - store
undefined
- theJSON.stringify/parse
serialization can`t handleundefined
as value because JSON don`t supportundefined
, andgetItem
can`t separate it from uninitiated value , we can overcome that by storing aObject
with the stored value in thelocalStorage
.
Implementation:
import { useState, useCallback } from "react";
export default 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(() => {
// Allow initialValue to be a function so we have same API as useState
const evaluatedInitialValue = typeof initialValue === "function" ? initialValue() : initialValue;
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return evaluatedInitialValue
return item ? JSON.parse(item).value : evaluatedInitialValue;
} catch (error) {
console.log(`can\`t parse localStorage item ${key}`, error);
// If error also return evaluatedInitialValue
return evaluatedInitialValue;
}
});
// Return a wrapped version of useState's setter function that persists the new value to localStorage.
const setValue = useCallback(
(value) => {
// Allow value to be a function so we have same API as useState
const valueToStore = typeof value === "function" ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
try {
// Save to local storage
window.localStorage.setItem(key, JSON.stringify({value: valueToStore}));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(`can\`t save localStorage item ${key}`, error);
}
},
[key, storedValue]
);
return [storedValue, setValue];
}
Getting this error on Next.js for the first time
ReferenceError: window is not defined
Getting this error on Next.js for the first time
ReferenceError: window is not defined
Because your page is initially rendered on the server and not the browser, the window
object is not available yet. In that case you need to use the useEffect
when interacting with the window
object, since useEffect
gets called only on the client side. I don't know why this is not happening to me, but I have a different problem, but with the same cause. I get the warning:
Warning: Did not expect server HTML to contain a <div> in <div>
It happens because the initial client state is different the initial server state. As someone already pointed out interacting with localStorage is a side effect, therefore it should be in useEffect
. My solution was to set the passed initial state and use useEffect
to update it with the state from local storage. If I was the author of this hook, I would correct that.
With that said, I'm very grateful for this hook and it's still an awesome thing!
Getting this error on Next.js for the first time
ReferenceError: window is not definedBecause your page is initially rendered on the server and not the browser, the
window
object is not available yet. In that case you need to use theuseEffect
when interacting with thewindow
object, sinceuseEffect
gets called only on the client side. I don't know why this is not happening to me, but I have a different problem, but with the same cause. I get the warning:Warning: Did not expect server HTML to contain a <div> in <div>
It happens because the initial client state is different the initial server state. As someone already pointed out interacting with localStorage is a side effect, therefore it should be in
useEffect
. My solution was to set the passed initial state and useuseEffect
to update it with the state from local storage. If I was the author of this hook, I would correct that.With that said, I'm very grateful for this hook and it's still an awesome thing!
Thanks man, I got the solution long back.
Add initial value saving to LS in the first try/catch block, because otherwise no LS record is created before setValue call. Also make setValue a callback for optimization.
import { useState, useCallback } from 'react'
export const useLocalStorage = (key, initialValue) => {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
// Parse stored json or if none return initialValue
if (item !== null) {
return JSON.parse(item)
} else {
window.localStorage.setItem(key, JSON.stringify(initialValue))
return 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 = useCallback(
(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
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error)
}
},
[key, storedValue],
)
return [storedValue, setValue]
}
Hi all!
I would like to sugest a minor improvement to the setValue
method:
instead of this:
const setValue = (value: any) => {
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
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error)
}
}
I suggest this:
const setValue = (value: any) => {
try {
setStoredValue(latestValue => {
const valueToStore = value instanceof Function ? value(latestValue) : value
// Save to local storage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
return valueToStore
})
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error)
}
}
The purpose is to have access to the real previous value in the callback method :)
I think this implementation has a small flaw in that you are assuming storedValue
is up to date when you call
const valueToStore = value instanceof Function ? value(storedValue) : value
Probably better to use useEffect
with a dependency on the value of useState
and write the value there to localstorage instead.
When using this hook in an application and over time changing the object that is managed it will still return the object from localstorage which will miss the newly added property.
For example on my first day I commit using this hook:
useLocalStorage("example", { test: "hello" })
I deploy this code.
The next day I change the usage of this hook:
useLocalStorage("example", { test: "hello", somethingNew: "this was just added" })
Because I have test: "hello"
in my storage, this won't return the initial value for somethingNew.
2 possible solutions:
- the consumer of this hook should change the storage key every time this is done
- change
return item ? JSON.parse(item) : initialValue;
toreturn item ? { ...initialValue, ...JSON.parse(item)} : initialValue;
There is also the problem when removing an attribute it will still return it, but this might be less problematic.
You can sync the state of localStorage between all tabs that application is opened.
My solution was:
import { useEffect, useState } from "react";
const useLocalStorage = (key, initialValue) => {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
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
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
// Sync localStorage between tabs
useEffect(() => {
const listener = (e) => {
if (e.key === key) {
setStoredValue(JSON.parse(e.newValue));
}
};
window.addEventListener("storage", listener);
return () => {
window.removeEventListener("storage", listener);
};
}
, [
key,
initialValue,
]);
return [storedValue, setValue];
};
export default useLocalStorage;
Could we add a custom serialization/deserialization logic?
function useLocalStorage(key, initialValue, serialize = JSON.stringify, deserialize = JSON.parse) {
And use those functions instead of calling JSON.stringify
/JSON.parse
.
Maybe I am missing something, but why isn't it as simple as this?
TypeScript:
useEffect
but in this case it might be important to render the first time with the correct value from thelocalStorage
so can't put the reading part there. In my case component re-render causes flashing so I can't allow second re-render (when default value change to the one from localStorage) need the value right away, I presume this could be the case for others as well.