Artículo original escrito por Yogesh Chavan
Artículo original React CRUD App Tutorial – How to Build a Book Management App in React from Scratch
Traducido y adaptado por Ezequiel Castellanos

En este artículo, construirás una aplicación de administración de libros en React desde cero.

**Nota: ABM es un acrónimo que se refiere a las cuatro funciones que se consideran necesarias para  implentar una aplicacion de almacenamiento persistente: Agregar, Borrar y Modificar. ABM es equiválete lo que es conocido como CRUD en ingles, lo cual es: Create, Read, Update y Delete.

Al crear esta applicación, aprendaras:

  1. Como realizar operaciones ABM
  2. Como usar React Router para la navegacion entre rutas
  3. Como usar la API de contexto de React para pasar datos a traves de rutas
  4. Como crear un Hook personalizado en React
  5. Como almacenar datos en almacenamiento local para persistirlos incluso despues de actualizar la pagina.
  6. Como administrar los datos almacenados en el almacenamiento local mediate un hook personalizado

y mucho mas.

Usaremos React Hooks para construir esta aplicación. Así que si eres nuevo en React Hooks, echa un vistazo a mi artículo de introdución a React Hooks para aprender los conceptos básicos.

¿Quieres aprender Redux desde el principio absoluto y construir una aplicación de pedidos de alimentos desde cero? Echa un vistazo al curso de Masterización Redux

Configuración Inicial

Crea un nuevo proyecto usando create-react-app:

npx create-react-app book-management-app

Una vez creado el proyecto, elimina todos los archivos de la carpeta src y crea el index.js y styles.scss dentro de la carpeta src. Además, crea components,  context, hooks y carpetas de router dentro de la carpeta src.

Instalar 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

Abre styles.scss y añade los estilos que tiene aquí.

Como crear las páginas de inicio

Crea un nuevo archivo Header.js dentro de la carpeta components con el siguiente contenido:

import React from 'react';
import { NavLink } from 'react-router-dom';

const Header = () => {
  return (
    <header>
      <h1>Aplicación de Gestió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">
          Añade Libro
        </NavLink>
      </div>
    </header>
  );
};

export default Header;

Aquí, hemos agregado dos enlaces de navegación usando el componente NavLink de react-router-dom: uno para ver una lista de todos los libros, y el otro para agregar un nuevo libro.

Estamos usando 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.

Crea un nuevo llamado BooksList.js dentro de la carpeta components con el siguiente contenido:

import React from 'react';

const BooksList = () => {
  return <h2>Lista de libros</h2>;
};

export default BooksList;

Crea un nuevo archivo llamado AddBook.js dentro de la carpeta components con el siguiente contenido:

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;

En este archivo, estamos mostrando un componente BookForm (que aún no hemos creado).

Para el componente BookForm estamos pasando el métodohandleOnSubmit para que podamos hacer algún procesamiento más tarde una vez que enviemos el formulario.

Ahora, crea un nuevo archivo BookForm.js dentro de la carpeta components con el siguiente contenido:

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="Introduzce el nombre del libro"
            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="Introduzce el nombre del autor"
            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="Introduzce la cantidad disponible"
            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="Introduzce el precio del libro"
            onChange={handleInputChange}
          />
        </Form.Group>
        <Button variant="primary" type="submit" className="submit-btn">
          Someter
        </Button>
      </Form>
    </div>
  );
};

export default BookForm;

Vamos a entender lo que estamos haciendo aquí.

Inicialmente, hemos definido un estado como un objeto usando el hook useState para almacenar todos los detalles introducidos de esta manera:

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 utilizaremos el mismo componente BookForm para agregar y editar el libro, primero comprobemos si la prop de book pasa o no usando el operador ternario.

Si pasa la prop, la establecemos al valor pasado, de lo contrario una cadena vacía ('').

No te preocupes si parece complicado ahora. Lo entenderás mejor una vez que construyamos alguna funcionalidad inicial.

Luego agregamos un estado para mostrar un mensaje de error y usamos la sintaxis de desestructuración ES6 para referir cada una de las propiedades dentro del estado de esta manera:

const [errorMsg, setErrorMsg] = useState('');
const { bookname, author, price, quantity } = book;

Desde el componente BookForm 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 buen formato.

Cada campo de entrada ha añadido un controlador onChange que llama al método handleInputChange.

Dentro del método handleInputChange, hemos agregado una instrucción "switch" para cambiar el valor del estado en función del campo de entrada que se cambia.

