In one of my previous articles, I have explained how to use react context for state management. In this tutorial, we will see how to access local storage and use it to store the application state.
We will be building an application to input the name of the user,
then we will ask them to choose their favorite fruits and save them in the local storage.
When the user visits the page next time, we will display their favorite fruits and will provide them with an option to change them.
Setting up the project
First, let's create a react app using the following command:
npx create-react-app react-local-storage
We will be using BluePrintJS to style the application
so that we don't have to worry about the styling part and we can focus on the logic.
Run the following command to install BluePrintJS:
yarn add @blueprintjs/core
Now, let's import the stylesheet files related to BluePrintJS in index.css
and we will add some basic styling:
@import "~normalize.css";
@import "~@blueprintjs/core/lib/css/blueprint.css";
@import "~@blueprintjs/icons/lib/css/blueprint-icons.css";
body {
margin: 10px auto;
max-width: 400px;
}
.space {
margin: 5px 5px 10px 5px;
}
Building the App
In App.js let's create a form with input box to enter name and a submit button:
import { Button, Card, FormGroup, InputGroup } from "@blueprintjs/core"
import { useState } from "react"
function App() {
const [name, setName] = useState("")
const formSubmitHandler = e => {
e.preventDefault()
}
return (
<div>
<Card elevation="1">
<form onSubmit={formSubmitHandler}>
<FormGroup label="Name" labelFor="name">
<InputGroup
id="Name"
placeholder="Name"
type="Name"
value={name}
onChange={e => setName(e.target.value)}
/>
</FormGroup>
<Button intent="primary" text="Submit" fill type="submit" />
</form>
</Card>
</div>
)
}
export default App
Here we are storing the name in the local storage and we are not doing anything when the form is submitted.
Let's save the name to the local storage in the formSubmitHandler
function:
const formSubmitHandler = e => {
e.preventDefault()
try {
window.localStorage.setItem("user", JSON.stringify({ name, favorites: [] }))
} catch (error) {
console.log(error)
}
}
Here,
- We are converting the Javascript object to string since local storage can store only string values.
- We are also setting an empty array named
favorites
, which will be used later to store user's favorite fruits. - We have enclosed the code within a try-catch block since accessing localStorage may cause an exception if the localStorage is not supported or the user has blocked access to it.
If you run the application and submit the form, you will be able to see the name getting saved in the localStorage:
Displaying the options
Since we have saved the name to the localStorage, let's have a list of fruits and display it to the user.
import {
Button,
Card,
Checkbox,
FormGroup,
InputGroup,
} from "@blueprintjs/core"
import { useEffect, useState } from "react"
const fruits = [
"Apple",
"Orange",
"Guava",
"Mango",
"Grapes",
"Kiwi",
"Strawberry",
]
function App() {
const [name, setName] = useState("")
const [userData, setUserData] = useState()
const [editMode, setEditMode] = useState(false)
useEffect(() => {
// Fetch the user data from the localStorage and set it to the local state userData
try {
const user = window.localStorage.getItem("user")
if (!user) {
setUserData(null)
} else {
const parsedData = JSON.parse(user)
setUserData(parsedData)
if (parsedData.favorites.length === 0) {
setEditMode(true)
}
}
} catch (error) {
console.log(error)
setUserData(null)
}
}, [])
const onFruitChecked = (e, fruit) => {
// Check if the fruit exists in the current list of favorites
const index = userData.favorites.indexOf(fruit)
// If the checkbox is checked and fruit is not part of favorites
if (e.target.checked && index === -1) {
setUserData(prevValues => {
// Add the fruit to the current list of favorites
return { ...prevValues, favorites: [...prevValues.favorites, fruit] }
})
} else if (!e.target.checked && index !== -1) {
// If the checkbox is unchecked and fruit is part of favorites
setUserData(prevValues => {
// Remove the fruit from the current list of favorites
return {
...prevValues,
favorites: [
...prevValues.favorites.slice(0, index),
...prevValues.favorites.slice(index + 1),
],
}
})
}
}
const formSubmitHandler = e => {
e.preventDefault()
try {
setUserData({ name, favorites: [] })
setEditMode(true)
window.localStorage.setItem(
"user",
JSON.stringify({ name, favorites: [] })
)
} catch (error) {
console.log(error)
}
}
return (
<div>
{userData === null && (
<Card elevation="1">
<form onSubmit={formSubmitHandler}>
<FormGroup label="Name" labelFor="name">
<InputGroup
id="Name"
placeholder="Name"
type="Name"
value={name}
onChange={e => setName(e.target.value)}
/>
</FormGroup>
<Button intent="primary" text="Submit" fill type="submit" />
</form>
</Card>
)}
{userData && editMode && (
<Card elevation="1">
<p>
Welcome <strong>{userData.name}</strong>, choose your favorite
fruits:
</p>
{fruits.map(fruit => {
return (
<Checkbox
key={fruit}
label={fruit}
inline={true}
className="space"
checked={userData.favorites.indexOf(fruit) !== -1}
onChange={e => {
onFruitChecked(e, fruit)
}}
/>
)
})}
<Button intent="primary" text="Save" fill type="submit" />
</Card>
)}
</div>
)
}
export default App
In the above code,
- We have an effect where we fetch the user data from the local storage, pretty much similar to how we stored it. Since the data is stored in string format, we are converting it back to Javascript object.
If you want to learn more about useEffect,
I have written a comprehensive article on how to use useEffect in React
- We have introduced 2 additional local states,
one to store the user data and another to store a boolean value called
editMode
, which will be used later to toggle between the display and edit screens. - In the
formSubmitHandler
function, we are updating the user data andeditMode
so that the user will be switched to edit mode once they submit their name. - While rendering the application, we are checking if the
userData
isnull
, i.e., if user is visiting the page for the first time, then show them the form to submit the name otherwise allow them to choose their favorite fruits. - We have a function called
onFruitChecked
, which updates the currently selected favorite fruits when the user checks or un-checks them.
Now if you run the application, it will present you with options as shown below:
Saving the choices and displaying them
Now we have the user-selected choices in userData.favorites
array. Let's save it to the localStorage.
import {
Button,
Card,
Checkbox,
FormGroup,
InputGroup,
Tag,
} from "@blueprintjs/core"
import { useEffect, useState } from "react"
const fruits = [
"Apple",
"Orange",
"Guava",
"Mango",
"Grapes",
"Kiwi",
"Strawberry",
]
function App() {
const [name, setName] = useState("")
const [userData, setUserData] = useState()
const [editMode, setEditMode] = useState(false)
useEffect(() => {
// Fetch the user data from the localStorage and set it to the local state userData
try {
const user = window.localStorage.getItem("user")
if (!user) {
setUserData(null)
} else {
const parsedData = JSON.parse(user)
setUserData(parsedData)
if (parsedData.favorites.length === 0) {
setEditMode(true)
}
}
} catch (error) {
console.log(error)
setUserData(null)
}
}, [])
const onFruitChecked = (e, fruit) => {
// Check if the fruit exists in the current list of favorites
const index = userData.favorites.indexOf(fruit)
// If the checkbox is checked and fruit is not part of favorites
if (e.target.checked && index === -1) {
setUserData(prevValues => {
// Add the fruit to the current list of favorites
return { ...prevValues, favorites: [...prevValues.favorites, fruit] }
})
} else if (!e.target.checked && index !== -1) {
// If the checkbox is unchecked and fruit is part of favorites
setUserData(prevValues => {
// Remove the fruit from the current list of favorites
return {
...prevValues,
favorites: [
...prevValues.favorites.slice(0, index),
...prevValues.favorites.slice(index + 1),
],
}
})
}
}
const formSubmitHandler = e => {
e.preventDefault()
try {
setUserData({ name, favorites: [] })
setEditMode(true)
window.localStorage.setItem(
"user",
JSON.stringify({ name, favorites: [] })
)
} catch (error) {
console.log(error)
}
}
const saveFavorites = () => {
try {
window.localStorage.setItem("user", JSON.stringify(userData))
setEditMode(false)
} catch (error) {
console.log(error)
}
}
return (
<div>
{userData === null && (
<Card elevation="1">
<form onSubmit={formSubmitHandler}>
<FormGroup label="Name" labelFor="name">
<InputGroup
id="Name"
placeholder="Name"
type="Name"
value={name}
onChange={e => setName(e.target.value)}
/>
</FormGroup>
<Button intent="primary" text="Submit" fill type="submit" />
</form>
</Card>
)}
{userData &&
(editMode ? (
<Card elevation="1">
<p>
Welcome <strong>{userData.name}</strong>, choose your favorite
fruits:
</p>
{fruits.map(fruit => {
return (
<Checkbox
key={fruit}
label={fruit}
inline={true}
className="space"
checked={userData.favorites.indexOf(fruit) !== -1}
onChange={e => {
onFruitChecked(e, fruit)
}}
/>
)
})}
<Button
intent="primary"
text="Save"
fill
type="submit"
onClick={saveFavorites}
/>
</Card>
) : (
<Card elevation="1">
<p>
Welcome <strong>{userData.name}</strong>, your favorite fruits
are:
</p>
{userData.favorites.map(fruit => {
return (
<Tag
key={fruit}
round
minimal
large
intent="success"
className="space"
>
{fruit}
</Tag>
)
})}
<Button
intent="primary"
text="Change"
fill
type="submit"
onClick={() => setEditMode(true)}
/>
</Card>
))}
</div>
)
}
export default App
In the above code,
- We have added a function called
saveFavorites
, which saves the favorites to the localStorage and sets theeditMode
tofalse
. - We are displaying the favorite fruits inside nice little tags.
- We have also given an option to go back to edit mode, to update the favorites.
If we run the application now, you will see the favorite fruits getting saved in the localStorage.
If you refresh the page, you will see the data is persisted.
Creating useLocalStorage hook
You might have observed that we are accessing the local storage in multiple places.
Let's create a hook so that we can separate it into a file and it can act as a utility function.
Create a folder named hooks
and a file called useLocalStorage.js
inside it with the following code:
import { useState } from "react"
const useLocalStorage = (key, initialValue) => {
const [state, setState] = useState(() => {
// Initialize the state
try {
const value = window.localStorage.getItem(key)
// Check if the local storage already has any values,
// otherwise initialize it with the passed initialValue
return value ? JSON.parse(value) : initialValue
} catch (error) {
console.log(error)
}
})
const setValue = value => {
try {
// If the passed value is a callback function,
// then call it with the existing state.
const valueToStore = value instanceof Function ? value(state) : value
window.localStorage.setItem(key, JSON.stringify(valueToStore))
setState(value)
} catch (error) {
console.log(error)
}
}
return [state, setValue]
}
export default useLocalStorage
In the above hook,
- We have a local state to store the localStorage data, which has a initialize function, which checks if a value corresponding to the passed key exists. If it exists, then it initializes the state with the data from the local storage. Otherwise, it sets the value to the initial value, which is passed to the hook.
- We have
setValue
function, which checks if the passed value is a callback function. If it is a callback function, then it calls it with the existing state and updates the response of the callback to the state and localStorage. - Finally, we return both the
state
as well as thesetValue
, similar to that of auseState
hook.
Let's now use the newly created hook in App.js
:
import {
Button,
Card,
Checkbox,
FormGroup,
InputGroup,
Tag,
} from "@blueprintjs/core"
import { useState } from "react"
import useLocalStorage from "./hooks/useLocalStorage"
const fruits = [
"Apple",
"Orange",
"Guava",
"Mango",
"Grapes",
"Kiwi",
"Strawberry",
]
function App() {
const [name, setName] = useState("")
const [userData, setUserData] = useLocalStorage("user", null)
// Set edit mode to true whenever the userData is not present or
// selected favorites are 0
const [editMode, setEditMode] = useState(
userData === null || userData?.favorites?.length === 0
)
const onFruitChecked = (e, fruit) => {
// Check if the fruit exists in the current list of favorites
const index = userData.favorites.indexOf(fruit)
// If the checkbox is checked and fruit is not part of favorites
if (e.target.checked && index === -1) {
setUserData(prevValues => {
// Add the fruit to the current list of favorites
return { ...prevValues, favorites: [...prevValues.favorites, fruit] }
})
} else if (!e.target.checked && index !== -1) {
// If the checkbox is unchecked and fruit is part of favorites
setUserData(prevValues => {
// Remove the fruit from the current list of favorites
return {
...prevValues,
favorites: [
...prevValues.favorites.slice(0, index),
...prevValues.favorites.slice(index + 1),
],
}
})
}
}
const formSubmitHandler = e => {
e.preventDefault()
try {
setUserData({ name, favorites: [] })
setEditMode(true)
} catch (error) {
console.log(error)
}
}
return (
<div>
{userData === null && (
<Card elevation="1">
<form onSubmit={formSubmitHandler}>
<FormGroup label="Name" labelFor="name">
<InputGroup
id="Name"
placeholder="Name"
type="Name"
value={name}
onChange={e => setName(e.target.value)}
/>
</FormGroup>
<Button intent="primary" text="Submit" fill type="submit" />
</form>
</Card>
)}
{userData &&
(editMode ? (
<Card elevation="1">
<p>
Welcome <strong>{userData.name}</strong>, choose your favorite
fruits:
</p>
{fruits.map(fruit => {
return (
<Checkbox
key={fruit}
label={fruit}
inline={true}
className="space"
checked={userData.favorites.indexOf(fruit) !== -1}
onChange={e => {
onFruitChecked(e, fruit)
}}
/>
)
})}
<Button
intent="primary"
text="Done"
fill
type="submit"
onClick={() => setEditMode(false)}
/>
</Card>
) : (
<Card elevation="1">
<p>
Welcome <strong>{userData.name}</strong>, your favorite fruits
are:
</p>
{userData.favorites.map(fruit => {
return (
<Tag
key={fruit}
round
minimal
large
intent="success"
className="space"
>
{fruit}
</Tag>
)
})}
<Button
intent="primary"
text="Change"
fill
type="submit"
onClick={() => setEditMode(true)}
/>
</Card>
))}
</div>
)
}
export default App
As you may see, we are using the useLocalStorage
in a similar way we use useState
hook and we got rid of the useEffect
and saveFavorites
(since checking on the checkbox itself saves it to the localStorage) functions.
Top comments (0)