Original article: https://www.freecodecamp.org/news/react-crud-app-how-to-create-a-book-management-app-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á:

  1. Cómo realizar operaciones CRUD
  2. Cómo usar React Router para navegar entre rutas
  3. Cómo usar React Context API para pasar datos a través de rutas
  4. Cómo crear un hook personalizado en React
  5. Cómo almacenar datos en el almacenamiento local para conservarlos incluso después de actualizar la página
  6. 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/.

initial_screen
add_book

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.

added_local_storage

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.

list_page

Agreguemos otro libro para verificar todo el flujo.

add_delete

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.

edit_book

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.

edit_delete

¡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.