One tool for storing data browser-side we might reach for is local storage. In this post, we'll use local storage in React by rolling our own useLocalStorage hook.
If you enjoy this tutorial, please give it a 💓, 🦄, or 🔖 and consider:
- signing up for my free weekly dev newsletter
- subscribing to my free YouTube dev channel
Our Approach
To approach this problem, let's break it down into pieces.
-
Provide a local storage key. Local storage works off of key-value pairs, so we'll want to be able to provide a
key
for our stored data. -
Provide a default value. If there's no existing data in local storage under the provided
key
, we'll want to be able to provide adefualtValue
for our data. -
Load the local storage value into state (or default if no local storage value exists). We'll still be maintaining stateful information in our app, so we can still use the
useState
hook. The difference here is we'll use the local storage value if it exists before we consider the user-provideddefaultValue
. - Save the stateful data to local storage. When our stateful data changes, we'll want to make sure local storage is kept up to date. Therefore, on any change to our variable, let's run an effect to sync up local storage.
-
Expose the state variable and a setter. Much like the
useState
hook, ouruseLocalStorage
hook will return a 2-element array. The first element will be the variable and the second will be a setter for that variable.
Creating the Hook
Let's create the hook! As noted above, the hook will take two inputs: the key
that will be used in localStorage
and the defaultValue
, which will be used in the even that there's nothing in localStorage
yet.
useLocalStorage.js
export const useLocalStorage = (key, defaultValue) => {};
Next up, let's load any data in localStorage
under the provided key
.
export const useLocalStorage = (key, defaultValue) => {
const stored = localStorage.getItem(key);
};
Now we know that the initial value for our stateful variable is going to be this stored
value. However, if there's nothing in localStorage
yet under the provided key
, we'll default to the user-provided defaultValue
.
Note: since localStorage
data are stored as strings, we make sure to JSON.parse
any data we retrieve from there.
export const useLocalStorage = (key, defaultValue) => {
const stored = localStorage.getItem(key);
const initial = stored ? JSON.parse(stored) : defaultValue;
};
Now that we have our initial
value for state, we can use our regular useState
hook format and return our stateful variable and its setter.
import { useState } from 'react';
export const useLocalStorage = (key, defaultValue) => {
const stored = localStorage.getItem(key);
const initial = stored ? JSON.parse(stored) : defaultValue;
const [value, setValue] = useState(initial);
return [value, setValue];
};
Almost done! We still have one outstanding requirement we haven't met yet: we need to save any data back to localStorage
when it's changed. I like doing this in a useEffect
hook that's triggered when value
changes.
import { useState, useEffect } from 'react';
export const useLocalStorage = (key, defaultValue) => {
const stored = localStorage.getItem(key);
const initial = stored ? JSON.parse(stored) : defaultValue;
const [value, setValue] = useState(initial);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
There we have it! Whenever value
changes, our effect will run, meaning we'll set the localStorage
item to be set to the JSON.stringify
of our value
. Note that the provided key
is also a dependency of our effect, so we include it in the dependency array for completeness even though we don't really expect it to change.
Testing Out Our New Hook
Let's take the hook our for a test drive! We'll create a simple component that has a text input whose value is based on our useLocalStorage
hook.
App.jsx
import React from 'react';
import { useLocalStorage } from './useLocalStorage';
function App() {
const [name, setName] = useLocalStorage('username', 'John');
return (
<input
value={name}
onChange={e => {
setName(e.target.value);
}}
/>
);
}
export default App;
Now let's run our app. We can see that, when we first run the app, our stateful name
variable is defaulted to the string John
. However, when we change the value and then refresh the page, we're now defaulting to the value persisted to localStorage
.
Success!
Top comments (2)
Now time to implement something like this with IndexedDB
Nice post, I'll implement this on a Chrome extension, for settings state management.