React CRUD 应用教程 – 从零开始用 React 构建图书管理应用

2025-06-07

React CRUD 应用教程 – 从零开始用 React 构建图书管理应用

在本文中,您将从头开始在 React 中构建一个图书管理应用程序,并学习如何执行 CRUD(创建、读取、更新和删除)操作。

通过创建此应用程序,您将学习

  1. 如何执行 CRUD 操作
  2. 如何使用 React Router 在路由之间导航
  3. 如何使用 React Context API 跨路由传递数据
  4. 如何在 React 中创建自定义 Hook
  5. 如何将数据存储在本地存储中,以便在页面刷新后也能保留
  6. 如何使用自定义钩子管理本地存储中的数据

等等。

我们将使用 React Hooks 来构建此应用程序。如果您是 React Hooks 新手,请查看我的React Hooks 简介文章,了解 Hooks 的基础知识。

想从零开始学习 Redux,并从零开始构建一个订餐应用吗?快来学习“精通 Redux”课程吧!

初始设置

使用以下方法创建新项目create-react-app

npx create-react-app book-management-app
Enter fullscreen mode Exit fullscreen mode

项目创建完成后,删除文件夹中的所有文件src并在文件夹中创建index.jsstyles.scss文件src。此外,在文件夹中创建componentscontext文件夹hooksroutersrc

安装必要的依赖项:

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
Enter fullscreen mode Exit fullscreen mode

打开并从这里styles.scss添加内容

如何创建初始页面

Header.js在文件夹内创建一个新文件components,内容如下:

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

const Header = () => {
  return (
    <header>
      <h1>Book Management App</h1>
      <hr />
      <div className="links">
        <NavLink to="/" className="link" activeClassName="active" exact>
          Books List
        </NavLink>
        <NavLink to="/add" className="link" activeClassName="active">
          Add Book
        </NavLink>
      </div>
    </header>
  );
};

export default Header;
Enter fullscreen mode Exit fullscreen mode

在这里,我们使用NavLink组件添加了两个导航链接react-router-dom:一个用于查看所有书籍的列表,另一个用于添加新书。

我们使用NavLink组件而不是锚标签<a />,因此单击任何链接时页面都不会刷新。

BooksList.js在文件夹内创建一个新文件components,内容如下:

import React from 'react';

const BooksList = () => {
  return <h2>List of books</h2>;
};

export default BooksList;
Enter fullscreen mode Exit fullscreen mode

AddBook.js在文件夹内创建一个新文件components,内容如下:

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;
Enter fullscreen mode Exit fullscreen mode

在这个文件中,我们显示一个BookForm组件(我们尚未创建)。

对于BookForm组件,我们传递handleOnSubmit方法,以便我们可以在提交表单后进行一些处理。

现在,在文件夹BookForm.js内创建一个components包含以下内容的新文件:

import React, { useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { v4 as uuidv4 } from 'uuid';

const BookForm = (props) => {
  const [book, setBook] = useState({
    bookname: props.book ? props.book.bookname : '',
    author: props.book ? props.book.author : '',
    quantity: props.book ? props.book.quantity : '',
    price: props.book ? props.book.price : '',
    date: props.book ? props.book.date : ''
  });

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

  const handleOnSubmit = (event) => {
    event.preventDefault();
    const values = [bookname, author, price, quantity];
    let errorMsg = '';

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

    if (allFieldsFilled) {
      const book = {
        id: uuidv4(),
        bookname,
        author,
        price,
        quantity,
        date: new Date()
      };
      props.handleOnSubmit(book);
    } else {
      errorMsg = 'Please fill out all the fields.';
    }
    setErrorMsg(errorMsg);
  };

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    switch (name) {
      case 'quantity':
        if (value === '' || parseInt(value) === +value) {
          setBook((prevState) => ({
            ...prevState,
            [name]: value
          }));
        }
        break;
      case 'price':
        if (value === '' || value.match(/^\d{1,}(\.\d{0,2})?$/)) {
          setBook((prevState) => ({
            ...prevState,
            [name]: value
          }));
        }
        break;
      default:
        setBook((prevState) => ({
          ...prevState,
          [name]: value
        }));
    }
  };

  return (
    <div className="main-form">
      {errorMsg && <p className="errorMsg">{errorMsg}</p>}
      <Form onSubmit={handleOnSubmit}>
        <Form.Group controlId="name">
          <Form.Label>Book Name</Form.Label>
          <Form.Control
            className="input-control"
            type="text"
            name="bookname"
            value={bookname}
            placeholder="Enter name of book"
            onChange={handleInputChange}
          />
        </Form.Group>
        <Form.Group controlId="author">
          <Form.Label>Book Author</Form.Label>
          <Form.Control
            className="input-control"
            type="text"
            name="author"
            value={author}
            placeholder="Enter name of author"
            onChange={handleInputChange}
          />
        </Form.Group>
        <Form.Group controlId="quantity">
          <Form.Label>Quantity</Form.Label>
          <Form.Control
            className="input-control"
            type="number"
            name="quantity"
            value={quantity}
            placeholder="Enter available quantity"
            onChange={handleInputChange}
          />
        </Form.Group>
        <Form.Group controlId="price">
          <Form.Label>Book Price</Form.Label>
          <Form.Control
            className="input-control"
            type="text"
            name="price"
            value={price}
            placeholder="Enter price of book"
            onChange={handleInputChange}
          />
        </Form.Group>
        <Button variant="primary" type="submit" className="submit-btn">
          Submit
        </Button>
      </Form>
    </div>
  );
};

