Artigo original: React CRUD App Tutorial – How to Build a Book Management App in React from Scratch
Neste artigo, você construirá uma aplicação de gerenciamento de livros em React do zero.
Ao criar essa aplicação, você aprenderá:
- Como realizar operações de CRUD
- Como usar o React Router para navegação entre rotas
- Como usar a API React Context para passar dados através de rotas
- Como criar um hook personalizado em React
- Como armazenar dados em armazenamento local para persistir mesmo após a atualização da página
- Como gerenciar os dados armazenados no armazenamento local usando um hook personalizado
e muito mais.
Usaremos hooks do React para construir esta aplicação. Portanto, se você é novo no React Hooks, confira meu artigo Introdução ao React Hooks (texto em inglês) para aprender o básico.
Quer aprender Redux desde o início absoluto e construir uma aplicação de pedido de comida a partir do zero? Confira o curso Mastering Redux.
Configuração inicial
Crie um projeto utilizando o create-react-app
:
npx create-react-app book-management-app
Uma vez criado o projeto, exclua todos os arquivos da pasta src
e crie os arquivos index.js
e styles.scss
dentro da pasta src
. Além disso, crie as pastas components
, context
, hooks
e router
dentro da pasta src
.
Instale as dependências necessárias:
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
Abra styles.scss
e adicione este conteúdo a ela.
Como criar as páginas iniciais
Crie um novo arquivo Header.js
dentro da pasta components
com o seguinte conteúdo:
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;
Aqui, adicionamos dois links de navegação usando o componente NavLink
do react-router-dom
: um para ver uma lista de todos os livros e o outro para adicionar um novo livro.
Estamos usando o NavLink
em vez da tag de âncora (<a />
). Portanto, a página não será atualizada quando um usuário clicar em qualquer um dos links.
Crie um arquivo chamado BooksList.js
dentro da pasta components
com o seguinte conteúdo:
import React from 'react';
const BooksList = () => {
return <h2>List of books</h2>;
};
export default BooksList;
Crie um arquivo chamado AddBook.js
dentro da pasta components
com o seguinte conteúdo:
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;
Neste arquivo, exibimos um componente BookForm
(formulário de livros, que ainda não criamos).
Para o componente BookForm
, passamos o método handleOnSubmit
para que possamos fazer o processamento mais tarde, quando enviarmos o formulário.
Agora, crie um arquivo BookForm.js
dentro da pasta components
com o seguinte conteúdo:
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;
Vamos entender o que estamos fazendo aqui.
Inicialmente, definimos um estado como um objeto usando o hook useState
para armazenar todos os detalhes inseridos deste modo:
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 : ''
});
Como usaremos o mesmo componente BookForm
para adicionar e editar o livro, verificamos primeiro se a prop book
é passada ou não usando o operador ternário.
Se a prop tiver sido passada, definimos a prop como o valor passado, Do contrário, a definimos como uma string vazia (''
).
Não se preocupe se isso parece complicado agora. Você entenderá melhor quando criarmos as funcionalidades iniciais.
Em seguida, adicionamos um state para exibir uma mensagem de erro e usamos a sintaxe de desestruturação da ES6 para nos referirmos a cada uma das propriedades dentro do state assim:
const [errorMsg, setErrorMsg] = useState('');
const { bookname, author, price, quantity } = book;
A partir do componente BookForm
, retornamos um formulário onde inserimos o nome do livro, autor do livro, quantidade e preço. Usamos o react-bootstrap para exibir o formulário em um formato mais bonito.
Cada campo de entrada adicionou um manipulador de eventos onChange
, que chama o método handleInputChange
.
Dentro do método handleInputChange
, adicionamos uma declaração de mudança para alterar o valor do state com base no campo de entrada que é alterado.
Quando digitarmos qualquer coisa no campo de entrada quantity
, event.target.name
será a quantidade para corresponder ao primeiro caso do switch. Dentro desse caso switch, verificamos se o valor inserido é um número inteiro sem um ponto decimal.
Se for um número inteiro, simplesmente atualizamos o state conforme mostrado abaixo:
if (value === '' || parseInt(value) === +value) {
setBook((prevState) => ({
...prevState,
[name]: value
}));
}
Portanto, o usuário não poderá inserir nenhum valor decimal para o campo de entrada quantity
.
Para o caso do switch de price
, estamos verificando um número decimal com apenas dois algarismos após a vírgula. Depois, adicionamos uma verificação de expressão regular come esta aparência: value.match(/^\d{1,}(\d{0,2})?$/)
.
Se o valor de price
corresponder à expressão regular, atualizamos o state.
Observação: tanto para os casos de switch de quantity
como de price
, verificamos valores vazios como este: value === ''
. Isso é para permitir que o usuário exclua totalmente o valor inserido, se necessário.
Sem essa verificação, o usuário não poderá excluir o valor inserido pressionando Ctrl + A + Delete
.
Para todos os outros campos de entrada, será executado o caso switch que atualizará o state com base no valor inserido pelo usuário.
Em seguida, uma vez enviado o formulário, o método handleOnSubmit
será chamado.
Dentro desse método, verificamos primeiro se o usuário digitou todos os detalhes usando o método de array every
:
const allFieldsFilled = values.every((field) => {
const value = `${field}`.trim();
return value !== '' && value !== '0';
});
O método de array every
é um dos métodos de array mais úteis em JavaScript.
Confira o meu artigo aqui para aprender sobre os métodos mais úteis de array JavaScript junto com o seu suporte ao navegador.
Se todos os valores forem preenchidos, criaremos um objeto com eles. Também chamamos o método handleOnSubmit
passando livro como argumento. Caso contrário, definimos uma mensagem de erro.
O método handleOnSubmit
é passado como uma prop do componente AddBook
.
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 que, para criar uma ID única, chamamos o método uuidv4()
a partir do pacote uuid do npm.
Agora, crie um arquivo AppRouter.js
dentro da pasta router
com o seguinte conteúdo:
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;
Aqui, estabelecemos rotas para vários componentes como BooksList
e AddBook
usando a biblioteca react-router-dom
.
Se você é novo no React Router, confira meu curso gratuito de Introdução ao React Router (em inglês).
Agora, abra o arquivo src/index.js
e adicione o seguinte conteúdo a ele:
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'));
Agora, inicie a aplicação do React, executando o seguinte comando a partir do terminal:
yarn start
Você verá a tela a seguir ao acessar a aplicação em http://localhost:3000/.


