Original article: React CRUD App Tutorial – How to Build a Book Management App in React from Scratch
En este artículo, construirá una aplicación de administración de libros en React desde cero.
Al crear esta aplicación, aprenderá:
- Cómo realizar operaciones CRUD
- Cómo usar React Router para navegar entre rutas
- Cómo usar React Context API para pasar datos a través de rutas
- Cómo crear un hook personalizado en React
- Cómo almacenar datos en el almacenamiento local para conservarlos incluso después de actualizar la página
- Cómo administrar los datos almacenados en el almacenamiento local mediante un hook personalizado
y mucho más.
Usaremos React Hooks para construir esta aplicación.
Configuración inicial
Crear un nuevo proyecto usando create-react-app
:
npx create-react-app administracion-de-libros-app
Una vez que se crea el proyecto, elimine todos los archivos de la carpeta src
y crea archivos index.js
y styles.scss
dentro de la carpeta src
. También, crea carpetas de componentes
, contexto
, hooks
y enrutadores
dentro de la carpeta src
.
Instala las dependencias necesarias:
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 y agregue los contenidos de aquí adentro.
Cómo crear las páginas iniciales
Crea un nuevo archivo Header.js
dentro de la carpeta de componentes
con el siguiente contenido:
import React from 'react';
import { NavLink } from 'react-router-dom';
const Header = () => {
return (
<header>
<h1>Aplicación de Administración de Libros</h1>
<hr />
<div className="links">
<NavLink to="/" className="link" activeClassName="active" exact>
Lista de Libros
</NavLink>
<NavLink to="/add" className="link" activeClassName="active">
Agrega Libro
</NavLink>
</div>
</header>
);
};
export default Header;
Aquí, hemos agregado dos enlaces de navegación utilizando el componente NavLink
de react-router-dom
: uno para ver una lista de todos los libros y el otro para agregar un nuevo libro.
Usamos NavLink
en lugar de la etiqueta de anclaje ( <a />
) para que la página no se actualice cuando un usuario haga clic en cualquiera de los enlaces.
Cree un nuevo archivo llamado ListaDeLibros.js
dentro de la carpeta de componentes
con el siguiente contenido:
import React from 'react';
const ListaDeLibros = () => {
return <h2>Lista de Libros</h2>;
};
export default ListaDeLibros;
Crea un nuevo archivo llamado AgregaLibro.js
dentro de la carpeta de componentes
con el siguiente contenido:
import React from 'react';
import FormularioDeLibro from './FormularioDeLibro';
const AgregaLibro = () => {
const handleOnSubmit = (libro) => {
console.log(libro);
};
return (
<React.Fragment>
<FormularioDeLibro handleOnSubmit={handleOnSubmit} />
</React.Fragment>
);
};
export default AgregaLibro;
En este archivo, estamos mostrando un componente FormularioDeLibro
(que aún tenemos que crear).
Para el componente FormularioDeLibro
, estamos pasando el método handleOnSubmit
para que podamos realizar algún procesamiento más adelante una vez que enviemos el formulario.
Ahora, crea un nuevo archivo FormularioDeLibro.js
dentro de la carpeta de componentes
con el siguiente contenido:
import React, { useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { v4 as uuidv4 } from 'uuid';
const FormularioDeLibro = (props) => {
const [libro, setLibro] = useState({
nombrelibro: props.libro ? props.libro.nombrelibro : '',
autor: props.libro ? props.libro.autor : '',
cantidad: props.libro ? props.libro.cantidad : '',
precio: props.libro ? props.libro.precio : '',
fecha: props.libro ? props.libro.fecha : ''
});
const [errorMsg, setErrorMsg] = useState('');
const { nombrelibro, autor, precio, cantidad } = libro;
const handleOnSubmit = (event) => {
event.preventDefault();
const valores = [nombrelibro, autor, precio, cantidad];
let errorMsg = '';
const todosLosCamposLlenos = valores.every((campo) => {
const valor = `${campo}`.trim();
return valor !== '' && valor !== '0';
});
if (todosLosCamposLlenos) {
const libro = {
id: uuidv4(),
nombrelibro,
autor,
precio,
cantidad,
fecha: new Date()
};
props.handleOnSubmit(libro);
} else {
errorMsg = 'Por favor, rellene todos los campos.';
}
setErrorMsg(errorMsg);
};
const handleInputChange = (event) => {
const { nombre, valor } = event.target;
switch (nombre) {
case 'cantidad':
if (valor === '' || parseInt(valor) === +valor) {
setLibro((prevState) => ({
...prevState,
[nombre]: valor
}));
}
break;
case 'precio':
if (valor === '' || valor.match(/^\d{1,}(\.\d{0,2})?$/)) {
setLibro((prevState) => ({
...prevState,
[nombre]: valor
}));
}
break;
default:
setLibro((prevState) => ({
...prevState,
[nombre]: valor
}));
}
};
return (
<div className="main-form">
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<Form onSubmit={handleOnSubmit}>
<Form.Group controlId="nombre">
<Form.Label>Nombre del Libro</Form.Label>
<Form.Control
className="input-control"
type="text"
name="nombrelibro"
value={nombrelibro}
placeholder="Ingrese el nombre del libro"
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group controlId="autor">
<Form.Label>Autor del Libro</Form.Label>
<Form.Control
className="input-control"
type="text"
name="autor"
value={autor}
placeholder="Ingrese el nombre del autor"
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group controlId="cantidad">
<Form.Label>Cantidad</Form.Label>
<Form.Control
className="input-control"
type="number"
name="cantidad"
value={cantidad}
placeholder="Ingrese la cantidad disponible"
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group controlId="precio">
<Form.Label>Precio del Libro</Form.Label>
<Form.Control
className="input-control"
type="text"
name="precio"
value={precio}
placeholder="Ingrese el precio del libro"
onChange={handleInputChange}
/>
</Form.Group>
<Button variant="primary" type="submit" className="submit-btn">
Enviar
</Button>
</Form>
</div>
);
};
export default FormularioDeLibro;
Vamos a entender lo que estamos haciendo aquí.
Inicialmente, hemos definido un estado como un objeto usando el useState hook para almacenar todos los detalles ingresados de esta manera:
const [libro, setLibro] = useState({
nombrelibro: props.libro ? props.libro.nombrelibro : '',
autor: props.libro ? props.libro.autor : '',
cantidad: props.libro ? props.libro.cantidad : '',
precio: props.libro ? props.libro.precio : '',
fecha: props.libro ? props.libro.fecha : ''
});
Como usaremos el mismo componente FormularioDeLibro
para agregar y editar el libro, primero verificamos si el prop del libro
pasa o no usando el operador ternario.
Si el prop pasa, lo estamos configurando en el valor pasado, de lo contrario, una cadena vacía ('')
.
No se preocupe si parece complicado ahora. Lo comprenderá mejor una vez que construyamos algunas funciones iniciales.
Luego agregamos un estado para mostrar un mensaje de error y usamos la sintaxis de desestructuración de ES6 para hacer referencia a cada una de las propiedades dentro del estado de esta manera:
const [errorMsg, setErrorMsg] = useState('');
const { nombrelibro, autor, precio, cantidad } = libro;
Desde el componente FormularioDeLibro
, estamos devolviendo un Formulario donde ingresamos el nombre del libro, el autor del libro, la cantidad y el precio. Estamos usando el framework react-bootstrap para mostrar el formulario en un formato agradable.
Cada campo de entrada ha agregado un controlador onChange
que llama al método handleInputChange
.
Dentro del método handleInputChange
, agregamos una declaración de cambio para cambiar el valor del estado según el campo de entrada que se cambia.
Cuando escribimos algo en el campo de entrada de cantidad
, event.target.nombre
será cantidad
, por lo que el primer caso de switch coincidirá. Dentro de la declaración switch , estamos verificando si el valor ingresado es un número entero sin un punto decimal.
Si es así, solo actualizamos el estado como se muestra a continuación:
if (valor === '' || parseInt(valor) === +valor) {
setLibro((prevState) => ({
...prevState,
[nombre]: valor
}));
}
Entonces, el usuario no puede ingresar ningún valor decimal para el campo de entrada de cantidad.
Para el caso del switch de precio
, buscamos un número decimal con solo dos dígitos después del punto decimal. Así que hemos agregado una verificación de expresión regular que se ve así: value.match(/^\d{1,}(.\d{0,2})?$/)
.
Si el valor del precio coincide solo con la expresión regular, entonces actualizamos el estado.
Nota: Para los casos de switch de cantidad
y precio
, también estamos buscando valores vacíos como este: valor === ''
. Esto es para permitir que el usuario elimine por completo el valor ingresado si es necesario.
Sin esa verificación, el usuario no podrá eliminar el valor ingresado presionando Ctrl + A + Delete
.
Para todos los demás campos de entrada, se ejecutará el caso de switch predeterminado que actualizará el estado según el valor ingresado por el usuario.
A continuación, una vez que enviemos el formulario, se llamará al método handleOnSubmit
.
Dentro de este método, primero verificamos si el usuario ha ingresado todos los detalles usando el método de every
arreglo:
const todosLosCamposLlenos = valores.every((campo) => {
const valor = `${campo}`.trim();
return valor !== '' && valor !== '0';
});
El método de every
arreglo es uno de los métodos de arreglo más útiles en JavaScript.
Si se completan todos los valores, entonces estamos creando un objeto con todos los valores completos. También estamos llamando al método handleOnSubmit
pasando libro como argumento, de lo contrario, estamos configurando un mensaje de error.
El método handleOnSubmit
se pasa como prop del componente AgregaLibro
.
if (todosLosCamposLlenos) {
const libro = {
id: uuidv4(),
nombrelibro,
autor,
precio,
cantidad,
fecha: new Date()
};
props.handleOnSubmit(libro);
} else {
errorMsg = 'Por favor, rellene todos los campos.';
}
Tenga en cuenta que para crear una ID única llamamos al método uuidv4()
del paquete uuid npm.
Ahora, crea un nuevo archivo EnrutadorDeApp.js
dentro de la carpeta del enrutador con el siguiente contenido:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../componentes/Header';
import AgregaLibro from '../componentes/AgregaLibro';
import ListaDeLibros from '../componentes/ListaDeLibros';
const EnrutadorDeApp = () => {
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<Switch>
<Route component={ListaDeLibros} path="/" exact={true} />
<Route component={AgregaLibro} path="/add" />
</Switch>
</div>
</div>
</BrowserRouter>
);
};
export default EnrutadorDeApp;
Aquí, hemos configurado el enrutamiento para varios componentes como ListaDeLibros
y AgregaLibro
utilizando la biblioteca react-router-dom
.
Ahora, abra el archivo src/index.js
y agregue los siguientes contenidos dentro de él:
import React from 'react';
import ReactDOM from 'react-dom';
import EnrutadorDeApp from './enrutadores/EnrutadorDeApp';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.scss';
ReactDOM.render(<EnrutadorDeApp />, document.getElementById('root'));
Ahora, inicie la aplicación React ejecutando el siguiente comando desde la terminal:
yarn start
Verá la siguiente pantalla cuando acceda a la aplicación en http://localhost:3000/.


