Written by Ibadehin Mojeed ✏️
localStorage
is one of the two mechanisms of a browsers’ web storage. It allows users to save data as key/value pairs in the browser for later use.
Unlike the sessionStorage
mechanism, which persists data in the browser storage as long as the current browser tab is running, localStorage
does not clear data when the browser closes.
This makes it ideal for persisting data not bound to the current browser tab.
Developers often implement localStorage
when adding a dark mode feature to an application, persisting a to-do item, or persisting a user’s form input values, among many other use cases.
In this guide, we cover how to use localStorage
to persist a user’s form input in the browser storage using React Hooks. We’ll also cover how to create a custom React Hook to share similar logic between multiple components.
localStorage
with React Hooks prerequisites
To follow this guide, ensure you have a basic understanding of React and React Hooks. Also, ensure you have Node.js installed on your computer.
Initial localStorage
project setup
Working with a fresh React application, let’s head over to the computer terminal and run the following command to create a new React project:
npx create-react-app localstorage-react-hook
Once the project folder generates, open it with a code editor and start the development server by running the npm start
command.
The project should launch in the browser at http://localhost:3000/.
Creating a React form component
As mentioned earlier, we will use the localStorage
to persist a user’s form input in the browser storage.
Like every React application, our focus is on the src
folder. So, let’s delete all the files inside the src
and create an index.js
file inside src
to avoid a frontend break.
Then, add the following code to index.js
:
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
// styles
import "./app.css";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
Notice that we imported a CSS file to add styling to the app. So, let’s create an app.css
folder in the src
folder.
Copy the styles from the localstorage-react-hook-project and add them to the app.css
file.
Next, create a components
folder in the src
folder to hold the component files. Then, add an App.js
file and a Form1.js
file. The App.js
file is the root and parent component while Form1.js
will hold the form inputs.
Add the following code in the components/App.js
file:
import Form1 from "./Form1";
const App = () => {
return (
<div className="container">
<h1>localStorage with React hooks</h1>
<Form1 />
</div>
);
};
export default App;
And finally, add this code to the components/Form1.js
file:
import { useState } from "react";
const Form1 = () => {
const [name, setName] = useState("");
return (
<form>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Full name"
aria-label="fullname"
/>
<input type="submit" value="Submit"></input>
</form>
);
};
export default Form1;
After saving the files, test the project and you should see this rendering:
The code above is the simplest implementation of the form inputs in React. By using the useState
React Hook to control the component, we keep the input state up-to-date on every keystroke, as seen above.
But, once we trigger a page refresh, the input data clears, which is expected.
To persist the input data so it’s available on a page reload or on subsequent revisits, we must save the data in the localStorage
.
Saving the form input data in localStorage
localStorage
gives us access to a browser’s Storage
object. The Storage
object has methods available to save, read, and remove data, among many other actions.
To see a list of the Storage
methods, open the browser console and type localStorage
. After pressing enter, the methods become available on the Storage
object's prototype
.
Using the setItem()
method
To store the form input data in the browser storage, we must invoke the setItem()
storage method by using the following syntax:
localStorage.setItem("key", "value")
The browser storage only accepts data-type strings. So, for values of different data types like the object or array, we must convert it to a JSON string using JSON.stringify()
.
Using the useEffect
Hook to perform side effects
We can also use the useEffect
React Hook to perform side effects, such as storing data in the browser storage. This makes this Hook a perfect place to call the setItem
method.
Open the components/Form1.js
file and add the following code above the return
statement:
useEffect(() => {
// storing input name
localStorage.setItem("name", JSON.stringify(name));
}, [name]);
Ensure to import the useEffect
from React like so:
import { useState, useEffect } from "react";
Here, we’ve assigned a key, "name"
, and a dynamic value from the state variable, which is name
.
The initial value of the name
state variable defaults to an empty string:
const [name, setName] = useState("");
Using JSON.stringify
in the setItem
is optional when saving string data to the storage:
localStorage.setItem("name", JSON.stringify(name));
However, JSON.stringify
is required if the value is a different data type, like an object or array.
Now, save the file and test the project; we should see the following render:
On every keystroke, the input value saves in the local storage because the useEffect
Hook holding the setItem
storage method runs on the first component render and after every state change.
However, on a page reload, the value in the storage returns to an empty string. This is happening because we’ve assigned a default empty string to the state variable, name
. Hence, React uses the empty value on the initial render.
Now, instead of assigning an empty string, we must get the updated state value at every point from the storage and assign it as the default state value.
Reading data from the localStorage
On an initial page load, instead of assigning an empty string to the name
state variable, we must assign a function that accesses the local storage, retrieve the saved value, and use that value as the default.
Using the getItem()
method
Update the useState
Hook in the components/Form1.js
file:
const [name, setName] = useState(() => {
// getting stored value
const saved = localStorage.getItem("name");
const initialValue = JSON.parse(saved);
return initialValue || "";
});
Here, we use the getItem()
storage method to retrieve data from the local storage. The JSON.parse()
used in the code deserializes the returned JSON string from the storage.
Both the JSON.Stringify
and the JSON.parse
are optional when working with string values (as seen in our case). However, other data types, like objects and arrays, require them.
Save the file and test the project. The input data should be available in the form field on a page reload or a later page visit.
Creating a custom React Hook to persist form inputs
Sometimes we might want to render and persist more form inputs, such as a text input and a checkbox input, in a different component.
While we could easily copy the logic from the one we’ve already created and use it in the new component, it’s not always practicable, especially if we decide to create more of these inputs.
Instead, React allows us to extract and share similar logic between components using custom Hooks.
In this section, we will learn how to create a custom Hook to persist form inputs in multiple components.
Let’s start by creating another form. In the src/components
folder, create a new file called Form2.js
, and add the following code:
import { useState } from "react";
const Form2 = () => {
const [name, setName] = useState("");
const [checked, setChecked] = useState(false);
return (
<form>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Full name"
aria-label="fullname"
/>
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>{" "}
Not a robot?
</label>
<input type="submit" value="Submit"></input>
</form>
);
};
export default Form2;
Then, import and use the component in the components/App.js
file:
// ...
import Form2 from "./Form2";
const App = () => {
return (
<div className="container">
{/* ... */}
<Form2 />
</div>
);
};
export default App;
Save the files and view the form in the frontend.
Interacting with this form does not persist the state value in localStorage
since we don’t have the logic yet.
So, let’s define a single logic to manage all our form inputs.
Extracting the localStorage
logic
To begin extracting the localStorage
logic, create a file called useLocalStorage.js
in the src
folder and add the following code:
import { useState, useEffect } from "react";
function getStorageValue(key, defaultValue) {
// getting stored value
const saved = localStorage.getItem(key);
const initial = JSON.parse(saved);
return initial || defaultValue;
}
export const useLocalStorage = (key, defaultValue) => {
const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue);
});
useEffect(() => {
// storing input name
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Taking a closer look at the code above, we’ve only extracted the storage logic from the components/Form1.js
file. We’ve done nothing special.
By creating a custom Hook called useLocalStorage
, we maintain all of the storage logic we have in the Form1
component.
The useLocalStorage
Hook expects two arguments: the key
and the defaultValue
. This means we expect to pass these values when calling the Hook in our different components.
Note that you can name your custom Hook anything, but ensure you start with use
.
Using the useLocalStorage
custom Hook
In the components/Form1.js
file, replace the logic above the return
statement with the custom Hook so you have the following:
import { useLocalStorage } from "../useLocalStorage";
const Form1 = () => {
const [name, setName] = useLocalStorage("name", "");
return (
<form>
{/* ... */}
</form>
);
};
export default Form1;
After importing the custom Hook, we can use it and pass along the unique key and the default value, which, in this case, is an empty string.
If we do the same for the Form2
component in the components/Form2js
file, we should have the following:
import { useLocalStorage } from "../useLocalStorage";
const Form2 = () => {
const [name, setName] = useLocalStorage("name2", "");
const [checked, setChecked] = useLocalStorage("checked", false);
return (
<form>
{/* ... */}
</form>
);
};
export default Form2;
Save all the files and test the project. We should be able to persist all the form inputs in localStorage
.
Good job!
Problems accessing localStorage
for a server-side rendered application
When working with a framework like Next.js that executes code on the server-side, using localStorage
gets an error stating, “window is not defined.”
The localStorage
as used in our code is a built-in property of the window
object, window.localStorage
.
In our code, we ignored the window
while accessing the localStorage
because it’s a global object; we can choose to include the window
object because it’s optional.
Now, this window
object is not available on the server side but rather the client side/browser, which prompts the error.
To fix the error on the server side, check whether the window
object is defined or not. This way, our code only runs on the environment where the window
is available.
Open the src/useLocalStorage.js
file and update the getStorageValue()
function so you have the following:
function getStorageValue(key, defaultValue) {
// getting stored value
if (typeof window !== "undefined") {
const saved = localStorage.getItem(key);
const initial = saved !== null ? JSON.parse(saved) : defaultValue;
return initial;
}
}
Don’t forget that we’ve also used the localStorage
inside the useEffect
Hook in the useLocalStorage.js
file.
But in this case, the localStorage
is safe because the useEffect
Hook only runs on the client-side where we have access to the window
object.
Test the project to ensure everything still works as expected.
Conclusion
We’ve covered how to use the localStorage
to persist data in a browser using the React Hooks. We’ve also learned how to create a custom Hook to extract component logic into reusable functions.
If you enjoyed this guide, share it around the web. And, if you have any questions or contributions, please share them via the comment section.
Find the entire source code for the project here.
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Top comments (0)