export default BookForm;
Enter fullscreen mode Exit fullscreen mode

让我们了解一下我们在这里做什么。

最初,我们将状态定义为一个对象,使用useState钩子来存储所有输入的详细信息,如下所示:

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 : ''
  });
Enter fullscreen mode Exit fullscreen mode

由于我们将使用相同的BookForm组件来添加和编辑书籍,因此我们首先book使用三元运算符检查 prop 是否已传递。

如果传递了 prop,我们将其设置为传递的值,否则为空字符串('')。

如果现在看起来很复杂,别担心。一旦我们构建了一些初始功能,你就会更好地理解它。

然后我们添加了一个用于显示错误消息的状态,并使用 ES6 解构语法来引用状态中的每个属性,如下所示:

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

BookForm组件中,我们返回一个表单,用于输入书名、作者、数量和价格。我们使用React-bootstrap框架以美观的格式显示表单。

每个输入字段都添加了一个onChange调用该handleInputChange方法的处理程序。

在这个handleInputChange方法中,我们添加了一个 switch 语句,根据输入字段的变化来改变状态的值。

当我们在quantity输入字段中输入任何内容时,第一个 switch caseevent.target.name将会quantity匹配,并且在该 switch case 中,我们会检查输入的值是否是没有小数点的整数。

如果是,那么我们只会更新状态,如下所示:

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

因此用户无法在数量输入字段中输入任何十进制值。

对于priceswitch case,我们检查小数点后是否只有两位数。因此,我们添加了正则表达式检查value.match(/^\d{1,}(\.\d{0,2})?$/)

如果价格值与正则表达式匹配,那么我们才会更新状态。

注意:对于quantitypriceswitch 情况,我们也像这样检查空值value === ''。这是为了让用户在需要时完全删除输入的值。

如果没有该检查,用户将无法通过按 删除输入的值Ctrl + A + Delete

对于所有其他输入字段,将执行默认的 switch case,它将根据输入的值更新状态。

接下来,一旦我们提交表单,该handleOnSubmit方法就会被调用。

在这个方法中,我们首先检查用户是否使用数组every方法输入了所有详细信息:

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

数组every方法是 JavaScript 中最有用的数组方法之一。

查看我的这篇文章,了解最有用的 JavaScript 数组方法及其浏览器支持。

如果所有值都已填写,那么我们将创建一个包含所有填写值的对象,并handleOnSubmit通过传递书籍作为参数来调用该方法,否则我们将设置一条错误消息。

handleOnSubmit方法作为组件的 prop 传递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.';
}
Enter fullscreen mode Exit fullscreen mode

请注意,要创建唯一的 ID,我们需要调用uuid npm 包uuidv4()中的方法。

现在,在文件夹AppRouter.js内创建一个router包含以下内容的新文件:

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;
Enter fullscreen mode Exit fullscreen mode

在这里,我们已经为各种组件设置了路由,例如BooksListAddBook使用react-router-dom库。

如果您是 React Router 新手,请查看我的免费React Router 介绍课程。

现在,打开src/index.js文件并在其中添加以下内容:

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'));
Enter fullscreen mode Exit fullscreen mode

现在,通过从终端运行以下命令来启动 React App:

yarn start
Enter fullscreen mode Exit fullscreen mode

当您访问http://localhost:3000/ 的应用程序时,您将看到以下屏幕。

初始屏幕.gif

添加书籍.gif

如您所见,我们能够正确添加书籍并将其显示在控制台上。

但是我们不需要登录控制台,而是将其添加到本地存储。

如何为本地存储创建自定义钩子