Como puede ver, podemos agregar correctamente el libro y mostrarlo en la consola.
Pero en lugar de iniciar sesión en la consola, agréguemoslo al almacenamiento local.
Cómo crear un hook personalizado para almacenamiento local
El almacenamiento local es increíble. Nos permite almacenar fácilmente datos de aplicaciones en el navegador y es una alternativa a las cookies para almacenar datos.
La ventaja de utilizar el almacenamiento local es que los datos se guardarán de forma permanente en la memoria caché del navegador hasta que los eliminemos manualmente para poder acceder a ellos incluso después de actualizar la página. Como sabrá, los datos almacenados en el estado React se perderán una vez que actualicemos la página.
Hay muchos casos de uso para el almacenamiento local, y uno de ellos es almacenar elementos del carrito de compras para que no se eliminen incluso si actualizamos la página.
Para agregar datos al almacenamiento local, usamos el método setItem
proporcionando una clave y un valor:
localStorage.setItem(clave, valor)
Tanto la clave como el valor deben ser una cadena. Pero también podemos almacenar el objeto JSON usando el método JSON.stringify
.
Cree un nuevo archivo usaAlmacenamientoLocal.js
dentro de la carpeta de hooks
con el siguiente contenido:
import { useState, useEffect } from 'react';
const usaAlmacenamientoLocal = (clave, valorInicial) => {
const [valor, setValor] = useState(() => {
try {
const almacenamientoLocal = window.localStorage.getItem(clave);
return almacenamientoLocal ? JSON.parse(lmacenamientoLocal) : initialValue;
} catch (error) {
return valorInicial;
}
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(valor));
}, [clave, valor]);
return [valor, setValor];
};
export default usaAlmacenamientoLocal;
Aquí, hemos usado un usaAlmacenamientoLocal
hook que acepta una clave y un valor inicial.
Para declarar el estado usando el useState
hook, estamos usando la inicialización diferida.
Por lo tanto, el código dentro de la función que se pasa a useState
se ejecutará solo una vez, incluso si el usaAlmacenamientoLocal
hook se llama varias veces en cada nueva representación de la aplicación.
Entonces, inicialmente estamos verificando si hay algún valor en el almacenamiento local con la clave
proporcionada y devolvemos el valor al analizarlo usando el método JSON.parse
:
try {
const valorLocal = window.localStorage.getItem(clave);
return valorLocal ? JSON.parse(valorLocal) : valorInicial;
} catch (error) {
return valorInicial;
}
Luego, si hay algún cambio en la clave
o el valor
, actualizaremos el almacenamiento local:
useEffect(() => {
window.localStorage.setItem(clave, JSON.stringify(valor));
}, [clave, valor]);
return [valor, setValor];
Luego, devolvemos el valor
almacenado en el almacenamiento local y la función setValor
a la que llamaremos para actualizar los datos del localStorage.
Cómo utilizar el hook de almacenamiento local
Ahora, usemos este usaAlmacenamientoLocal
hook para que podamos agregar o eliminar datos del almacenamiento local.
Abra el archivo EnrutadorDeApp.js
y use el hook usaAlmacenamientoLocal
dentro del componente:
import usaAlmacenamientoLocal from '../hooks/usaAlmacenamientoLocal';
const EnrutadorDeApp = () => {
const [libros, setLibros] = usaAlmacenamientoLocal('libros', []);
return (
...
)
}
Ahora, necesitamos pasar los libros
y setLibros
como props al componente AgregaLibro
para que podamos agregar el libro al almacenamiento local.
Así que cambia la ruta desde este código:
<Route component={AgregaLibro} path="/add" />
al siguiente código:
<Route
render={(props) => (
<AgregaLibro {...props} libros={libros} setLibros={setLibros} />
)}
path="/add"
/>
Aquí, estamos usando render props de representación para pasar los props predeterminados pasados por el enrutador React junto con los libros
y setLibros
.
Su archivo EnrutadorDeApp.js
completo se verá así ahora:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../componentes/Header';
import AgregaLibro from '../componentes/AgregaLibro';
import ListaDeLibros from '../componentes/ListaDeLibros';
import usaAlmacenamientoLocal from '../hooks/usaAlmacenamientoLocal';
const EnrutadorDeApp = () => {
const [libros, setLibros] = usaAlmacenamientoLocal('libros', []);
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<Switch>
<Route component={BooksList} path="/" exact={true} />
<Route
render={(props) => (
<AddBook {...props} libros={libros} setLibros={setLibros} />
)}
path="/add"
/>
</Switch>
</div>
</div>
</BrowserRouter>
);
};
export default EnrutadorDeApp;
Now open AgregaLibro.js
and replace its content with the following code:
import React from 'react';
import FormularioDeLibro from './FormularioDeLibro';
const AgregaLibro = ({ historia, libros, setLibros }) => {
const handleOnSubmit = (libro) => {
setLibros([libro, ...libros]);
historia.push('/');
};
return (
<React.Fragment>
<BookForm handleOnSubmit={handleOnSubmit} />
</React.Fragment>
);
};
export default AgregaLibro;
Primero, estamos usando la sintaxis de desestructuración de ES6 para acceder a los props de historia
, libros
y setLibros
en el componente.
React Router pasa automáticamente el prop de historia
a cada componente mencionado en <Route />
. Estamos pasando los props libros
y setLibros
del archivo EnrutadorDeApp.js
.
Estamos almacenando todos los libros agregados en un arreglo. Dentro del método handleOnSubmit
, estamos llamando a la función setLibros
pasando un arreglo agregando primero un libro recién agregado y luego distribuyendo todos los libros ya agregados en el arreglo de libros
como se muestra a continuación:
setLibros([libro, ...libros]);
Aquí, primero agrego el libro
recién agregado y luego distribuyo los libros
ya agregados porque quiero que el último libro se muestre primero cuando mostremos la lista de libros más tarde.
Pero puedes cambiar el orden si quieres así:
setLibros([...libros, libro]);
Esto agregará el libro recién agregado al final de todos los libros ya agregados.
Podemos usar el operador de propagación porque sabemos que los libros
son un arreglo (ya que lo hemos inicializado en un arreglo vacío []
en el archivo EnrutadorDeApp.js
como se muestra a continuación):
const [libros, setLibros] = usaAlmacenamientoLocal('libros', []);
Luego, una vez que el libro se agrega al almacenamiento local llamando al método setLibros
, dentro del método handleOnSubmit
estamos redirigiendo al usuario a la página Lista de libros
usando el método historia.push
:
historia.push('/');
Ahora, verifiquemos si podemos guardar los libros en el almacenamiento local o no.