Como você pode ver, conseguimos adicionar corretamente um livro e exibi-lo no console.
Em vez de logar no console, no entanto, vamos adicioná-lo ao armazenamento local.
Como criar um hook personalizado para armazenamento local
O armazenamento local é surpreendente. Ele nos permite armazenar facilmente os dados da aplicação no navegador e é uma alternativa aos cookies para o armazenamento de dados.
A vantagem de se usar o armazenamento local é que os dados serão salvos permanentemente no cache do navegador até que os excluamos manualmente para podermos acessá-los mesmo após a atualização da página. Como você deve saber, os dados armazenados no state do React serão perdidos uma vez que atualizarmos a página.
Há muitos casos de uso para o armazenamento local. Um deles é para armazenar itens do carrinho de compras para que não sejam excluídos mesmo que atualizemos a página.
Para adicionar dados ao armazenamento local, usamos o método setItem
, fornecendo uma chave e um valor:
localStorage.setItem(key, value)
Tanto a chave quanto o valor precisam ser strings. Podemos, contudo, armazenar o objeto JSON também usando o método JSON.stringify
.
Para aprender em detalhes sobre o armazenamento local e sobre suas diversas aplicações, confira este artigo (em inglês).
Crie um arquivo useLocalStorage.js
dentro da pasta hooks
com o seguinte conteúdo:
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;
Aqui, usamos um hook useLocalStorage
, que aceita uma key
e um initialValue
.
Para declarar o state usando o hook useState
, estamos usando inicialização preguiçosa (em inglês).
Assim, o código dentro da função passada para useState
será executado apenas uma vez, mesmo que o hook useLocalStorage
seja chamado várias vezes em cada renderização da aplicação.
Então, inicialmente, estamos verificando se existe algum valor no armazenamento local com a key
fornecida e retornamos o valor usando o método JSON.parse
:
try {
const localValue = window.localStorage.getItem(key);
return localValue ? JSON.parse(localValue) : initialValue;
} catch (error) {
return initialValue;
}
Depois, se houver alguma mudança em key
ou em value
, atualizaremos o armazenamento local:
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
Em seguida, estamos retornando value
armazenado no armazenamento local e a função setValue
que chamaremos para atualizar os dados do armazenamento local.
Como usar o hook de armazenamento local
Agora, vamos usar este hook chamado useLocalStorage
para que possamos adicionar ou remover dados do armazenamento local.
Abra o arquivo AppRouter.js
e use o hook de useLocalStorage
dentro do componente:
import useLocalStorage from '../hooks/useLocalStorage';
const AppRouter = () => {
const [books, setBooks] = useLocalStorage('books', []);
return (
...
)
}
Agora, precisamos passar books
e setBooks
como props para o componente AddBook
para que possamos adicionar o livro ao armazenamento local.
Portanto, mude a rota a partir deste código:
<Route component={AddBook} path="/add" />
para o código abaixo:
<Route
render={(props) => (
<AddBook {...props} books={books} setBooks={setBooks} />
)}
path="/add"
/>
Aqui, estamos usando o padrão de renderização de props para passar as props padrão do React Router juntamente com books
e setBooks
.
Confira o meu curso gratuito de Introdução ao React Router (em inglês) para entender melhor esse padrão de renderização de props e a importância de se usar a palavra-chaverender
em vez decomponent
.
Seu arquivo AppRouter.js
terá agora esta aparência:
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;
Agora, abra AddBook.js
e substitua o seu conteúdo com o seguinte código:
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;
Primeiro, usamos a sintaxe de desestruturação da ES6 para acessar as props history
, books
e setBooks
no componente.
A prop history
é passada automaticamente pelo React Router a cada componente mencionado na <Route />
. Passamos as props books
e setBooks
do arquivo AppRouter.js
.
Armazenamos todos os livros adicionados em um array. Dentro do método handleOnSubmit
, chamamos a função setBooks
passando um array ao adicionar primeiro um livro recém-adicionado e depois fazendo o spread de todos os livros já adicionados no array books
, como mostrado abaixo:
setBooks([book, ...books]);
Aqui, adicionamos primeiro o livro recém-adicionado e, depois, fazemos o spread dos livros já adicionados, pois queremos que o último livro seja exibido primeiro quando mostrarmos a lista de livros mais tarde.
Você, no entanto, pode mudar a ordem se quiser:
setBooks([...books, book]);
Isto adicionará o livro recém-adicionado no final de todos os livros já adicionados.
Podemos usar o operador spread, pois sabemos que books
é um array (que iniciamos como um array vazio [] no arquivo AppRouter.js
, como mostrado abaixo):
const [books, setBooks] = useLocalStorage('books', []);
Então, uma vez que o livro é adicionado ao armazenamento local chamando o método setBooks
, dentro do método handleOnSubmit
, estamos redirecionando o usuário para a página Books List
usando o método history.push
:
history.push('/');
Agora, vamos verificar se conseguimos salvar ou não os livros no armazenamento local.

