Hooks in React have been available since the version 16.7.0-alpha. They are functions that allow you to use React state and a component's lifecycle methods in a functional component. Hooks do not work with classes. If you are familiar with React, you know that the functional component has been called as a functional stateless component. Not any more.
Since previously, only a class component allowed you to have a local state. Using Hooks, you do not have to refactor a class component when using React or React Native into a functional component only because you want to introduce local state or lifecycle methods in that component. In other words, Hooks allow us to write apps in React with functional components.
In this tutorial, you are going to build a small Expense Tracker app that using React Hooks. Furthermore, to add real-time functionality, you are going to learn how to use localStorage
API in a React application.
What are we building?
Here is a demo of how the end result of this tutorial will look like.
Table of Contents
- Requirements
- Setup Bootstrap
- Implementing Hooks in a React app
- Building the Expense Tracker App
- Add input fields
- Add a list to display expenses
- Handling controlled input fields
- Handling Form submission
- Adding localStorage API to persist data
- Adding side-effects
- Deleting all items from the list
- Conclusion
Requirements
In order to follow this tutorial, you are required to have the following installed on your dev machine:
- NodeJS above
10.x.x
installed on your local machine - Know, how to run simple npm/yarn commands
- JavaScript/ES6 and React basics
Setup Bootstrap
Bootstrap 4 uses flexbox
to handle the layout of a web app. In order to get started using Bootstrap in a React app, you have first to create a react app.
npx create-react-app react-expense-tracker
Next, install the following libraries to add Bootstrap. I am going to use reactstrap that offers built-in Bootstrap 4 components. Reactstrap does not include Bootstrap CSS, so it needs to be installed.
yarn add bootstrap reactstrap
After installing these dependencies, open the React project you created and open the file index.js
. Add an import statement to include Bootstrap CSS file.
// after other imports
import 'bootstrap/dist/css/bootstrap.min.css'
The last step is to test and verify that reactstrap
components are available to the current React app. Let us add a simple button. Open App.js
file and import both Container
and Button
components from reactstrap
. A Container
is a layout component.
import React from 'react'
import { Button, Container } from 'reactstrap'
function App() {
return (
<Container style={{ marginTop: 20 }}>
<Button color="success">Let's start</Button>
</Container>
)
}
export default App
Now, go back to the terminal window and run yarn start
. You will see similar results in the browser window on the URL http://localhost:3000
.
That's it for setting up Bootstrap 4 in a React app.
Implementing Hooks in a React App
In this section, you are going to learn how to use useState
hook to define an initial state to a React component. Open App.js
file and start by importing useState
from React core. All built-in hooks can be imported this way.
import React, { useState } from 'react'
The useState
hook returns two values in an array. The first value is the current value of the state object and the second value in the array the function to update the state value fo the first. This why the second value starts with a conventional prefix fo set
. Although you can make it anything but following conventions that are commonly used in the React world is a good practice to follow.
Hooks are always called at the top level of function. Meaning when defining a state, they must be the first thing in the function, especially before returning a JSX. Let us implement a classic example of incrementing and decrementing an initial value of 0
. Inside the App
function, define the following.
const [count, setCount] = useState(0)
React preserves this state between all the re-rendering happening. useState()
hook also takes a single argument that represents the initial state. Here is the code of the complete App
function.
function App() {
const [count, setCount] = useState(0)
return (
<Container style={{ marginTop: 20 }}>
<p className="text-primary">You clicked {count} times.</p>
<Button onClick={() => setCount(count + 1)} color="success">
Increase the count
</Button> <Button onClick={() => setCount(count - 1)} color="danger">
Decrease the count
</Button>
</Container>
)
}
Make sure that yarn start
is running and go the browser window to see this component in action. Click on any of the button to increase or decrease the count's value.
It works!
Building the Expense Tracker App
The Expense Tracker React application you are going to build going to contain two input fields that will contain the expense cause or the name of the expense and amount of that expense. It will also be going to show the total amount of all expenses below a list of individual expenses. These are the main functionalities you have to implement first.
To get started, let us define an initial array that is going to store the value of each expense and the name or the title of the expense. Then, using this array with useState
you can render to the total amount of all expenses. Open App.js
file and define an object ALL_EXPENSES
as below.
const ALL_EXPENSES = [
{ id: 1, name: 'Buy a book', amount: 20 },
{ id: 2, name: 'Buy a milk', amount: 5 },
{ id: 3, name: 'Book a flight ticket', amount: 225 }
]
The App
function is going to be simple since there are now handler functions to modify or add a new expense for now. Define the state expenses
with its initial value being all the expenses stored in ALL_EXPENSES
. Then, using array.reduce
, you calculate the sum of all the expenses.
import React, { useState } from 'react'
import { Jumbotron, Container } from 'reactstrap'
import Logo from './logo.svg'
const ALL_EXPENSES = [
{ id: 1, name: 'Buy a book', amount: 20 },
{ id: 2, name: 'Buy a milk', amount: 5 },
{ id: 3, name: 'Book a flight ticket', amount: 225 }
]
function App() {
const [expenses, setExpenses] = useState(ALL_EXPENSES)
return (
<Container className="text-center">
<Jumbotron fluid>
<h3 className="display-6">
Expense Tracker React App
<img src={Logo} style={{ width: 50, height: 50 }} alt="react-logo" />
</h3>
<div>
<p>
Total Expense:{' '}
<span className="text-success">
${' '}
{expenses.reduce((accumulator, currentValue) => {
return (accumulator += parseInt(currentValue.amount))
}, 0)}
</span>
</p>
</div>
</Jumbotron>
</Container>
)
}
export default App
The reduce()
method executes a function to output a single value from all the individual values from an array. In the current case, you have to calculate the total sum of all the amount
in ALL_EXPENSES
array. This method executes a callback that takes two arguments once for each assigned value present in the array.
The first argument, accumulator
returns the value of the previous invocation of the callback. If the callback hasn't invoked yet, provide an initial value (in the current scenario) such that the accumulator
will be equal to it on the first iteration. On the initial run of the callback, the currentValue
is going to be equal to the first value of the array. As this callback will run for each of value in the array, on the second iteration, the accumulator
is equal to the currentValue
of the first or initial iteration. That is going to be the first value in the array. Also, on the second iteration, the currentValue
will be equal to the second value in the array. The process continues. To read more about how reduce()
works, visit this MDN web docs.
Now, if you go to the browser window, you will get the following result.
Add input fields
The application currently requires two input fields and a button. Both the input field are going to represent the name of the expense and the amount of each expense. The button is going to add these expenses to the list of all expenses (which still needs to be created). Let us set up a new component inside components
within a new file called Form
.
import React from 'react'
import {
Form as BTForm,
FormGroup,
Input,
Label,
Col,
Button
} from 'reactstrap'
const Form = () => (
<BTForm style={{ margin: 10 }}>
<FormGroup className="row">
<Label for="exampleEmail" sm={2}>
Name of Expense
</Label>
<Col sm={4}>
<Input
type="text"
name="name"
id="expenseName"
placeholder="Name of expense?"
/>
</Col>
</FormGroup>
<FormGroup className="row">
<Label for="exampleEmail" sm={2}>
$ Amount
</Label>
<Col sm={4}>
<Input
type="number"
name="amount"
id="expenseAmount"
placeholder="0.00"
/>
</Col>
</FormGroup>
<Button type="submit" color="primary">
Add
</Button>
</BTForm>
)
export default Form
You will notice how helpful UI library such as reactstrap
is going to be at the end of this section to serve the purpose of displaying a form, rather than adding your own CSS. Also, note that both the input fields are of a different type. The name of the expense is of type text
whereas the amount of the expense is type number
.
Import this component in the App.js
file.
// after other imports...
import Form from './components/Form'
function App() {
const [expenses, setExpenses] = useState(ALL_EXPENSES)
return (
<Container>
<Jumbotron fluid>
<h3 className='display-6 text-center'>
Expense Tracker React App
<img src={Logo} style={{ width: 50, height: 50 }} alt='react-logo' />
</h3>
<div className='text-center'>
<p>
Total Expense:{' '}
<span className='text-success'>
${' '}
{expenses.reduce((accumulator, currentValue) => {
return (accumulator += parseInt(currentValue.amount))
}, 0)}
</span>
</p>
</div>
{*/ ADD THE BELOW LINE/*}
<Form />
</Jumbotron>
</Container>
)
}
export default App
In the browser window, you will get the following result.
Add a list to display expenses
Let us setup another component that is going to display a list of expense items with their corresponding amount. To display the item in the list, items from array ALL_EXPENSES
are going to be used since it will serve as some mock data for now.
Create a file List.js
and use UI components ListGroup
and ListGroupItem
to create an unordered list.
import React from 'react'
import { ListGroup, ListGroupItem } from 'reactstrap'
const List = ({ expenses }) => (
<div>
<ListGroup>
{expenses.map(item => (
<ListGroupItem key={item.id}>
{item.name} - $ {item.amount}
</ListGroupItem>
))}
</ListGroup>
</div>
)
export default List
Import this component in App.js
file. In the above snippet, you will notice that it accepts one prop: expenses
. This refers to the ALL_EXPENSES
array from the initial value of useState
hook.
// after other impors
import List from './components/List'
function App() {
const [expenses, setExpenses] = useState(ALL_EXPENSES)
return (
<Container>
<Jumbotron fluid>
<h3 className='display-6' className='text-center'>
Expense Tracker React App
<img src={Logo} style={{ width: 50, height: 50 }} alt='react-logo' />
</h3>
<div className='text-center'>
<p>
Total Expense:{' '}
<span className='text-success'>
${' '}
{expenses.reduce((accumulator, currentValue) => {
return (accumulator += parseInt(currentValue.amount))
}, 0)}
</span>
</p>
</div>
<Form />
{*/ ADD THE BELOW LINE/*}
<List expenses={expenses} />
</Jumbotron>
</Container>
)
}
Visiting the browser window will yield the following list.
Handling controlled input fields with Hooks
In this section, let us manage to convert both the static input fields which are as of right now, of no use, into usable controlled input fields. A controlled input field accepts its current value as a prop as well as a callback to change that value.
Of course, you are going to use Hooks to do this. Add the following initial state for name
and amount
use useState()
inside App
component. Both of them are going to have an empty string as their initial values.
const [name, setName] = useState('')
const [amount, setAmount] = useState('')
To update their values when a user starts typing, add the following handler methods. Both of these functions are going to retrieve the value from the corresponding field. The console
statements are for testing purpose.
const handleName = event => {
console.log('Name ', event.target.value)
setName(event.target.value)
}
const handleAmount = event => {
console.log('Amount ', event.target.value)
setAmount(event.target.value)
}
Lastly, to submit the form, there is going to be another handler method called handleSubmitForm
.
const handleSubmitForm = event => {
event.preventDefault()
// do something when submitting the form
}
Right now, it doesn't have business logic to add the expense to the list. It is just preventing the form from refreshing the whole page in submission using event.preventDefault()
.
All of these have to be passed as props to the Form
component. Modify it.
<Form
name={name}
amount={amount}
handleName={handleName}
handleAmount={handleAmount}
handleSubmitForm={handleSubmitForm}
/>
Next, open Form.js
file and destructor the props as well as update both the input fields with attributes such as value
and onChange
method.
Here is how the modified Form.js
component looks like.
import React from 'react'
import {
Form as BTForm,
FormGroup,
Input,
Label,
Col,
Button
} from 'reactstrap'
const Form = ({ name, amount, handleName, handleAmount, handleSubmitForm }) => (
<BTForm style={{ margin: 10 }} onSubmit={handleSubmitForm}>
<FormGroup className="row">
<Label for="exampleEmail" sm={2}>
Name of Expense
</Label>
<Col sm={4}>
<Input
type="text"
name="name"
id="expenseName"
placeholder="Name of expense?"
value={name}
onChange={handleName}
/>
</Col>
</FormGroup>
<FormGroup className="row">
<Label for="exampleEmail" sm={2}>
$ Amount
</Label>
<Col sm={4}>
<Input
type="number"
name="amount"
id="expenseAmount"
placeholder="0.00"
value={amount}
onChange={handleAmount}
/>
</Col>
</FormGroup>
<Button type="submit" color="primary">
Add
</Button>
</BTForm>
)
export default Form
Now, go to the browser window. Make sure to open Console tab from the Developer Tools. Start typing into an input field, and you will see the console statement corresponding to a particular input fields triggers.
Handling Form submission
In this section, you are going to add the logic to handle the form submission. Start by adding an if/else
statement to check whether the first input field name
is not empty, and the second input field amount
is not a negative value.
Next, create a single expense
object that takes the current value of name
and amount
input fields. Now the hard part. Right now, the expenses
array has already an initial value with three individual expense objects. If you are going to add to that array, you will have to take care of not override the previous expense objects in that array. Spread operator to the rescue.
const handleSubmitForm = event => {
event.preventDefault()
//check whether the name is not empty and the amount is not negative
if (name !== '' && amount > 0) {
// single expense object
const expense = { name, amount }
// do not override previous values in the array
// use spread operator to access previous values
setExpenses([...expenses, expense])
// clean input fields
setName('')
setAmount('')
} else {
console.log('Invalid expense name or the amount')
}
}
Lastly, you have to clear both the input fields after the form submission. Set them back to their initial values, that is, empty strings.
Go the browser window and try adding a few items. Do notice that the Total Expense gets an update after each form submission.
On the empty submission, it will trigger the else
clause. To see it in action, make sure you have Console tab from Developer Tools open.
Adding localStorage API to persist data
Right now, there is a way to persist these values permanently since all you are using a mock array to display and add new expenses. Using localStorage()
API let us add the functionality to save all the expenses that the user adds to the list.
The localStorage
API allows you to access a Storage
object that is the stored data saved across browser sessions.
Each expense value you are going to store in the localStorage
API is going to be a string so make sure you understand the difference between JSON.stringify()
and JSON.parse()
.
Replace the current mock ALL_EXPENSES
with the following conditional operator.
const ALL_EXPENSES = localStorage.getItem('expenses')
? JSON.parse(localStorage.getItem('expenses'))
: []
Using the method getItem()
from the localStorage
API you can read any value stored. However, right now, there is no value stored so it is going to be an empty array. You can verify this by opening Developer Tools > Application > Storage > LocalStorage > https://localhost:3000.
Adding side-effects
Using the hook useEffect
you can handle lifecycle methods directly inside the functional components. By default, it runs after every render including the initial render, but you can control that behavior by passing dependencies in an array. If dependency being passed changes or gets an update, then only it will run.
Import the useEffect
app from React in App.js
file.
import React, { useState, useEffect } from 'react'
Inside this useEffect
function you are going to use localStorage.setItem()
to store the expenses. It accepts two parameters. First is going to be a callback function and second is going to the dependency.
This dependency is going to be the expenses
from the state. Basically, you are saying that, whenever there is an update to the initial value of the expenses
, run the useEffect
method.
Add the following after other handler methods.
useEffect(() => {
localStorage.setItem('expenses', JSON.stringify(expenses))
}, [expenses])
Make sure the key (expenses
) you are passing in setItem()
is the same as the key whose value you are getting using getItem
.
The useEffect
is still going to run after the initial render but won't run after that until there is a change in the value of expenses
.
See the demo below.
Notice that it works. If you refresh the browser window, the list stays as it is.
Deleting all items from the list
This is a small section in which you are going to add the functionality of clearing the whole list of expenses with a single button click. To do so, create a handler method and inside it, set the initial value of the expenses
to an empty array.
Open App.js
file and add this:
const handleClearExpenses = () => {
setExpenses([])
}
Pass it as a prop to the Form
component.
<Form
name={name}
amount={amount}
handleName={handleName}
handleAmount={handleAmount}
handleSubmitForm={handleSubmitForm}
handleClearExpenses={handleClearExpenses}
/>
Next, edit the Form.js
file and add a new button to delete the list of items. Do not forget to destructor the new prop handleClearExpenses
.
import React from 'react'
import {
Form as BTForm,
FormGroup,
Input,
Label,
Col,
Button
} from 'reactstrap'
const Form = ({
name,
amount,
handleName,
handleAmount,
handleSubmitForm,
handleClearExpenses
}) => (
<BTForm style={{ margin: 10 }} onSubmit={handleSubmitForm}>
<FormGroup className="row">
<Label for="exampleEmail" sm={2}>
Name of Expense
</Label>
<Col sm={4}>
<Input
type="text"
name="name"
id="expenseName"
placeholder="Name of expense?"
value={name}
onChange={handleName}
/>
</Col>
</FormGroup>
<FormGroup className="row">
<Label for="exampleEmail" sm={2}>
$ Amount
</Label>
<Col sm={4}>
<Input
type="number"
name="amount"
id="expenseAmount"
placeholder="0.00"
value={amount}
onChange={handleAmount}
/>
</Col>
</FormGroup>
<Button type="submit" color="primary">
Add
</Button>{' '}
<Button type="submit" color="danger" onClick={handleClearExpenses}>
Delete
</Button>
</BTForm>
)
export default Form
Notice, that right now there are three items in the list, as shown below.
On clicking the delete button will erase all the items from the localstorage.
Conclusion
Congratulations π
You have just learned the basics of React hooks and how to implement them in a real-time application. Also, using localStorage
API is easy. I hope you had fun and gained something useful out of this tutorial. Go ahead, and try to extend this app by adding features like:
- editing a single item in the list
- deleting a single item in the list
- adding a uniquely generated id for each item
To learn more about React hooks, I can highly recommend following React official documentation here.
Originally published at Crowdbotics' Blog.
πππ
I frequently write on Nodejs, Reactjs, and React Native. You can subscribe to my weekly newsletter and join 950+ devs to receive new updates straight to your inbox.
Top comments (3)
the page is getting full blank once i reload it to test my localstorage is working or not.
Hey, I want to delete the single item in the list, how can I add this function ??
the localStorage() is not working for me!