In this article, you will build a Book Management App in React from scratch and learn how to perform CRUD(Create, Read, Update and Delete) operations.
By creating this app, you will learn
- How to perform CRUD operations
- How to use React Router for navigation between routes
- How to use React Context API to pass data across routes
- How to create a Custom Hook in React
- How to store data in local storage to persist it even after page refresh
- How to manage data stored in local storage using custom hook
and much more.
We will be using React Hooks for building this application. So if you're new to React Hooks, check out my Introduction to React Hooks article to learn the basics of Hooks.
Want to learn Redux from the absolute beginning and build a food ordering app from scratch? Check out the Mastering Redux course.
Initial Setup
Create a new project using create-react-app
:
npx create-react-app book-management-app
Once the project is created, delete all files from the src
folder and create index.js
and styles.scss
files inside the src
folder. Also, create components
, context
, hooks
and router
folders inside the src
folder.
Install the necessary dependencies:
yarn add bootstrap@4.6.0 lodash@4.17.21 react-bootstrap@1.5.2 node-sass@4.14.1 react-router-dom@5.2.0 uuid@8.3.2
Open styles.scss
and add the contents from here inside it.
How to Create the Initial Pages
Create a new file Header.js
inside the components
folder with the following content:
import React from 'react';
import { NavLink } from 'react-router-dom';
const Header = () => {
return (
<header>
<h1>Book Management App</h1>
<hr />
<div className="links">
<NavLink to="/" className="link" activeClassName="active" exact>
Books List
</NavLink>
<NavLink to="/add" className="link" activeClassName="active">
Add Book
</NavLink>
</div>
</header>
);
};
export default Header;
Here, we've added two navigation links using the NavLink
component of react-router-dom
: one to see a list of all the books and the other to add a new book.
We're using the NavLink
component instead of the anchor tag <a />
so the page will not refresh when clicked on any of the links.
Create a new file BooksList.js
inside the components
folder with the following content:
import React from 'react';
const BooksList = () => {
return <h2>List of books</h2>;
};
export default BooksList;
Create a new file AddBook.js
inside the components
folder with the following content:
import React from 'react';
import BookForm from './BookForm';
const AddBook = () => {
const handleOnSubmit = (book) => {
console.log(book);
};
return (
<React.Fragment>
<BookForm handleOnSubmit={handleOnSubmit} />
</React.Fragment>
);
};
export default AddBook;
In this file, we're displaying a BookForm
component(which we're yet to create).
For the BookForm
component, we're passing the handleOnSubmit
method so we can do some processing later once we submit the form.
Now, create a new file BookForm.js
inside the components
folder with the following content:
import React, { useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { v4 as uuidv4 } from 'uuid';
const BookForm = (props) => {
const [book, setBook] = useState({
bookname: props.book ? props.book.bookname : '',
author: props.book ? props.book.author : '',
quantity: props.book ? props.book.quantity : '',
price: props.book ? props.book.price : '',
date: props.book ? props.book.date : ''
});
const [errorMsg, setErrorMsg] = useState('');
const { bookname, author, price, quantity } = book;
const handleOnSubmit = (event) => {
event.preventDefault();
const values = [bookname, author, price, quantity];
let errorMsg = '';
const allFieldsFilled = values.every((field) => {
const value = `${field}`.trim();
return value !== '' && value !== '0';
});
if (allFieldsFilled) {
const book = {
id: uuidv4(),
bookname,
author,
price,
quantity,
date: new Date()
};
props.handleOnSubmit(book);
} else {
errorMsg = 'Please fill out all the fields.';
}
setErrorMsg(errorMsg);
};
const handleInputChange = (event) => {
const { name, value } = event.target;
switch (name) {
case 'quantity':
if (value === '' || parseInt(value) === +value) {
setBook((prevState) => ({
...prevState,
[name]: value
}));
}
break;
case 'price':
if (value === '' || value.match(/^\d{1,}(\.\d{0,2})?$/)) {
setBook((prevState) => ({
...prevState,
[name]: value
}));
}
break;
default:
setBook((prevState) => ({
...prevState,
[name]: value
}));
}
};
return (
<div className="main-form">
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<Form onSubmit={handleOnSubmit}>
<Form.Group controlId="name">
<Form.Label>Book Name</Form.Label>
<Form.Control
className="input-control"
type="text"
name="bookname"
value={bookname}
placeholder="Enter name of book"
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group controlId="author">
<Form.Label>Book Author</Form.Label>
<Form.Control
className="input-control"
type="text"
name="author"
value={author}
placeholder="Enter name of author"
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group controlId="quantity">
<Form.Label>Quantity</Form.Label>
<Form.Control
className="input-control"
type="number"
name="quantity"
value={quantity}
placeholder="Enter available quantity"
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group controlId="price">
<Form.Label>Book Price</Form.Label>
<Form.Control
className="input-control"
type="text"
name="price"
value={price}
placeholder="Enter price of book"
onChange={handleInputChange}
/>
</Form.Group>
<Button variant="primary" type="submit" className="submit-btn">
Submit
</Button>
</Form>
</div>
);
};
export default BookForm;
Let's understand what we're doing here.
Initially, we've defined a state as an object using useState
hook to store all the entered details like this:
const [book, setBook] = useState({
bookname: props.book ? props.book.bookname : '',
author: props.book ? props.book.author : '',
quantity: props.book ? props.book.quantity : '',
price: props.book ? props.book.price : '',
date: props.book ? props.book.date : ''
});
As we'll be using the same BookForm
component to add and edit the book, we're first checking if the book
prop is passed or not using the ternary operator.
If the prop is passed, we're setting it to the passed value otherwise an empty string ('').
Don't worry If it looks complicated now. You will understand it better once we build some initial functionality.
Then we've added a state for displaying an error message and used ES6 destructuring syntax to refer each of the property inside the state like this:
const [errorMsg, setErrorMsg] = useState('');
const { bookname, author, price, quantity } = book;
From the BookForm
component, we're returning a Form where we enter book name, book author, quantity and price. We're using react-bootstrap framework to display the form in a nice format.
Each input field has added a onChange
handler which calls the handleInputChange
method.
Inside the handleInputChange
method, we've added a switch statement to change the value of the state based on which input field is changed.
When we type anything in the quantity
input field, event.target.name
will be quantity
so the first switch case will match and inside that switch case, we're checking If the entered value is an integer without a decimal point.
If yes, then only, we're updating the state as shown below:
if (value === '' || parseInt(value) === +value) {
setBook((prevState) => ({
...prevState,
[name]: value
}));
}
So the user is not able to enter any decimal value for the quantity input field.
For the price
switch case, we're checking for a decimal number with only two digits after the decimal point. So we've added a regular expression check value.match(/^\d{1,}(\.\d{0,2})?$/)
.
If the price value matches with the regular expression then only we're updating the state.
Note: For both the quantity
and price
switch case, we're checking for empty value also like this value === ''
. This is to allow the user to entirely delete the entered value if required.
Without that check, the user will not be able to able to delete the entered value by pressing Ctrl + A + Delete
.
For all other input fields, the default switch case will be executed which will update the state based on entered value.
Next, once we submit the form, the handleOnSubmit
method will be called.
Inside this method, we're first checking If the user has entered all the details using the array every
method:
const allFieldsFilled = values.every((field) => {
const value = `${field}`.trim();
return value !== '' && value !== '0';
});
Array every
method is one of the most useful array methods in JavaScript.
Check out my this article to learn about the most useful JavaScript array methods along with their browser support.
If all the values are filled in, then we're creating an object with all the filled in values and calling the handleOnSubmit
method by passing the book as an argument otherwise we're setting an error message.
The handleOnSubmit
method is passed as a prop from the AddBook
component.
if (allFieldsFilled) {
const book = {
id: uuidv4(),
bookname,
author,
price,
quantity,
date: new Date()
};
props.handleOnSubmit(book);
} else {
errorMsg = 'Please fill out all the fields.';
}
Note that, to create a unique id we're calling uuidv4()
method from uuid npm package.
Now, create a new file AppRouter.js
inside the router
folder with the following content:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';
const AppRouter = () => {
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<Switch>
<Route component={BooksList} path="/" exact={true} />
<Route component={AddBook} path="/add" />
</Switch>
</div>
</div>
</BrowserRouter>
);
};
export default AppRouter;
Here, we have set up routing for various components like BooksList
and AddBook
using react-router-dom
library.
If you're new to React Router, Check out my free React Router Introduction course.
Now, open src/index.js
file and add the following contents inside it:
import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './router/AppRouter';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.scss';
ReactDOM.render(<AppRouter />, document.getElementById('root'));
Now, start the React App by running the following command from the terminal:
yarn start
You will see the following screen when you access the application at http://localhost:3000/.
As you can see, we're correctly able to add the book and display it on the console.
But Instead of logging into the console, let's add it to local storage.
How to Create a Custom Hook for Local Storage
Local storage is amazing. It allows us to easily store application data in the browser and is an alternative to cookies for storing data.
The advantage of using local storage is that the data will be saved permanently in the browser cache until we manually delete it so we can access it even after refreshing the page, as you might be aware that, data stored in the React state will be lost once we refresh the page.
There are many use cases of local storage, one of them is to store shopping cart items so it will not be deleted even we refresh the page.
To add data to the local storage, we use the setItem
method by providing a key and value:
localStorage.setItem(key, value)
Both the key and value have to be a string. But we can store the JSON object also by using
JSON.stringify
method.
To learn about local storage and its various applications in detail, check out my this article.
Create a new file useLocalStorage.js
inside the hooks
folder with the following content:
import { useState, useEffect } from 'react';
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
try {
const localValue = window.localStorage.getItem(key);
return localValue ? JSON.parse(localValue) : initialValue;
} catch (error) {
return initialValue;
}
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
export default useLocalStorage;
Here, we've used a useLocalStorage
hook that accepts a key
and initialValue
.
For declaring a state using the useState
hook, we're using lazy initialization.
So the code inside the function passed to the useState
will be executed only once even If the useLocalStorage
hook will be called multiple times on every re-render of the application.
So initially we're checking If there is any value in local storage with the provided key
and returning the value by parsing it using JSON.parse
method
try {
const localValue = window.localStorage.getItem(key);
return localValue ? JSON.parse(localValue) : initialValue;
} catch (error) {
return initialValue;
}
Then later, If there is any change in the key
or value
, we'll update the local storage:
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
and then we're returning the value
stored in local storage and setValue
function which we will call to update the localStorage data.
How to Use the Local Storage Hook
Now, let's use this useLocalStorage
hook so we can add or remove data from local storage.
Open the AppRouter.js
file and use the useLocalStorage
hook inside the component:
import useLocalStorage from '../hooks/useLocalStorage';
const AppRouter = () => {
const [books, setBooks] = useLocalStorage('books', []);
return (
...
)
}
Now, we need to pass the books
and setBooks
as a prop to the AddBook
component so we can add the book to the local storage.
So change the route from this code:
<Route component={AddBook} path="/add" />
to the below code:
<Route
render={(props) => (
<AddBook {...props} books={books} setBooks={setBooks} />
)}
path="/add"
/>
Here, we're using the render props pattern to pass the default props passed by React router along with the books
and setBooks
.
Check out my free React Router Introduction course to better understand this render props pattern and the importance of using the
render
keyword instead ofcomponent
.
Your entire AppRouter.js
file will look like this now:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';
import useLocalStorage from '../hooks/useLocalStorage';
const AppRouter = () => {
const [books, setBooks] = useLocalStorage('books', []);
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<Switch>
<Route component={BooksList} path="/" exact={true} />
<Route
render={(props) => (
<AddBook {...props} books={books} setBooks={setBooks} />
)}
path="/add"
/>
</Switch>
</div>
</div>
</BrowserRouter>
);
};
export default AppRouter;
Now open AddBook.js
and replace its content with the following code:
import React from 'react';
import BookForm from './BookForm';
const AddBook = ({ history, books, setBooks }) => {
const handleOnSubmit = (book) => {
setBooks([book, ...books]);
history.push('/');
};
return (
<React.Fragment>
<BookForm handleOnSubmit={handleOnSubmit} />
</React.Fragment>
);
};
export default AddBook;
First, we're using ES6 destructuring syntax to access the history
, books
and setBooks
props into the component.
history
prop is automatically passed by React Router to every component mentioned in the <Route />
and we're passing books
and setBooks
props from the AppRouter.js
file.
We're storing all the added books in an array so, inside the handleOnSubmit
method, we're calling the setBooks
function by passing an array by adding a newly added book first and then spreading all the books already added in the books
array as shown below:
setBooks([book, ...books]);
Here, I'm adding the newly added book
first and then spreading the already added books
because I want the latest book to be displayed first when we display the list of books later.
But you can change the order If you want like this:
setBooks([...books, book]);
This will add the newly added book at the end of all already added books.
We're able to use spread operator because we know that books
is an array as we have initialized it to an empty array []
in AppRouter.js
file as shown below:
const [books, setBooks] = useLocalStorage('books', []);
Then once the book is added to local storage by calling the setBooks
method, inside the handleOnSubmit
method, we're redirecting the user to the Books List
page using the history.push
method:
history.push('/');
Now, let's check If we're able to save the books to local storage or not.
As you can see, the book is correctly getting added to the local storage as can be confirmed from the applications tab of chrome dev tools.
How to Display Added Books on the UI
Now, let's display the added books on the UI under the Books List
menu.
Open the AppRouter.js
file and pass the books
and setBooks
as a prop to the BooksList
component.
Your AppRouter.js
file will look like this now:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';
import useLocalStorage from '../hooks/useLocalStorage';
const AppRouter = () => {
const [books, setBooks] = useLocalStorage('books', []);
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<Switch>
<Route
render={(props) => (
<BooksList {...props} books={books} setBooks={setBooks} />
)}
path="/"
exact={true}
/>
<Route
render={(props) => (
<AddBook {...props} books={books} setBooks={setBooks} />
)}
path="/add"
/>
</Switch>
</div>
</div>
</BrowserRouter>
);
};
export default AppRouter;
Here, we've just changed the first Route related to the BooksList
component.
Now, create a new file Book.js
inside the components
folder with the following content:
import React from 'react';
import { Button, Card } from 'react-bootstrap';
const Book = ({
id,
bookname,
author,
price,
quantity,
date,
handleRemoveBook
}) => {
return (
<Card style={{ width: '18rem' }} className="book">
<Card.Body>
<Card.Title className="book-title">{bookname}</Card.Title>
<div className="book-details">
<div>Author: {author}</div>
<div>Quantity: {quantity} </div>
<div>Price: {price} </div>
<div>Date: {new Date(date).toDateString()}</div>
</div>
<Button variant="primary">Edit</Button>{' '}
<Button variant="danger" onClick={() => handleRemoveBook(id)}>
Delete
</Button>
</Card.Body>
</Card>
);
};
export default Book;
Now, open the BooksList.js
file and replace its contents with the following code:
import React from 'react';
import _ from 'lodash';
import Book from './Book';
const BooksList = ({ books, setBooks }) => {
const handleRemoveBook = (id) => {
setBooks(books.filter((book) => book.id !== id));
};
return (
<React.Fragment>
<div className="book-list">
{!_.isEmpty(books) ? (
books.map((book) => (
<Book key={book.id} {...book} handleRemoveBook={handleRemoveBook} />
))
) : (
<p className="message">No books available. Please add some books.</p>
)}
</div>
</React.Fragment>
);
};
export default BooksList;
In this file, we're looping over the books
using the array map
method and passing them as a prop to the Book
component.
Note that, we're also passing the handleRemoveBook
function as a prop so we will be able to delete any of the books.
Inside the handleRemoveBook
function, we're calling the setBooks
function by using the array filter
method to keep only books that do not match with the provided book id
.
const handleRemoveBook = (id) => {
setBooks(books.filter((book) => book.id !== id));
};
Now, If you check the application by visiting http://localhost:3000/, you will be able to see the added book on the UI.
Let's add another book to verify the entire flow.
As you can see, when we add a new book, we're getting redirected to the list page where we're able to delete the book and instantly the book is deleted from UI as well as from the local storage.
Also when we refresh the page, the data does not get lost. That's the power of local storage.
How to Edit a Book
Now, we have add and delete functionality for the book, let's add a way to edit the book.
Open Book.js
and change the below code:
<Button variant="primary">Edit</Button>{' '}
to this code:
<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
Edit
</Button>{' '}
Here, we've added an onClick
handler to redirect the user to the /edit/id_of_the_book
route when we click on the edit button.
But we don't have access to the history
object in the Book
component because history
prop is passed only to the components which are mentioned in the <Route />
.
We're rendering the Book
component inside the BooksList
component so we can get access to history
only inside the BooksList
component and then we can pass it as a prop to the Book
component.
But instead of that, React router provides an easy way using useHistory
hook.
Add import for the useHistory
hook at the top of the Book.js
file:
import { useHistory } from 'react-router-dom';
and inside the Book
component, call the useHistory
hook.
const Book = ({
id,
bookname,
author,
price,
quantity,
date,
handleRemoveBook
}) => {
const history = useHistory();
...
}
Now we got access to the history
object inside the Book
component.
Your entire Book.js
file looks like this now:
import React from 'react';
import { Button, Card } from 'react-bootstrap';
import { useHistory } from 'react-router-dom';
const Book = ({
id,
bookname,
author,
price,
quantity,
date,
handleRemoveBook
}) => {
const history = useHistory();
return (
<Card style={{ width: '18rem' }} className="book">
<Card.Body>
<Card.Title className="book-title">{bookname}</Card.Title>
<div className="book-details">
<div>Author: {author}</div>
<div>Quantity: {quantity} </div>
<div>Price: {price} </div>
<div>Date: {new Date(date).toDateString()}</div>
</div>
<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
Edit
</Button>{' '}
<Button variant="danger" onClick={() => handleRemoveBook(id)}>
Delete
</Button>
</Card.Body>
</Card>
);
};
export default Book;
Create a new file EditBook.js
inside the components
folder with the following content:
import React from 'react';
import BookForm from './BookForm';
import { useParams } from 'react-router-dom';
const EditBook = ({ history, books, setBooks }) => {
const { id } = useParams();
const bookToEdit = books.find((book) => book.id === id);
const handleOnSubmit = (book) => {
const filteredBooks = books.filter((book) => book.id !== id);
setBooks([book, ...filteredBooks]);
history.push('/');
};
return (
<div>
<BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />
</div>
);
};
export default EditBook;
Here, for the onClick
handler of the Edit button, we're redirecting the user to the /edit/some_id
route but such route does not exist yet. So let's create that first.
Open AppRouter.js
file and before the ending tag of Switch
add two more routes:
<Switch>
...
<Route
render={(props) => (
<EditBook {...props} books={books} setBooks={setBooks} />
)}
path="/edit/:id"
/>
<Route component={() => <Redirect to="/" />} />
</Switch>
The first Route is for the EditBook
component. Here, the path is defined as /edit/:id
where :id
represents any random id.
The second Route is to handle all other routes that do not match with any of the routes mentioned.
So If we access any random route like /help
or /contact
then we'll redirect the user to the /
route which is the BooksList
component.
Your entire AppRouter.js
file looks like this now:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';
import useLocalStorage from '../hooks/useLocalStorage';
const AppRouter = () => {
const [books, setBooks] = useLocalStorage('books', []);
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<Switch>
<Route
render={(props) => (
<BooksList {...props} books={books} setBooks={setBooks} />
)}
path="/"
exact={true}
/>
<Route
render={(props) => (
<AddBook {...props} books={books} setBooks={setBooks} />
)}
path="/add"
/>
<Route
render={(props) => (
<EditBook {...props} books={books} setBooks={setBooks} />
)}
path="/edit/:id"
/>
<Route component={() => <Redirect to="/" />} />
</Switch>
</div>
</div>
</BrowserRouter>
);
};
export default AppRouter;
Now, let's check the edit functionality of the app.
As you can see we're successfully able to edit the book. Let's understand how this works.
First, inside the AppRouter.js
file we've route like this:
<Route
render={(props) => (
<EditBook {...props} books={books} setBooks={setBooks} />
)}
path="/edit/:id"
/>
and inside the Book.js
file, we've edit button like this:
<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
Edit
</Button>
So whenever we're clicking on the Edit button for any of the books, we're redirecting the user to the EditBook
component using the history.push
method by passing the id of the book to be edited.
Then inside the EditBook
component, we're using the useParams
hook provided by react-router-dom
to access the props.params.id
So the below two lines are identical.
const { id } = useParams();
// the above line of code is the same as the below code
const { id } = props.match.params;
Once we got that id
, we're using the array find
method to find out the particular book from the list of books with the matching provided id
.
const bookToEdit = books.find((book) => book.id === id);
and this particular book we're passing to the BookForm
component as a book
prop:
<BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />
And inside the BookForm
component, we've defined the state as shown below:
const [book, setBook] = useState({
bookname: props.book ? props.book.bookname : '',
author: props.book ? props.book.author : '',
quantity: props.book ? props.book.quantity : '',
price: props.book ? props.book.price : '',
date: props.book ? props.book.date : ''
});
Here, we're checking If the book
prop exists. If yes, then we're using the details of the book passed as a prop otherwise we're initializing the state with an empty value('') for each property.
And each of the input element has provided a value
prop which we're setting from the state like this:
<Form.Control
...
value={bookname}
...
/>
But we can improve a bit on the useState
syntax inside the BookForm
component.
Instead of directly setting an object for the useState
hook, we can use lazy initialization as done in the useLocalStorage.js
file.
So change the below code:
const [book, setBook] = useState({
bookname: props.book ? props.book.bookname : '',
author: props.book ? props.book.author : '',
quantity: props.book ? props.book.quantity : '',
price: props.book ? props.book.price : '',
date: props.book ? props.book.date : ''
});
to this code:
const [book, setBook] = useState(() => {
return {
bookname: props.book ? props.book.bookname : '',
author: props.book ? props.book.author : '',
quantity: props.book ? props.book.quantity : '',
price: props.book ? props.book.price : '',
date: props.book ? props.book.date : ''
};
});
Because of this change, the code for setting state will not be executed on every re-render of the application. But it will be executed only once when the component is mounted.
Note that the re-rendering of the component happens on every state or prop change.
If you check the application, you will see that the application works exactly as before without any issue. But we've just improved the application performance by a little bit.
How to use React's Context API
Now, we're done with the entire application functionality. But If you check the AppRouter.js
file, you will see that each Route looks a bit complicated as we're passing the same books
and setBooks
props to each of the components by using the render props pattern.
So we can use the React Context API to simplify this code.
Note that this is an optional step. You don't need to use Context API as we're passing the props only one level deep and the current code is working perfectly fine and we've not used any wrong approach for passing the props.
But just to make the Router code simpler and to give you an idea about how to leverage the power of Context API, we will use it in our application.
Create a new file BooksContext.js
inside the context
folder with the following content:
import React from 'react';
const BooksContext = React.createContext();
export default BooksContext;
Now, inside the AppRouter.js
file, import the above exported context.
import BooksContext from '../context/BooksContext';
and replace the AppRouter
component with the below code:
const AppRouter = () => {
const [books, setBooks] = useLocalStorage('books', []);
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<BooksContext.Provider value={{ books, setBooks }}>
<Switch>
<Route component={BooksList} path="/" exact={true} />
<Route component={AddBook} path="/add" />
<Route component={EditBook} path="/edit/:id" />
<Route component={() => <Redirect to="/" />} />
</Switch>
</BooksContext.Provider>
</div>
</div>
</BrowserRouter>
);
};
Here, we've converted the render props pattern back to the normal routes and added the entire Switch
block inside the BooksContext.Provider
component like this:
<BooksContext.Provider value={{ books, setBooks }}>
<Switch>
...
</Switch>
</BooksContext.Provider>
Here, for the BooksContext.Provider
component we've provided a value
prop by passing the data we want to access inside the components mentioned in the Route.
So now, every component declared as a part of Route will be able to access the books
and setBooks
via context API.
Now, open BooksList.js
file and remove the books
and setBooks
props which are destructured as we are no longer directly passing the props.
Add import for the BooksContext
and useContext
at the top of the file:
import React, { useContext } from 'react';
import BooksContext from '../context/BooksContext';
And above the handleRemoveBook
function, add the following code:
const { books, setBooks } = useContext(BooksContext);
Here, we're taking out the books
and setBooks
props from the BooksContext
using the useContext
hook.
Your entire BooksList.js
file will look like this:
import React, { useContext } from 'react';
import _ from 'lodash';
import Book from './Book';
import BooksContext from '../context/BooksContext';
const BooksList = () => {
const { books, setBooks } = useContext(BooksContext);
const handleRemoveBook = (id) => {
setBooks(books.filter((book) => book.id !== id));
};
return (
<React.Fragment>
<div className="book-list">
{!_.isEmpty(books) ? (
books.map((book) => (
<Book key={book.id} {...book} handleRemoveBook={handleRemoveBook} />
))
) : (
<p className="message">No books available. Please add some books.</p>
)}
</div>
</React.Fragment>
);
};
export default BooksList;
Now, make similar changes in the AddBook.js
file.
Your entire AddBook.js
file will look like this:
import React, { useContext } from 'react';
import BookForm from './BookForm';
import BooksContext from '../context/BooksContext';
const AddBook = ({ history }) => {
const { books, setBooks } = useContext(BooksContext);
const handleOnSubmit = (book) => {
setBooks([book, ...books]);
history.push('/');
};
return (
<React.Fragment>
<BookForm handleOnSubmit={handleOnSubmit} />
</React.Fragment>
);
};
export default AddBook;
Note that, Here, we're still using the destructuring for the history
prop. We've only removed the books
and setBooks
from destructuring syntax.
Now, make similar changes in the EditBook.js
file.
Your entire EditBook.js
file will look like this:
import React, { useContext } from 'react';
import BookForm from './BookForm';
import { useParams } from 'react-router-dom';
import BooksContext from '../context/BooksContext';
const EditBook = ({ history }) => {
const { books, setBooks } = useContext(BooksContext);
const { id } = useParams();
const bookToEdit = books.find((book) => book.id === id);
const handleOnSubmit = (book) => {
const filteredBooks = books.filter((book) => book.id !== id);
setBooks([book, ...filteredBooks]);
history.push('/');
};
return (
<div>
<BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />
</div>
);
};
export default EditBook;
If you check the application, you will see that it works exactly as before but we're now using React Context API.
If you want to understand the Context API in detail, check out my this article.
Thanks for reading!
You can find the complete source code for this application in this repository.
Want to learn all ES6+ features in detail including let and const, promises, various promise methods, array and object destructuring, arrow functions, async/await, import and export and a whole lot more from scratch?
Check out my Mastering Modern JavaScript book. This book covers all the pre-requisites for learning React and helps you to become better at JavaScript and React.
Check out free preview contents of the book here.
Also, you can check out my free Introduction to React Router course to learn React Router from scratch.
Want to stay up to date with regular content regarding JavaScript, React, Node.js? Follow me on LinkedIn.