Como você pode ver, o livro está sendo corretamente adicionado ao armazenamento local (e você pode confirmar isso na aba Applications das ferramentas de desenvolvedor do Chrome).
Como exibir os livros adicionados na interface de usuário
Agora, vamos exibir os livros adicionados na UI (interface do usuário) no menu de Books List
.
Abra o AppRouter.js
e passe books
e setBooks
como props para o componente BooksList
.
O seu arquivo AppRouter.js
terá esta aparência agora:
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;
Aqui, acabamos de alterar a primeira rota relacionada ao componente BooksList
.
Agora, crie um arquivo Book.js
dentro da pasta components
com o seguinte conteúdo:
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;
Agora, abra o arquivo BooksList.js
e substitua o seu conteúdo pelo seguinte código:
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;
Neste arquivo, estamos percorrendo books
em um laço usando o método de array map
e passando os livros como uma prop para o componente Book
.
Note que também estamos passando a função handleRemoveBook
como uma prop para que possamos excluir qualquer livro que queiramos.
Dentro da função handleRemoveBook
, estamos chamando a função setBooks
usando o método de array filter
para manter somente os livros que não correspondem ao ID do livro fornecido.
const handleRemoveBook = (id) => {
setBooks(books.filter((book) => book.id !== id));
};
Agora, se você verificar a aplicação acessando http://localhost:3000/, poderá ver o livro adicionado na interface do usuário.

Vamos adicionar outro livro para verificar todo o fluxo.