本地存储非常棒。它允许我们轻松地在浏览器中存储应用程序数据,并且是存储数据的 Cookie 的替代方案。

使用本地存储的优点是数据将永久保存在浏览器缓存中,直到我们手动删除它,这样即使刷新页面后我们也可以访问它,您可能知道,一旦我们刷新页面,存储在 React 状态中的数据就会丢失。

本地存储有很多用例,其中之一就是存储购物车商品,这样即使我们刷新页面也不会被删除。

要将数据添加到本地存储,我们setItem通过提供键和值来使用方法:

localStorage.setItem(key, value)
Enter fullscreen mode Exit fullscreen mode

键和值都必须是字符串。但我们也可以通过使用JSON.stringify方法存储 JSON 对象。

要详细了解本地存储及其各种应用,请查看我的这篇文章

useLocalStorage.js在文件夹内创建一个新文件hooks,内容如下:

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;
Enter fullscreen mode Exit fullscreen mode

在这里,我们使用了一个useLocalStorage接受key和的钩子initialValue

为了使用钩子声明状态useState,我们使用延迟初始化

因此,即使在应用程序每次重新渲染时多次调用钩子,传递给函数的内部代码useState也只会执行一次。useLocalStorage

因此,首先我们检查本地存储中是否存在提供的值,然后通过使用方法key解析它来返回该值JSON.parse

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

key然后稍后,如果或有任何变化value,我们将更新本地存储:

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

return [value, setValue];
Enter fullscreen mode Exit fullscreen mode

然后我们返回value存储在本地存储中的setValue内容,并调用它来更新本地存储数据。

如何使用本地存储钩子

现在,让我们使用这个useLocalStorage钩子,以便我们可以从本地存储中添加或删除数据。

打开AppRouter.js文件并使用useLocalStorage组件内的钩子:

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

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

 return (
  ...
 )
}
Enter fullscreen mode Exit fullscreen mode

现在,我们需要将bookssetBooks作为道具传递给AddBook组件,以便我们可以将书添加到本地存储。

因此,从此代码更改路线:

<Route component={AddBook} path="/add" />
Enter fullscreen mode Exit fullscreen mode

到下面的代码:

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

在这里,我们使用渲染道具模式来传递 React 路由器传递的默认道具以及bookssetBooks

查看我的免费React Router 介绍render课程,以更好地理解这种渲染道具模式以及使用关键字而不是的重要性component

您的整个AppRouter.js文件现在看起来如下:

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;
Enter fullscreen mode Exit fullscreen mode

现在打开AddBook.js并将其内容替换为以下代码:

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;
Enter fullscreen mode Exit fullscreen mode

首先,我们使用 ES6 解构语法来访问组件中的historybooks和props。setBooks

historyprop 会被 React Router 自动传递给文件中提到的每个组件<Route />,并且我们会传递文件中的booksprops setBooksAppRouter.js

我们将所有添加的书籍存储在一个数组中,因此,在handleOnSubmit方法内部,我们setBooks通过传递一个数组来调用该函数,首先添加一本新添加的书籍,然后将所有已添加的书籍分散到books数组中,如下所示:

setBooks([book, ...books]);
Enter fullscreen mode Exit fullscreen mode

在这里,我book首先添加新添加的书籍,然后展开已添加的书籍books,因为我希望在稍后显示书籍列表时首先显示最新的书籍。

但如果您愿意的话,可以像这样更改顺序:

setBooks([...books, book]);
Enter fullscreen mode Exit fullscreen mode

这会将新添加的书籍添加到所有已添加的书籍的末尾。

我们可以使用扩展运算符,因为我们知道这是一个数组,因为我们已经将其初始化为文件中的books空数组,如下所示:[]AppRouter.js

 const [books, setBooks] = useLocalStorage('books', []);
Enter fullscreen mode Exit fullscreen mode

然后,一旦通过调用该方法将书籍添加到本地存储setBooks,在该方法内部,我们将使用该方法handleOnSubmit将用户重定向到页面Books Listhistory.push

history.push('/');
Enter fullscreen mode Exit fullscreen mode

现在,让我们检查是否能够将书籍保存到本地存储。

添加的本地存储.gif

如您所见,该书已正确添加到本地存储中,可以从 chrome dev tools 的应用程序选项卡中确认。

如何在UI上显示已添加的书籍

现在,让我们在菜单下的UI上显示添加的书籍Books List

打开AppRouter.js文件并将bookssetBooks作为道具传递给BooksList组件。

您的AppRouter.js文件现在看起来如下:

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;
Enter fullscreen mode Exit fullscreen mode