Como puede ver, el libro se agrega correctamente al almacenamiento local (y puede confirmarlo en la pestaña de aplicaciones de las herramientas de desarrollo de Chrome).
Cómo mostrar libros agregados en el interfaz de usuario
Ahora, mostremos los libros agregados en el interfaz de usuario en el menú Lista de libros
.
Abra EnrutadorDeApp.js
y pase los libros
y setLibros
como props al componente ListaDeLibros.
Su archivo EnrutadorDeApp.js
se verá así ahora:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../componentes/Header';
import AgregaLibro from '../componentes/AgregaLibro';
import ListaDeLibros from '../componentes/ListaDeLibros';
import usaAlmacenamientoLocal from '../hooks/usaAlmacenamientoLocal';
const EnrutadorDeApp = () => {
const [libros, setLibros] = usaAlmacenamientoLocal('libros', []);
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<Switch>
<Route
render={(props) => (
<BooksList {...props} libros={libros} setLibros={setLibros} />
)}
path="/"
exact={true}
/>
<Route
render={(props) => (
<AddBook {...props} libros={libros} setLibros={setLibros} />
)}
path="/add"
/>
</Switch>
</div>
</div>
</BrowserRouter>
);
};
export default EnrutadorDeApp;
Aquí, acabamos de cambiar la primera ruta relacionada con el componente ListaDeLibros
.
Ahora, crea un nuevo archivo Libro.js
dentro de la carpeta de componentes
con el siguiente contenido:
import React from 'react';
import { Button, Card } from 'react-bootstrap';
const Libro = ({
id,
nombrelibro,
autor,
precio,
cantidad,
fecha,
manejarEliminarLibro
}) => {
return (
<Card style={{ width: '18rem' }} className="book">
<Card.Body>
<Card.Title className="book-title">{nombrelibro}</Card.Title>
<div className="book-details">
<div>Autor: {autor}</div>
<div>Cantidad: {cantidad} </div>
<div>Precio: {precio} </div>
<div>Fecha: {new Date(date).toDateString()}</div>
</div>
<Button variant="primary">Editar</Button>{' '}
<Button variant="danger" onClick={() => manejarEliminarLibro(id)}>
Eliminar
</Button>
</Card.Body>
</Card>
);
};
export default Libro;
Ahora, abre el archivo ListaDeLibros.js
y reemplaza su contenido con el siguiente código:
import React from 'react';
import _ from 'lodash';
import Libro from './Libro';
const ListaDeLibros = ({ libros, setLibros }) => {
const manejarEliminarLibro = (id) => {
setLibros(libros.filter((libro) => libro.id !== id));
};
return (
<React.Fragment>
<div className="book-list">
{!_.isEmpty(libros) ? (
libros.map((libro) => (
<Libro key={libro.id} {...libro} manejarEliminarLibro={manejarEliminarLibro} />
))
) : (
<p className="message">No hay libros disponibles. Por favor agregue algunos libros.</p>
)}
</div>
</React.Fragment>
);
};
export default ListaDeLibros;
En este archivo, estamos recorriendo los libros
usando el método de map
de arreglo y pasándolos como apoyo al componente Libro
.
Tenga en cuenta que también estamos pasando la función manejarEliminarLibro
como prop para que podamos eliminar cualquier libro que queramos.
Dentro de la función manejarEliminarLibro
, estamos llamando a la función setLibros
usando el método de filter
de arreglo para mantener solo los libros que no coinciden con la id
del libro proporcionada.
const manejarEliminarLibro = (id) => {
setLibros(libros.filter((libro) => libro.id !== id));
};
Ahora, si verifica la aplicación visitando http://localhost:3000/, podrá ver el libro agregado en el interfaz de usuario.