Como você pode ver, quando adicionamos um novo livro, somos redirecionados para a página da lista onde podemos excluir o livro. Você pode ver que ele é instantaneamente excluído da UI, bem como do armazenamento local.
Quando atualizamos a página, além disso, os dados não se perdem. Esse é o poder do armazenamento local.
Como editar um livro
Já temos a funcionalidade de adicionar e excluir os livros. Agora, vamos adicionar um modo de editar os livros que temos.
Abra Book.js
e mude o código abaixo:
<Button variant="primary">Edit</Button>{' '}
para este código:
<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
Edit
</Button>{' '}
Aqui, adicionamos um manipulador de eventos onClick
para redirecionar o usuário para a rota /edit/id_do_livro
quando clicamos no botão de edição.
Não temos, entretanto, acesso ao objeto history
no componente Book
, pois a prop history
é passada somente aos componentes que são mencionados em <Route />
.
Estamos renderizando o componente Book
dentro do componente BookList
para que possamos ter acesso a history
somente dentro do componente BookList
. Então, podemos passá-la como uma prop ao componente Book
.
Em vez disso, no entanto, o React Router oferece uma maneira fácil de usar o hook useHistory
.
Importe o hook useHistory
no topo do arquivo Book.js
:
import { useHistory } from 'react-router-dom';
e, dentro do componente Book
, chame o hook useHistory
.
const Book = ({
id,
bookname,
author,
price,
quantity,
date,
handleRemoveBook
}) => {
const history = useHistory();
...
}
Agora, temos acesso ao objeto history
dentro do componente Book
.
O arquivo Book.js
terá agora este aparência:
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;
Crie um arquivo chamado EditBook.js
dentro da pasta components
com o seguinte conteúdo:
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;
Aqui, para o manipulador de eventos onClick
do botão Edit, redirecionamos o usuário para a rota /edit/id_do_livro
– essa rota, no entanto, ainda não existe. Portanto, vamos criá-la primeiro.
Abra o AppRouter.js
e, antes da tag final de Switch
, adicione mais duas rotas:
<Switch>
...
<Route
render={(props) => (
<EditBook {...props} books={books} setBooks={setBooks} />
)}
path="/edit/:id"
/>
<Route component={() => <Redirect to="/" />} />
</Switch>
A primeira rota é para o componente EditBook
. Aqui, o caminho é definido como /edit/:id
onde :id
representa qualquer id aleatória.
A segunda rota serve para lidar com todas as outras rotas que não correspondem a nenhuma das rotas mencionadas.
Assim, se acessarmos qualquer rota aleatória, como /help
ou /contact
, redirecionaremos o usuário para a rota /
, que é a do componente BooksList
.
O arquivo AppRouter.js
terá agora esta aparência:
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;
Vamos verificar a funcionalidade de edição do aplicativo.