Cuando escribimos cualquier cosa en el campo de entrada de quantity, event.target.name será la quantity por lo que la primera caja del interruptor  coincidirá. Dentro de ese caso interruptor, estamos comprobando para ver si el valor introducido es un entero sin un punto decimal.

Si es así, solo actualizamos el estado como se muestra a continuación:

if (value === '' || parseInt(value) === +value) {
  setBook((prevState) => ({
    ...prevState,
    [name]: value
  }));
}

Por lo tanto, el usuario no puede ingresar ningún valor decimal para el campo de entrada de quantity.

Para el "switch case" price, estamos comprobando un número decimal con solo dos dígitos después del punto decimal. Así que hemos añadido una comprobación de expresión regular que se parece a esto: value.match(/^\d{1,}(\.\d{0,2})?$/).

Si el valor del precio "price" coincide con la expresión regular solo entonces actualizamos el estado.

Nota: Para los dos casos de switch quantity y price, también estamos verificando valores vacíos como este: value === ''.  Esto es para permitir el valor introducido si es necesario.

Si esa comprobación, el usuario no podrá eliminar el valor ingresado presionando Ctrl + A + Delete.

Para todos los demás campos de entrada, se ejecutara el caso switch predeterminado que actualizara el estado en función del valor introducido por el usuario.

A continuación, una vez que enviemos el formulario, se llamara al método handleOnSubmit.

Dentro de este método, primero verificamos si el usuario ha ingresado todos los detalles usando el método de arreglo every:

const allFieldsFilled = values.every((field) => {
  const value = `${field}`.trim();
  return value !== '' && value !== '0';
});

El método de arreglo every es uno de los métodos de arreglos más útiles en JavaScript.

Echa un vistazo a mi artículo aquí para aprender sobre los métodos de arreglos de JavaScript más útiles junto con su compatibilidad con el navegador.

Si todos los valores están rellenados, entonces estamos creando un objeto con todos los valores rellenados. También estamos llamando al método handleOnSubmit pasando "book" como argumento, de lo contrario estamos configurando un mensaje de error.

El método handleOnSubmit se pasa como prop desde el 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.';
}

Ten en cuenta que para crear un ID único estamos llamando al método uuidv4()  desde el paquete uuid npm.

Ahora, crea un nuevo archivo AppRouter.js dentro de la carpeta del router con el siguiente contenido:

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;

Aquí, hemos configurado el enrutamiento para varios componentes comoBooksList y AddBook utilizando la biblioteca react-router-dom.

Si eres nuevo en React Router, echa un vistazo a mi curso gratuito de introducción de React Router.

Ahora, abre el archivo src/index.js y agrega el siguiente contenido dentro de él:

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'));

Ahora, inicia la aplicación React ejecutando el siguiente comando desde el terminal:

yarn start

Veras la siguiente pantalla cuando accedas la aplicación en http://localhost:3000/.

initial_screen
add_book

Como puedes ver, podemos agregar correctamente el libro y mostrarlo en la consola.

Pero en lugar de iniciar sesión en la consola, vamos a agregarlo a un almacenamiento local.

Cómo crear un hook personalizado para el 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 usar almacenamiento local es que los datos se guardaran permanentemente en la cache del navegador hasta que los eliminemos manualmente para que podamos acceder a ellos incluso después de actualizar la página.  Como puede saber, los datos almacenados en el estado de 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 artículos del carrito de compras para que no se eliminen incluso si actualizamos la página.

Para agregar datos al almacenamiento local, utilizamos el método setItem al proporcionar una clave y un valor:

localStorage.setItem(key, value)
Tanto la clave como el valor tienen que ser una cadena. Pero también  podemos almacenar el objeto JSON usando el método JSON.stringify.

Para obtener más información sobre el almacenamiento local y sus diversas aplicaciones en detalle, consulta este artículo.

Crear un nuevo archivo useLocalStorage.js dentro de la carpeta hooks con el siguiente contenido:

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;

Aquí, hemos utilizado un hook useLocalStorage que acepta una key y initialValue.

Para declarar estado usando el hook useState, estamos usando inicialización perezosa (lazy initialization).

Por lo tanto, el código dentro de la función pasada a useState se ejecutará solo una vez, incluso si el hook useLocalStorage se llama varias veces en cada re-renderización de la aplicación.

Así que inicialmente estamos comprobando para ver si hay algún valor en el almacenamiento local con la key proporcionada y devolver el valor mediante el análisis utilizando el JSON.parse:

try {
  const localValue = window.localStorage.getItem(key);
  return localValue ? JSON.parse(localValue) : initialValue;
} catch (error) {
  return initialValue;
}

Luego, si hay algún cambio en la key o en value, actualizaremos el almacenamiento local:

useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);

return [value, setValue];

Luego estamos devolviendo el value almacenado en el almacenamiento local y la función setValue que llamaremos para actualizar los datos de localStorage.

Cómo usar el hook de almacenamiento local

Ahora, vamos a usar este hook useLocalStorage para que podamos agregar o eliminar datos del almacenamiento local.

Abre el archivo AppRouter.js y utiliza el hook useLocalStorage dentro del componente:

import useLocalStorage from '../hooks/useLocalStorage';

const AppRouter = () => {
 const [books, setBooks] = useLocalStorage('books', []);
 
 return (
  ...
 )
}

Ahora, necesitamos pasar los books y setBooks como props al componente AddBook para que podamos agregar el libro al almacenamiento local.

Así que cambia la ruta desde este código:

<Route component={AddBook} path="/add" />

al siguiente código:

<Route
  render={(props) => (
    <AddBook {...props} books={books} setBooks={setBooks} />
  )}
  path="/add"
/>

Aquí, estamos usando el patrón props renderizado para pasar los props predeterminados que pasarón por React router junto con los books y setBooks.

Echa un vistazo a mi curso gratuito de introducción de React Router para comprender mejor este patrón de accesorios de renderizado y la importancia de usar la palabra clave de renderizado en lugar de component.

Todo tu archivo AppRouter.js se verá así ahora:

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;

Ahora abre AddBook.js y sustituir tu contenido por el siguiente 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;

En primer lugar, estamos utilizando la sintaxis de desestructuración ES6 para acceder a la history, books y props setBooks en el componente.

El prop de history es pasado automáticamente por el React Router a cada componente mencionado en el <Route />. Estamos pasando los props books y setBooks desde el archivo AppRouter.js.

Estamos almacenando todos los libros agregados en un arreglo. Dentro del método handleOnSubmit, estamos llamando a la función setBooks pasando un arreglo agregando primero un libro recién agregado y luego extendiendo todos los libros ya agregados en el arreglo de books como se muestra a continuación:

setBooks([book, ...books]);

Aquí, estoy agregando el book recién agregad primero y luego extendiendo los books 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í:

setBooks([...books, book]);

Esto agregará el libro recién agregado al final de todos los libros ya agregados.

Podemos usar el operador spread porque sabemos que books es un arreglo (ya que lo hemos inicializado a un arreglo vacío [] en el archivo AppRouter.js como se muestra a continuación):

 const [books, setBooks] = useLocalStorage('books', []);

Luego, una vez que el libro se agrega al almacenamiento local llamando al método setBooks, dentro del método handleOnSubmit estamos redirigiendo al usuario a la página de Books List utilizando el método history.push:

history.push('/');

Ahora verifiquemos si pudimos guardar los libros al almacenamiento local o no.

added_local_storage

Como puedes ver, el libro se está agregando correctamente al almacenamiento local (y puedes confirmar esto en la pestaña aplicaciones de Chrome dev tools).

Cómo mostrar libros agregados en la interfaz de usuario

Ahora, vamos a mostrar los libros agregados en la interfaz de usuario en el menú Books List.

Abre el  AppRouter.js y pasa los books y setBooks como props al componente BooksList.

Tu archivo AppRouter.js se verá así ahora:

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;

Aquí, acabamos de cambiar la primera ruto relacionada con el componente BooksList.

Ahora, crea un nuevo archivo llamado Book.js dentro de la carpeta components con el siguiente contenido:

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;

Ahora, abre el archivo BooksList.js y reemplaza su contenido con el siguiente 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;

En este archivo, estamos haciendo un bucle sobre los books usando el método map de arreglo y pasándolos como un prop al componente Book.

Tenga en cuenta que también estamos pasando la función handleRemoveBook como prop para que podamos eliminar cualquier libro que queramos.

Dentro de la función handleRemoveBook, estamos llamando a la función setBooks utilizando el método de arreglo filter para solo quedarnos con los libros que no coincidan con el id del libro proporcionado.

const handleRemoveBook = (id) => {
    setBooks(books.filter((book) => book.id !== id));
};

Ahora si revisas la aplicación ingresando a http://localhost:3000/, vas a ver el libro agregado en la interfaz de usuario.