Agreguemos otro libro para verificar todo el flujo.

Como puede ver, cuando agregamos un nuevo libro, somos redirigidos a la página de la lista donde podemos eliminar el libro. Puede ver que se elimina instantáneamente de el interfaz de usuario y del almacenamiento local.
Además cuando refrescamos la página los datos no se pierden. Ese es el poder del almacenamiento local.
Cómo editar un libro
Ahora tenemos la funcionalidad de agregar y eliminar para los libros. Agreguemos una forma de editar los libros que tenemos.
Abra Libro.js
y cambie el siguiente código:
<Button variant="primary">Editar</Button>{' '}
a este código:
<Button variant="primary" onClick={() => historia.push(`/edit/${id}`)}>
Editar
</Button>{' '}
Aquí, hemos agregado un controlador onClick
para redirigir al usuario a la ruta /edit/id_del_libro
cuando hacemos clic en el botón editar.
Pero no tenemos acceso al objeto historia
en el componente Libro
porque la propiedad historia se pasa solo a los componentes que se mencionan en la <Route />
.
Estamos renderizando el componente Libro
dentro del componente ListaDeLibros
para que podamos tener acceso a la historia
solo dentro del componente ListaDeLibros
. Luego podemos pasarlo como prop al componente Libro
.
Pero en lugar de eso, el React router proporciona una manera fácil de usar el useHistory
hook.
Importe el useHistory
hook en la parte superior del archivo Libro.js
:
import { useHistory } from 'react-router-dom';
y dentro del componente Libro
, llame al useHistory
hook.
const Book = ({
nombrelibro,
autor,
precio,
cantidad,
fecha,
manejarEliminarLibro
}) => {
const historia = useHistory();
...
}
Ahora tenemos acceso al objeto de historia
dentro del componente Libro
.
Todo su archivo Libro.js
se ve así ahora:
import React from 'react';
import { Button, Card } from 'react-bootstrap';
import { useHistory } from 'react-router-dom';
const Libro = ({
id,
nombrelibro,
autor,
precio,
cantidad,
fecha,
manejarEliminarLibro
}) => {
const historia = useHistory();
return (
<Card style={{ width: '18rem' }} className="book">
<Card.Body>
<Card.Title className="book-title">{nombrelibro}</Card.Title>
<div className="book-details">
<div>Autor: {autor}</div>
<div>Cantidad: {cantidad} </div>
<div>Precio: {precio} </div>
<div>Fecha: {new Date(date).toDateString()}</div>
</div>
<Button variant="primary" onClick={() => historia.push(`/edit/${id}`)}>
Editar
</Button>{' '}
<Button variant="danger" onClick={() => manejarEliminarLibro(id)}>
Eliminar
</Button>
</Card.Body>
</Card>
);
};
export default Libro;
Crea un nuevo archivo llamado EditarLibro.js
dentro de la carpeta de componentes
con el siguiente contenido:
import React from 'react';
import FormularioDeLibro from './FormularioDeLibro';
import { useParams } from 'react-router-dom';
const EditarLibro = ({ historia, libros, setLibros }) => {
const { id } = useParams();
const libroParaEditar = libros.find((libro) => libro.id === id);
const handleOnSubmit = (libro) => {
const librosFiltrados = books.filter((libro) => libro.id !== id);
setLibros([libro, ...librosFiltrados]);
historia.push('/');
};
return (
<div>
<FormularioDeLibro libro={libroParaEditar} handleOnSubmit={handleOnSubmit} />
</div>
);
};
export default EditarLibro;
Aquí, para el controlador onClick
del botón Editar, estamos redirigiendo al usuario a la ruta /edit/alguna_id
, pero esa ruta aún no existe. Así que vamos a crear eso primero.
Abra EnrutadorDeApp.js
y antes de la etiqueta final del Switch
agregue dos rutas más:
<Switch>
...
<Route
render={(props) => (
<EditarLibro {...props} libros={libros} setLibros={setLibros} />
)}
path="/edit/:id"
/>
<Route component={() => <Redirect to="/" />} />
</Switch>
La primera Ruta es para el componente EditarLibro
. Aquí, la ruta se define como /edit/:id
donde :id
representa cualquier id aleatorio.
La segunda ruta es para manejar todas las demás rutas que no coinciden con ninguna de las rutas mencionadas.
Entonces, si accedemos a cualquier ruta aleatoria como /ayuda
o /contacto
, redirigiremos al usuario a la ruta /
que es el componente ListaDeLibros
.
Todo su archivo EnrutadorDeApp.js
se ve así ahora:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../componentes/Header';
import AgregaLibro from '../componentes/AgregaLibro';
import ListaDeLibros from '../componentes/ListaDeLibros';
import usaAlmacenamientoLocal from '../hooks/usaAlmacenamientoLocal';
const EnrutadorDeApp = () => {
const [libros, setLibros] = usaAlmacenamientoLocal('libros', []);
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<Switch>
<Route
render={(props) => (
<ListaDeLibros {...props} libros={libros} setLibros={setLibros} />
)}
path="/"
exact={true}
/>
<Route
render={(props) => (
<AgregaLibro {...props} libros={libros} setLibros={setLibros} />
)}
path="/add"
/>
<Route
render={(props) => (
<EditarLibro {...props} libros={libros} setLibros={setLibros} />
)}
path="/edit/:id"
/>
<Route component={() => <Redirect to="/" />} />
</Switch>
</div>
</div>
</BrowserRouter>
);
};
export default EnrutadorDeApp;
Ahora, verifiquemos la funcionalidad de editar de la aplicación.