这里我们只是改变了与BooksList组件相关的第一个路由。

现在,在文件夹Book.js内创建一个components包含以下内容的新文件:

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;
Enter fullscreen mode Exit fullscreen mode

现在,打开该BooksList.js文件并将其内容替换为以下代码:

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;
Enter fullscreen mode Exit fullscreen mode

在这个文件中,我们循环books使用数组map方法并将它们作为 prop 传递给Book组件。

请注意,我们还将该handleRemoveBook函数作为道具传递,以便我们能够删除任何书籍。

在函数内部handleRemoveBook,我们setBooks使用数组filter方法调用函数来仅保留与提供的书籍不匹配的书籍id

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

现在,如果您通过访问http://localhost:3000/检查应用程序,您将能够在 UI 上看到添加的书籍。

列表页面.png

让我们添加另一本书来验证整个流程。

添加_删除.gif

如您所见,当我们添加一本新书时,我们会被重定向到列表页面,在该页面我们可以删除该书,并且该书会立即从 UI 以及本地存储中删除。

而且当我们刷新页面时,数据也不会丢失。这就是本地存储的强大之处。

如何编辑书籍

现在,我们已经有了书籍的添加和删除功能,让我们添加一种编辑书籍的方法。

打开Book.js并更改以下代码:

<Button variant="primary">Edit</Button>{' '}
Enter fullscreen mode Exit fullscreen mode

到此代码:

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

在这里,我们添加了一个处理程序,当我们单击编辑按钮时,onClick将用户重定向到路线。/edit/id_of_the_book

但是我们无法访问组件history中的对象,Book因为historyprop 仅传递给中提到的组件<Route />

我们在组件Book内部渲染组件BooksList,以便我们只能访问组件history内部BooksList,然后我们可以将其作为 prop 传递给Book组件。

但与此相反,React 路由器提供了一种使用useHistory钩子的简单方法。

useHistory在文件顶部添加钩子的导入Book.js

import { useHistory } from 'react-router-dom';
Enter fullscreen mode Exit fullscreen mode

并在Book组件内部调用useHistory钩子。

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

现在我们可以访问组件history内部的对象了Book

您的整个Book.js文件现在看起来像这样:

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;
Enter fullscreen mode Exit fullscreen mode

EditBook.js在文件夹内创建一个新文件components,内容如下:

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;
Enter fullscreen mode Exit fullscreen mode

这里,对于onClick“编辑”按钮的处理程序,我们将用户重定向到/edit/some_id路由,但该路由尚不存在。因此,我们先创建它。

打开AppRouter.js文件并在结束标记之前Switch添加另外两个路由:

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

第一个 Route 是针对EditBook组件的。这里的路径定义为/edit/:idwhere :id,表示任意随机 id。

第二条路线是处理与所提到的任何路线不匹配的所有其他路线。

因此,如果我们访问任何随机路线,例如/help或,/contact那么我们将把用户重定向到/作为BooksList组件的路线。

您的整个AppRouter.js文件现在看起来像这样:

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;
Enter fullscreen mode Exit fullscreen mode

现在,让我们检查应用程序的编辑功能。

编辑书.gif

如您所见,我们已成功编辑本书。让我们了解一下它的工作原理。

首先,在AppRouter.js文件内部我们有这样的路由:

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

在文件内部Book.js,我们有这样的编辑按钮:

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

因此,每当我们单击任何书籍的“编辑”按钮时,我们都会通过传递要编辑的书籍的 ID 的方法将用户重定向到EditBook组件。history.push

然后在EditBook组件内部,我们使用useParams提供的钩子react-router-dom来访问props.params.id

所以下面两行是相同的。

const { id } = useParams();

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

const { id } = props.match.params;
Enter fullscreen mode Exit fullscreen mode

一旦我们得到它id,我们就使用数组find方法从提供的匹配书籍列表中找出特定的书籍id

const bookToEdit = books.find((book) => book.id === id);
Enter fullscreen mode Exit fullscreen mode

我们将这本特定的书BookForm作为book道具传递给组件:

<BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />
Enter fullscreen mode Exit fullscreen mode

在组件内部BookForm,我们定义了状态,如下所示:

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 : ''
});
Enter fullscreen mode Exit fullscreen mode

这里,我们检查bookprop 是否存在。如果存在,我们就使用作为 prop 传递的书籍详情,否则,我们将每个属性初始化为空值 ('')。

每个输入元素都提供了一个valueprop,我们可以像这样从状态中设置它:

 <Form.Control
  ...
  value={bookname}
  ...