Como você pode ver, somos capazes de editar o livro com sucesso. Vamos entender como isso funciona.
Primeiro, dentro do arquivo AppRouter.js
, temos uma rota como esta:
<Route
render={(props) => (
<EditBook {...props} books={books} setBooks={setBooks} />
)}
path="/edit/:id"
/>
e dentro do arquivo Book.js
, temos um botão de edição, como este:
<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
Edit
</Button>
Assim, sempre que clicarmos no botão Edit para qualquer um dos livros, estamos redirecionando o usuário para o componente EditBook
usando o método history.push
, passando o id do livro a ser editado.
Depois, dentro do componente EditBook
, usamos o hook useParams
fornecido pelo react-router-dom
para acessar props.params.id
.
Portanto, as duas linhas abaixo são idênticas.
const { id } = useParams();
// A linha de código acima equivale à linha abaixo
const { id } = props.match.params;
Uma vez que tenhamos obtido essa identificação, usaremos o método de array find
para encontrar o livro específico a partir da lista de livros com a id correspondente fornecida.
const bookToEdit = books.find((book) => book.id === id);
e esse livro específico é passado para o componente BookForm
como uma prop book
:
<BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />
Dentro do componente BookForm
, definimos o state como mostrado abaixo:
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 : ''
});
Aqui, verificamos se a prop book
existe. Se existir, usamos os detalhes do livro passado como uma prop. Caso contrário, inicializamos o state com um valor vazio (''
) para cada propriedade.
Cada um dos elementos de entrada forneceu uma prop value
, que estamos definindo a partir do state, deste modo:
<Form.Control
...
value={bookname}
...
/>
Podemos, porém, melhorar um pouco a sintaxe de useState
dentro do componente BookForm
.
Ao invés de definir diretamente um objeto para o hook useState
, podemos usar a inicialização preguiçosa como fizemos no arquivo useLocalStorage.js
.
Assim, mude o código abaixo:
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 : ''
});
para este código:
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 : ''
};
});
Em função dessa mudança, o código para definir o state não será executado em cada renderização da aplicação. Ele será executado apenas uma vez quando o componente for montado.
Note que a renderização do componente acontece em cada state ou mudança de prop.
Se você verificar a aplicação, verá que a aplicação funciona exatamente como antes, sem qualquer problema. Nós apenas melhoramos um pouco o desempenho da aplicação.
Como usar a API React Context
Agora, terminamos de construir toda a funcionalidade da aplicação. Se você, no entanto, verificar o arquivo AppRouter.js
, verá que cada Rota parece um pouco complicada. Isso ocorre porque estamos passando as mesmos props books
e setBooks
para cada um dos componentes, usando o padrão de renderização de props.
Assim, podemos usar a API React Context para simplificar esse código.
Note que este é um passo opcional. Você não precisa usar a API React Context, já que estamos passando as props com apenas um nível de profundidade, o código atual está funcionando perfeitamente bem e não usamos nenhuma abordagem errada para passar as props
Apenas para tornar o código do Router mais simples e para dar uma ideia de como aproveitar o poder da API React Context, usaremos a API em nossa aplicação.
Crie um arquivo BooksContext.js
dentro da pasta de contexto com o seguinte conteúdo:
import React from 'react';
const BooksContext = React.createContext();
export default BooksContext;
Agora, dentro do arquivo AppRouter.js
, importe o contexto exportado acima.
import BooksContext from '../context/BooksContext';
e substitua o componente AppRouter
pelo código abaixo:
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>
);
};
Aqui, convertemos o padrão de renderização das props para as rotas normais e adicionamos todo o bloco de Switch
dentro do componente BooksContext.Provider
assim:
<BooksContext.Provider value={{ books, setBooks }}>
<Switch>
...
</Switch>
</BooksContext.Provider>
Aqui, para o componente BooksContext.Provider
, fornecemos uma prop value
ao passar os dados que queremos acessar dentro dos componentes mencionados em Route.
Assim, agora, cada componente declarado como parte da Route poderá acessar books
e setBooks
através da API React Context.
Agora, abra o arquivo BooksList.js
e remova as props books
e setBooks
que estão desestruturadas, pois não estamos mais passando diretamente as props.
Importe o BooksContext
e useContext
na parte superior do arquivo:
import React, { useContext } from 'react';
import BooksContext from '../context/BooksContext';
Acima da função handleRemoveBook
, adicione o seguinte código:
const { books, setBooks } = useContext(BooksContext);
Aqui, estamos retirando os livros e os adereços de BooksContext
usando o hook useContext
.
O arquivo BooksList.js
terá agora esta aparência:
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;
Agora, faça mudanças similares no arquivo AddBook.js
.
O arquivo AddBooks.js
terá agora esta aparência:
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 que, aqui, ainda estamos usando a desestruturação para a prop history
. Apenas removemos books
e setBooks
da sintaxe de desestruturação.
Agora, faça mudanças similares no arquivo EditBook.js
.
O arquivo EditBooks.js
terá agora esta aparência:
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;
Se você verificar a aplicação, verá que ela funciona exatamente como antes, mas agora estamos usando a API React Context.

Se você quiser entender a API React Context em detalhes, confira este artigo (texto em inglês).
Obrigado pela leitura!
Você pode encontrar o código-fonte completo para esta aplicação neste repositório.
Quer aprender todas as características do ES6+ em detalhes, incluindo let e const, promises, vários métodos de promises, desestruturação de arrays e objetos, arrow functions, async/await, importação e exportação e muito mais?
Confira o livro do autor, Mastering Modern JavaScript (em inglês). Esse livro cobre todos os pré-requisitos para aprender React e ajuda você a se tornar melhor em JavaScript e React.
Confira uma prévia do conteúdo do livro gratuitamente aqui (em inglês).
Além disso, você pode conferir o curso gratuito do autor, Introdução ao React Router (em inglês) para aprender React Router do zero.
Deseja manter-se atualizado com o conteúdo regular sobre JavaScript, React, Node.js? Siga o autor no LinkedIn.