list_page

Agreguemos otro libro para verificar el flujo completo.

add_delete

Como puedes ver, cuando agregamos un libro nuevo, nos redirige a la página de listado donde podemos eliminar el libro. Puedes ver que se ha eliminado instantáneamente de la interfaz de usuario como también del almacenamiento local.

Además, cuando refresquemos la página, los datos no se pierden. Ese es el poder del almacenamiento local.

Cómo editar un libro

Ahora tenemos funcionalidad para agregar y borrar los libros. Agreguemos una manera de editar los libros que tenemos.

Abre Book.js y cambia el código de abajo:

<Button variant="primary">Edit</Button>{' '}

a este código:

<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
  Edit
</Button>{' '}

Aquí agregamos un manejador onClick para redirigir al usuario a la ruta /edit/id_of_the_book cuando hacemos clik en el botón editar.

Pero no tenemos acceso al objeto history dentro del componente Book porque la prop history se pasa solo a los componentes que son mencionados en el <Route />.

Estamos renderizando el componente Book dentro del componente BooksList para que podamos tener acceso a history solo dentro del componente BooksList. Después podemos pasarlo como prop al componente Book.

Pero en lugar de eso, React router nos proporciona una manera fácil utilizando el hook useHistory.

Importar el hook useHistory al inicio del archivo Book.js:

import { useHistory } from 'react-router-dom';

y dentro del componente Book, llamar al hook useHistory.

const Book = ({
  id,
  bookname,
  author,
  price,
  quantity,
  date,
  handleRemoveBook
}) => {
  const history = useHistory();
  ...
}

Ahora tenemos acceso al objeto history dentro del componente Book.

Tu archivo completo Book.js ahora se ve así:

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;

Crea un nuevo archivo llamado EditBook.js dentro de la carpeta components con el siguiente contenido:

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;

Aquí para el manejador onClick del botón Edit, estamos redirigiendo al usuario a la ruta /edit/some_id – pero tal ruta todavía no existe. Así que creemos eso primero.

Abre AppRouter.js y antes de la etiqueta de cierre del Switch agrega dos rutas más:

<Switch>
...
<Route
  render={(props) => (
    <EditBook {...props} books={books} setBooks={setBooks} />
  )}
  path="/edit/:id"
/>
<Route component={() => <Redirect to="/" />} />
</Switch>

El primer Route es para el componente EditBook. Aquí la ruta se define como /edit/:id donde :id representa cualquier id aleatorio.

El segundo Route es para manejar todas las otras rutas que no coincidan con alguna de las rutas mencionadas.

Por lo tanto, si accedemos a cualquier ruta aleatoria como /help o /contact entonces vamos a redirigir al usuario a la ruta / que es el componente BooksList.

Tu archivo completo AppRouter.js ahora se ve así:

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;

Ahora verifiquemos la funcionalidad de edición de la aplicación.

edit_book

Como podrás ver, podemos editar el libro exitosamente. Entendamos cómo funciona esto.

Primero, dentro del archivo AppRouter.js tenemos una ruta que se ve así:

<Route
  render={(props) => (
    <EditBook {...props} books={books} setBooks={setBooks} />
  )}
  path="/edit/:id"
/>

y dentro del archivo Book.js tenemos un botón de edición que se ve así:

<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
  Edit
</Button>

Entonces, cada vez que hacemos clic en el botón Edit para cualquiera de los libros, estamos redirigiendo al usuario al componente EditBook utilizando el método history.push al pasar el id del libro a ser editado.

Luego dentro del componente EditBook estamos usando el hook useParams proporcionado por react-router-dom para acceder a props.params.id.

Por lo tanto las dos líneas de abajo son idénticas.

const { id } = useParams();

// the above line of code is same as the below code

const { id } = props.match.params;

Una vez que tengamos ese id, estamos usando el método de arreglo find para encontrar el libro particular de la lista de libros que coincida con el id provisto.

const bookToEdit = books.find((book) => book.id === id);

y este libro particular que estamos pasando al componente BookForm como una prop book:

<BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />

Dentro del componente BookForm, hemos definido el estado, tal como se muestra abajo:

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 : ''
});

Aquí estamos verificando si la prop book existe. Si existe entonces usaremos los detalles del libro pasado como prop, de lo contrario vamos a inicializar el estado con un valor vacío ('') para cada propiedad.

Cada uno de los elementos input ha proporcionado una propiedad value la cual estamos fijando desde el estado de esta manera:

<Form.Control
  ...
  value={bookname}
  ...
/>

Pero podemos mejorar un poco la sintáxis del useState dentro del componente BookForm.

En vez de fijar directamente un objeto para el hook useState, podemos utilizar inicialización perezosa (lazy initialization) tal como se hizo en el archivo useLocalStorage.js.

Entonces cambiemos el código debajo:

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 : ''
});

a 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 : ''
  };
});

Debido a este cambio el código para establecer el estado no se ejecutará en cada re-renderización de la aplicación. Solo se ejecutará una vez cuando el componente es montado.

Tenga en cuenta que la re-renderización de un componente ocurre en cada cambio de estado o propiedades.

Si verificas la aplicación verás que la aplicación funciona exactamente como lo hizo antes sin ningún problema. Pero hemos mejorado el rendimiento de la aplicación un poco.

¿Cómo utilizar la API Context de React?

Hemos terminado de armar la funcionalidad de toda la aplicación. Pero si vemos el archivo AppRouter.js, te darás cuenta de que todas las Route se ven un poco complicadas. Esto se debe a que estamos pasando las mismas propiedades books y setBooks a cada uno de los componentes usando el patrón render props.

Entonces podemos usar la API Context de React para simplificar este código.

Ten en cuenta que este es un paso opcional. No necesitas utilizar la API Context ya que estamos pasando las props solo un nivel de profundidad y el código actual está funcionando perfectamente y no hemos usado ningún enfoque equivocado para pasar las props.

Pero solo para que el código Router sea más simple y para darte una idea de como aprovechar el poder de la API Context, vamos a usarlo en nuestra aplicación.

Crea un nuevo archivo BooksContext.js dentro de la carpeta context con el siguiente contenido:

import React from 'react';

const BooksContext = React.createContext();

export default BooksContext;

Ahora dentro del archivo AppRouter.js importa el contexto exportado arriba.

import BooksContext from '../context/BooksContext';

y reemplaza el componente AppRouter con el código de abajo:

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>
  );
};

Aquí hemos convertido el patrón render props de vuelta a las rutas normales y agregamos el bloque Switch completo dentro del componente BooksContext.Provider de esta manera:

<BooksContext.Provider value={{ books, setBooks }}>
 <Switch>
 ...
 </Switch>
</BooksContext.Provider>

Aquí, para el componente BooksContext.Provider hemos proporcionado una prop value al pasarle la información que queremos acceder dentro del componente mencionado en el Route.

Entonces ahora cada componente declarado como parte de Route podrá acceder a books y setBooks por medio de la API Context.

Ahora abramos el archivo BooksList.js y quitemos las props books y setBooks que son desestructuradas, ya que no estanos pasando directamente las props.

Importa BooksContext y useContext al inicio del archivo:

import React, { useContext } from 'react';
import BooksContext from '../context/BooksContext';

Y arriba de la función handleRemoveBook agrega el siguiente código:

const { books, setBooks } = useContext(BooksContext);

Aquí extraemos las props books y setBooks del BooksContext utilizando el hook useContext .

Tu archivo BooksList.js completo se verá así:

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;

Ahora realicemos cambios similares al archivo AddBook.js .

Tu archivo AddBook.js completo se verá así:

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;

Ten en cuenta que todavía estamos usando desestructuración para la prop history. Solo hemos eliminado books y setBooks de la sintaxis de desestructuración.

Ahora hagamos cambios similares al archivo EditBook.js .

Tu archivo EditBook.js completo se verá así:

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;

Si verificas la aplicación podrás ver que funciona exactamente como antes, pero ahora estamos utilizando la API Context de React.

edit_delete
Si quieres entender la API Context en detalle echa un vistazo a mi artículo.

Gracias por leer!

Puedes encontrar el código fuente completo para esta aplicación en este repositorio.

¿Quieres aprender todas las características de ES6+ en detalle desde cero incluyendo let y const, promesas, varios métodos de promesas, desestructuración de arreglos y objetos, funciones flechas, async/await, import y export y un montón más?

Echa un vistazo a mi libro Mastering Modern JavaScript. Este libro cubre todos los prerrequisitos para aprender React y te ayuda a mejorar en JavaScript y React.

Echa un vistazo a una vista previa del contenido del libro aquí.

También puedes ver mi curso gratuito Introducción a React Router para aprender desde cero React Router.

¿Quieres mantenerte actualizado con contenido relacionado con JavaScript, React, Node.js? Sígueme en LinkedIn.

banner