/>
Enter fullscreen mode Exit fullscreen mode

但我们可以稍微改进一下组件useState内部的语法BookForm

我们可以使用文件useState中所做的延迟初始化,而不是直接为钩子设置对象。useLocalStorage.js

因此,请更改以下代码:

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 : ''
});
Enter fullscreen mode Exit fullscreen mode

到此代码:

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 : ''
  };
});
Enter fullscreen mode Exit fullscreen mode

由于这一变化,设置状态的代码将不会在应用程序每次重新渲染时执行。但它只会在组件挂载时执行一次。

请注意,组件的重新渲染发生在每次状态或属性改变时。

如果您检查应用程序,您会发现它运行起来和以前一样,没有任何问题。但我们只是稍微提升了应用程序的性能。

如何使用 React 的 Context API

现在,我们已经完成了整个应用程序的功能。但是如果你检查一下AppRouter.js文件,你会发现每个路由看起来都有点复杂,因为我们使用渲染道具模式将相同的books道具setBooks传递给每个组件。

因此我们可以使用 React Context API 来简化这段代码。

请注意,这是可选步骤。您无需使用 Context API,因为我们只传递了一层 props,并且当前代码运行良好,并且我们没有使用任何错误的 props 传递方法。

但为了使路由器代码更简单,并让您了解如何利用 Context API 的强大功能,我们将在我们的应用程序中使用它。

BooksContext.js在文件夹内创建一个新文件context,内容如下:

import React from 'react';

const BooksContext = React.createContext();

export default BooksContext;
Enter fullscreen mode Exit fullscreen mode

现在,在AppRouter.js文件内部,导入上面导出的上下文。

import BooksContext from '../context/BooksContext';
Enter fullscreen mode Exit fullscreen mode

AppRouter并用以下代码替换该组件:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

在这里,我们将渲染道具模式转换回正常路线,并将整个Switch块添加到组件内,BooksContext.Provider如下所示:

<BooksContext.Provider value={{ books, setBooks }}>
 <Switch>
 ...
 </Switch>
</BooksContext.Provider>
Enter fullscreen mode Exit fullscreen mode

在这里,我们通过传递我们想要在路由中提到的组件内访问的数据来为BooksContext.Provider组件提供一个prop。value

所以现在,每个声明为 Route 一部分的组件都将能够通过上下文 APIbooks访问。setBooks

现在,打开BooksList.js文件并删除已被解构的bookssetBooks道具,因为我们不再直接传递道具。

在文件顶部BooksContext添加导入:useContext

import React, { useContext } from 'react';
import BooksContext from '../context/BooksContext';
Enter fullscreen mode Exit fullscreen mode

并在handleRemoveBook函数上方添加以下代码:

const { books, setBooks } = useContext(BooksContext);
Enter fullscreen mode Exit fullscreen mode

在这里,我们使用钩子从中取出books和道具setBooksBooksContextuseContext

您的整个BooksList.js文件将如下所示:

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;
Enter fullscreen mode Exit fullscreen mode

现在,在文件中进行类似的更改AddBook.js

您的整个AddBook.js文件将如下所示:

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;
Enter fullscreen mode Exit fullscreen mode

请注意,这里我们仍然对 prop 使用了解构history。我们只是从解构语法中删除了booksand setBooks

现在,在文件中进行类似的更改EditBook.js

您的整个EditBook.js文件将如下所示:

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;
Enter fullscreen mode Exit fullscreen mode

如果您检查该应用程序,您会发现它的工作方式与以前完全相同,但我们现在使用的是 React Context API。

编辑删除.gif

如果您想详细了解 Context API,请查看我的这篇文章

感谢阅读!

您可以在此存储库中找到此应用程序的完整源代码

想要详细了解所有 ES6+ 功能,包括 let 和 const、承诺、各种承诺方法、数组和对象解构、箭头函数、async/await、导入和导出以及更多从头开始的内容吗?

看看我的《精通现代 JavaScript》这本书。这本书涵盖了学习 React 的所有先决条件,并能帮助你更好地掌握 JavaScript 和 React。

在此处查看本书的免费预览内容

此外,您还可以查看我的免费 “React Router 简介”课程,从头开始学习“React Router”。

想要定期了解有关 JavaScript、React 和 Node.js 的最新内容吗?请在 LinkedIn 上关注我

文章来源:https://dev.to/myogeshchavan97/react-crud-app-tutorial-build-a-book-management-app-in-react-from-scratch-f7b
PREV
给 JavaScript 开发者的超级实用技巧
NEXT
你必须知道的 Chrome 开发者工具最有用的功能