Como puede ver, pudimos editar el libro correctamente. Vamos a entender cómo funciona esto.
Primero, dentro del archivo EnrutadorDeApp.js
tenemos una ruta como esta:
<Route
render={(props) => (
<EditarLibro {...props} libros={libros} setLibros={setLibros} />
)}
path="/edit/:id"
/>
y dentro del archivo Libro.js
tenemos un botón de editar como este:
<Button variant="primary" onClick={() => historia.push(`/edit/${id}`)}>
Editar
</Button>
Entonces, cada vez que hacemos clic en el botón Editar para cualquiera de los libros, estamos redirigiendo al usuario al componente EditarLibro
utilizando el método historia.push
al pasar la identificación del libro que se editará.
Luego, dentro del componente EditarLibro
, estamos usando el enlace useParams proporcionado por react-router-dom
para acceder a props.params.id
.
Así que las dos líneas siguientes son idénticas:
const { id } = useParams();
// la línea de código de arriba es igual que el código de abajo
const { id } = props.match.params;
Una vez que hemos obtenido esa id
, estamos usando el método de find
de arreglo para encontrar el libro en particular de la lista de libros con la id
proporcionada coincidente.
const libroParaEditar = libros.find((libro) => libro.id === id);
y este libro en particular lo estamos pasando al componente FormularioDeLibro
como prop de libro
:
<FormularioDeLibro libro={libroParaEditar} handleOnSubmit={handleOnSubmit} />
Dentro del componente FormularioDeLibro
, hemos definido el estado como se muestra a continuación:
const [libro, setLibro] = useState({
nombrelibro: props.libro ? props.libro.nombrelibro : '',
autor: props.libro ? props.libro.autor : '',
cantidad: props.libro ? props.libro.cantidad : '',
precio: props.libro ? props.libro.precio : '',
fecha: props.libro ? props.libro.fecha : ''
});
Aquí, estamos comprobando si existe el prop del libro
. En caso afirmativo, estamos usando los detalles del libro pasado como prop; de lo contrario, estamos inicializando el estado con un valor vacío ('')
para cada propiedad.
Y cada uno de los elementos de entrada ha proporcionado un prop de valor
que estamos configurando desde el estado como este:
<Form.Control
...
valor={nombrelibro}
...
/>
Pero podemos mejorar un poco la sintaxis de useState
dentro del componente FormularioDeLibro
.
En lugar de configurar directamente un objeto para el useState
hook, podemos usar la inicialización diferida como se hizo en el archivo usaAlmacenamientoLocal.js
.
Así que cambia el siguiente código:
const [libro, setLibro] = useState({
nombrelibro: props.libro ? props.libro.nombrelibro : '',
autor: props.libro ? props.libro.autor : '',
cantidad: props.libro ? props.libro.cantidad : '',
precio: props.libro ? props.libro.precio : '',
fecha: props.libro ? props.libro.fecha : ''
});
a este código:
const [libro, setLibro] = useState(() => {
return {
nombrelibro: props.libro ? props.libro.nombrelibro : '',
autor: props.libro ? props.libro.autor : '',
cantidad: props.libro ? props.libro.cantidad : '',
precio: props.libro ? props.libro.precio : '',
fecha: props.libro ? props.libro.fecha : ''
};
});
Debido a este cambio, el código para establecer el estado no se ejecutará en cada nueva representación de la aplicación. Solo se ejecutará una vez cuando se monte el componente.
Tenga en cuenta que la nueva representación del componente ocurre en cada cambio de estado o prop.
Si revisa la aplicación, verá que la aplicación funciona exactamente como antes sin ningún problema. Pero acabamos de mejorar un poco el rendimiento de la aplicación.
Cómo usar el API de contexto de React
Ahora hemos terminado de desarrollar toda la funcionalidad de la aplicación. Pero si revisas el archivo EnrutadorDeApp.js
, verás que cada Ruta se ve un poco complicada. Esto se debe a que estamos pasando los mismos libros
y props de setLibros
a cada uno de los componentes mediante el patrón de render props.
Entonces podemos usar la API React Context para simplificar este código.
Tenga en cuenta que este es un paso opcional. No necesitas usar el Context API ya que estamos pasando los accesorios solo un nivel de profundidad y el código actual funciona perfectamente bien y no hemos usado ningún enfoque incorrecto para pasar los props.
Pero solo para simplificar el código del enrutador y darle una idea sobre cómo aprovechar el poder de el API React Context, la usaremos en nuestra aplicación.
Crea un nuevo archivo ContextoDeLosLibros.js
dentro de la carpeta contexto
con el siguiente contenido:
import React from 'react';
const ContextoDeLosLibros = React.createContext();
export default ContextoDeLosLibros;
Ahora, dentro del archivo EnrutadorDeApp.js, importe el contexto exportado anterior.
import ContextoDeLosLibros from '../contexto/ContextoDeLosLibros';
y reemplace el componente EnrutadorDeApp con el siguiente código:
const EnrutadorDeApp = () => {
const [libros, setLibros] = usaAlmacenamientoLocal('libros', []);
return (
<BrowserRouter>
<div>
<Header />
<div className="main-content">
<ContextoDeLosLibros.Provider value={{ libros, setLibros }}>
<Switch>
<Route component={ListaDeLibros} path="/" exact={true} />
<Route component={AgregaLibro} path="/add" />
<Route component={EditarLibro} path="/edit/:id" />
<Route component={() => <Redirect to="/" />} />
</Switch>
</ContextoDeLosLibros.Provider>
</div>
</div>
</BrowserRouter>
);
};
Aquí, hemos vuelto a convertir el patrón de render props a las rutas normales y hemos agregado el bloque Switch
completo dentro del componente ContextoDeLosLibros.Provider
de esta manera:
<ContextoDeLosLibros.Provider value={{ libros, setLibros }}>
<Switch>
...
</Switch>
</ContextoDeLosLibros.Provider>
Aquí, para el componente ContextoDeLosLibros.Provider
, proporcionamos un prop de value
al pasar los datos a los que queremos acceder dentro de los componentes mencionados en la Route.
Entonces, ahora, cada componente declarado como parte de Route podrá acceder a los libros
y setLibros
a través de el Context API.
Ahora, abra el archivo ListaDeLibros.js
y elimine los libros
y los props de setLibros
que están desestructurados, ya que ya no estamos pasando los props directamente.
Importe el ContextoDeLosLibros
y useContext
en la parte superior del archivo:
import React, { useContext } from 'react';
import ContextoDeLosLibros from '../contexto/ContextoDeLosLibros';
Y encima de la función manejarEliminarLibro
, agrega el siguiente código:
const { libros, setLibros } = useContext(ContextoDeLosLibros);
Aquí, estamos sacando los libros
y los props de setLibros
del ContextoDeLosLibros
usando el useContext
hook.
Todo su archivo ListaDeLibros.js
se verá así:
import React, { useContext } from 'react';
import _ from 'lodash';
import Libro from './Libro';
import ContextoDeLosLibros from '../contexto/ContextoDeLosLibros';
const ListaDeLibros = () => {
const { libros, setLibros } = useContext(ContextoDeLosLibros);
const manejarEliminarLibro = (id) => {
setLibros(libros.filter((libro) => libro.id !== id));
};
return (
<React.Fragment>
<div className="book-list">
{!_.isEmpty(libros) ? (
libros.map((libro) => (
<Libro key={libro.id} {...libro} manejarEliminarLibro={manejarEliminarLibro} />
))
) : (
<p className="message">No hay libros disponibles. Por favor agregue algunos libros.</p>
)}
</div>
</React.Fragment>
);
};
export default ListaDeLibros;
Ahora, haz cambios similares en el archivo AgregaLibro.js
.
Todo su archivo AgregaLibro.js
se verá así:
import React, { useContext } from 'react';
import FormularioDeLibro from './FormularioDeLibro';
import ContextoDeLosLibros from '../contexto/ContextoDeLosLibros';
const AgregaLibro = ({ historia }) => {
const { libros, setLibros } = useContext(ContextoDeLosLibros);
const handleOnSubmit = (libro) => {
setLibros([libro, ...libros]);
historia.push('/');
};
return (
<React.Fragment>
<FormularioDeLibro handleOnSubmit={handleOnSubmit} />
</React.Fragment>
);
};
export default AgregaLibro;
Tenga en cuenta que aquí, todavía estamos usando la desestructuración para el prop de historia
. Solo hemos eliminado los libros
y setLibros
de la sintaxis de desestructuración.
Ahora, haz cambios similares en el archivo EditarLibro.js
.
Todo su archivo EditarLibro.js
se verá así:
import React, { useContext } from 'react';
import FormularioDeLibro from './FormularioDeLibro';
import { useParams } from 'react-router-dom';
import ContextoDeLosLibros from '../contexto/ContextoDeLosLibros';
const EditarLibro = ({ historia }) => {
const { libros, setLibros } = useContext(ContextoDeLosLibros);
const { id } = useParams();
const libroParaEditar = libros.find((libro) => libro.id === id);
const handleOnSubmit = (libro) => {
const librosFiltrados = libros.filter((libro) => libro.id !== id);
setLibros([libro, ...librosFiltrados]);
historia.push('/');
};
return (
<div>
<FormularioDeLibro libro={libroParaEditar} handleOnSubmit={handleOnSubmit} />
</div>
);
};
export default EditarLibro;
Si revisa la aplicación, verá que funciona exactamente como antes, pero ahora estamos usando la React Context API.

¡Gracias por leer!
Puedes encontrar el código completo de esta aplicación en este repositorio.
¿Quieres mantenerte al día con el contenido regular sobre JavaScript, React, Node.js? Sígueme en LinkedIn.