Table of Contents
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:
1npx 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:
1yarn add @blueprintjs/core
Now, let's import the stylesheet files related to BluePrintJS in index.css
and we will add some basic styling:
1@import "~normalize.css";2@import "~@blueprintjs/core/lib/css/blueprint.css";3@import "~@blueprintjs/icons/lib/css/blueprint-icons.css";45body {6 margin: 10px auto;7 max-width: 400px;8}9.space {10 margin: 5px 5px 10px 5px;11}
Building the App
In App.js let's create a form with input box to enter name and a submit button:
1import { Button, Card, FormGroup, InputGroup } from "@blueprintjs/core"2import { useState } from "react"34function App() {5 const [name, setName] = useState("")67 const formSubmitHandler = e => {8 e.preventDefault()9 }10 return (11 <div>12 <Card elevation="1">13 <form onSubmit={formSubmitHandler}>14 <FormGroup label="Name" labelFor="name">15 <InputGroup16 id="Name"17 placeholder="Name"18 type="Name"19 value={name}20 onChange={e => setName(e.target.value)}21 />22 </FormGroup>23 <Button intent="primary" text="Submit" fill type="submit" />24 </form>25 </Card>26 </div>27 )28}2930export 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:
1const formSubmitHandler = e => {2 e.preventDefault()3 try {4 window.localStorage.setItem("user", JSON.stringify({ name, favorites: [] }))5 } catch (error) {6 console.log(error)7 }8}
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.
1import {2 Button,3 Card,4 Checkbox,5 FormGroup,6 InputGroup,7} from "@blueprintjs/core"8import { useEffect, useState } from "react"910const fruits = [11 "Apple",12 "Orange",13 "Guava",14 "Mango",15 "Grapes",16 "Kiwi",17 "Strawberry",18]1920function App() {21 const [name, setName] = useState("")22 const [userData, setUserData] = useState()23 const [editMode, setEditMode] = useState(false)2425 useEffect(() => {26 // Fetch the user data from the localStorage and set it to the local state userData27 try {28 const user = window.localStorage.getItem("user")2930 if (!user) {31 setUserData(null)32 } else {33 const parsedData = JSON.parse(user)34 setUserData(parsedData)35 if (parsedData.favorites.length === 0) {36 setEditMode(true)37 }38 }39 } catch (error) {40 console.log(error)41 setUserData(null)42 }43 }, [])4445 const onFruitChecked = (e, fruit) => {46 // Check if the fruit exists in the current list of favorites47 const index = userData.favorites.indexOf(fruit)48 // If the checkbox is checked and fruit is not part of favorites49 if (e.target.checked && index === -1) {50 setUserData(prevValues => {51 // Add the fruit to the current list of favorites52 return { ...prevValues, favorites: [...prevValues.favorites, fruit] }53 })54 } else if (!e.target.checked && index !== -1) {55 // If the checkbox is unchecked and fruit is part of favorites56 setUserData(prevValues => {57 // Remove the fruit from the current list of favorites58 return {59 ...prevValues,60 favorites: [61 ...prevValues.favorites.slice(0, index),62 ...prevValues.favorites.slice(index + 1),63 ],64 }65 })66 }67 }6869 const formSubmitHandler = e => {70 e.preventDefault()71 try {72 setUserData({ name, favorites: [] })73 setEditMode(true)74 window.localStorage.setItem(75 "user",76 JSON.stringify({ name, favorites: [] })77 )78 } catch (error) {79 console.log(error)80 }81 }8283 return (84 <div>85 {userData === null && (86 <Card elevation="1">87 <form onSubmit={formSubmitHandler}>88 <FormGroup label="Name" labelFor="name">89 <InputGroup90 id="Name"91 placeholder="Name"92 type="Name"93 value={name}94 onChange={e => setName(e.target.value)}95 />96 </FormGroup>97 <Button intent="primary" text="Submit" fill type="submit" />98 </form>99 </Card>100 )}101 {userData && editMode && (102 <Card elevation="1">103 <p>104 Welcome <strong>{userData.name}</strong>, choose your favorite105 fruits:106 </p>107 {fruits.map(fruit => {108 return (109 <Checkbox110 key={fruit}111 label={fruit}112 inline={true}113 className="space"114 checked={userData.favorites.indexOf(fruit) !== -1}115 onChange={e => {116 onFruitChecked(e, fruit)117 }}118 />119 )120 })}121 <Button intent="primary" text="Save" fill type="submit" />122 </Card>123 )}124 </div>125 )126}127128export 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.
1import {2 Button,3 Card,4 Checkbox,5 FormGroup,6 InputGroup,7 Tag,8} from "@blueprintjs/core"9import { useEffect, useState } from "react"1011const fruits = [12 "Apple",13 "Orange",14 "Guava",15 "Mango",16 "Grapes",17 "Kiwi",18 "Strawberry",19]2021function App() {22 const [name, setName] = useState("")23 const [userData, setUserData] = useState()24 const [editMode, setEditMode] = useState(false)2526 useEffect(() => {27 // Fetch the user data from the localStorage and set it to the local state userData28 try {29 const user = window.localStorage.getItem("user")3031 if (!user) {32 setUserData(null)33 } else {34 const parsedData = JSON.parse(user)35 setUserData(parsedData)36 if (parsedData.favorites.length === 0) {37 setEditMode(true)38 }39 }40 } catch (error) {41 console.log(error)42 setUserData(null)43 }44 }, [])4546 const onFruitChecked = (e, fruit) => {47 // Check if the fruit exists in the current list of favorites48 const index = userData.favorites.indexOf(fruit)49 // If the checkbox is checked and fruit is not part of favorites50 if (e.target.checked && index === -1) {51 setUserData(prevValues => {52 // Add the fruit to the current list of favorites53 return { ...prevValues, favorites: [...prevValues.favorites, fruit] }54 })55 } else if (!e.target.checked && index !== -1) {56 // If the checkbox is unchecked and fruit is part of favorites57 setUserData(prevValues => {58 // Remove the fruit from the current list of favorites59 return {60 ...prevValues,61 favorites: [62 ...prevValues.favorites.slice(0, index),63 ...prevValues.favorites.slice(index + 1),64 ],65 }66 })67 }68 }6970 const formSubmitHandler = e => {71 e.preventDefault()72 try {73 setUserData({ name, favorites: [] })74 setEditMode(true)75 window.localStorage.setItem(76 "user",77 JSON.stringify({ name, favorites: [] })78 )79 } catch (error) {80 console.log(error)81 }82 }8384 const saveFavorites = () => {85 try {86 window.localStorage.setItem("user", JSON.stringify(userData))87 setEditMode(false)88 } catch (error) {89 console.log(error)90 }91 }9293 return (94 <div>95 {userData === null && (96 <Card elevation="1">97 <form onSubmit={formSubmitHandler}>98 <FormGroup label="Name" labelFor="name">99 <InputGroup100 id="Name"101 placeholder="Name"102 type="Name"103 value={name}104 onChange={e => setName(e.target.value)}105 />106 </FormGroup>107 <Button intent="primary" text="Submit" fill type="submit" />108 </form>109 </Card>110 )}111 {userData &&112 (editMode ? (113 <Card elevation="1">114 <p>115 Welcome <strong>{userData.name}</strong>, choose your favorite116 fruits:117 </p>118 {fruits.map(fruit => {119 return (120 <Checkbox121 key={fruit}122 label={fruit}123 inline={true}124 className="space"125 checked={userData.favorites.indexOf(fruit) !== -1}126 onChange={e => {127 onFruitChecked(e, fruit)128 }}129 />130 )131 })}132 <Button133 intent="primary"134 text="Save"135 fill136 type="submit"137 onClick={saveFavorites}138 />139 </Card>140 ) : (141 <Card elevation="1">142 <p>143 Welcome <strong>{userData.name}</strong>, your favorite fruits144 are:145 </p>146 {userData.favorites.map(fruit => {147 return (148 <Tag149 key={fruit}150 round151 minimal152 large153 intent="success"154 className="space"155 >156 {fruit}157 </Tag>158 )159 })}160 <Button161 intent="primary"162 text="Change"163 fill164 type="submit"165 onClick={() => setEditMode(true)}166 />167 </Card>168 ))}169 </div>170 )171}172173export 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:
1import { useState } from "react"23const useLocalStorage = (key, initialValue) => {4 const [state, setState] = useState(() => {5 // Initialize the state6 try {7 const value = window.localStorage.getItem(key)8 // Check if the local storage already has any values,9 // otherwise initialize it with the passed initialValue10 return value ? JSON.parse(value) : initialValue11 } catch (error) {12 console.log(error)13 }14 })1516 const setValue = value => {17 try {18 // If the passed value is a callback function,19 // then call it with the existing state.20 const valueToStore = value instanceof Function ? value(state) : value21 window.localStorage.setItem(key, JSON.stringify(valueToStore))22 setState(value)23 } catch (error) {24 console.log(error)25 }26 }2728 return [state, setValue]29}3031export 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
:
1import {2 Button,3 Card,4 Checkbox,5 FormGroup,6 InputGroup,7 Tag,8} from "@blueprintjs/core"9import { useState } from "react"10import useLocalStorage from "./hooks/useLocalStorage"1112const fruits = [13 "Apple",14 "Orange",15 "Guava",16 "Mango",17 "Grapes",18 "Kiwi",19 "Strawberry",20]2122function App() {23 const [name, setName] = useState("")24 const [userData, setUserData] = useLocalStorage("user", null)25 // Set edit mode to true whenever the userData is not present or26 // selected favorites are 027 const [editMode, setEditMode] = useState(28 userData === null || userData?.favorites?.length === 029 )3031 const onFruitChecked = (e, fruit) => {32 // Check if the fruit exists in the current list of favorites33 const index = userData.favorites.indexOf(fruit)34 // If the checkbox is checked and fruit is not part of favorites35 if (e.target.checked && index === -1) {36 setUserData(prevValues => {37 // Add the fruit to the current list of favorites38 return { ...prevValues, favorites: [...prevValues.favorites, fruit] }39 })40 } else if (!e.target.checked && index !== -1) {41 // If the checkbox is unchecked and fruit is part of favorites42 setUserData(prevValues => {43 // Remove the fruit from the current list of favorites44 return {45 ...prevValues,46 favorites: [47 ...prevValues.favorites.slice(0, index),48 ...prevValues.favorites.slice(index + 1),49 ],50 }51 })52 }53 }5455 const formSubmitHandler = e => {56 e.preventDefault()57 try {58 setUserData({ name, favorites: [] })59 setEditMode(true)60 } catch (error) {61 console.log(error)62 }63 }6465 return (66 <div>67 {userData === null && (68 <Card elevation="1">69 <form onSubmit={formSubmitHandler}>70 <FormGroup label="Name" labelFor="name">71 <InputGroup72 id="Name"73 placeholder="Name"74 type="Name"75 value={name}76 onChange={e => setName(e.target.value)}77 />78 </FormGroup>79 <Button intent="primary" text="Submit" fill type="submit" />80 </form>81 </Card>82 )}83 {userData &&84 (editMode ? (85 <Card elevation="1">86 <p>87 Welcome <strong>{userData.name}</strong>, choose your favorite88 fruits:89 </p>90 {fruits.map(fruit => {91 return (92 <Checkbox93 key={fruit}94 label={fruit}95 inline={true}96 className="space"97 checked={userData.favorites.indexOf(fruit) !== -1}98 onChange={e => {99 onFruitChecked(e, fruit)100 }}101 />102 )103 })}104 <Button105 intent="primary"106 text="Done"107 fill108 type="submit"109 onClick={() => setEditMode(false)}110 />111 </Card>112 ) : (113 <Card elevation="1">114 <p>115 Welcome <strong>{userData.name}</strong>, your favorite fruits116 are:117 </p>118 {userData.favorites.map(fruit => {119 return (120 <Tag121 key={fruit}122 round123 minimal124 large125 intent="success"126 className="space"127 >128 {fruit}129 </Tag>130 )131 })}132 <Button133 intent="primary"134 text="Change"135 fill136 type="submit"137 onClick={() => setEditMode(true)}138 />139 </Card>140 ))}141 </div>142 )143}144145export 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.
Demo and Source Code
You can download the source code here and view a demo here.
Do follow me on twitter where I post developer insights more often!
Leave a Comment