React CRUD 应用教程 – 从零开始用 React 构建图书管理应用
在本文中,您将从头开始在 React 中构建一个图书管理应用程序,并学习如何执行 CRUD(创建、读取、更新和删除)操作。
通过创建此应用程序,您将学习
- 如何执行 CRUD 操作
- 如何使用 React Router 在路由之间导航
- 如何使用 React Context API 跨路由传递数据
- 如何在 React 中创建自定义 Hook
- 如何将数据存储在本地存储中,以便在页面刷新后也能保留
- 如何使用自定义钩子管理本地存储中的数据
等等。
我们将使用 React Hooks 来构建此应用程序。如果您是 React Hooks 新手,请查看我的React Hooks 简介文章,了解 Hooks 的基础知识。
想从零开始学习 Redux,并从零开始构建一个订餐应用吗?快来学习“精通 Redux”课程吧!
初始设置
使用以下方法创建新项目create-react-app
:
npx create-react-app book-management-app
项目创建完成后,删除文件夹中的所有文件,src
并在文件夹中创建index.js
和styles.scss
文件src
。此外,在文件夹中创建components
、context
和文件夹。hooks
router
src
安装必要的依赖项:
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
打开并从这里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;
在这里,我们使用NavLink
组件添加了两个导航链接react-router-dom
:一个用于查看所有书籍的列表,另一个用于添加新书。
我们使用NavLink
组件而不是锚标签<a />
,因此单击任何链接时页面都不会刷新。
BooksList.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
const BooksList = () => {
return <h2>List of books</h2>;
};
export default BooksList;
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;
在这个文件中,我们显示一个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;
让我们了解一下我们在这里做什么。
最初,我们将状态定义为一个对象,使用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 : ''
});
由于我们将使用相同的BookForm
组件来添加和编辑书籍,因此我们首先book
使用三元运算符检查 prop 是否已传递。
如果传递了 prop,我们将其设置为传递的值,否则为空字符串('')。
如果现在看起来很复杂,别担心。一旦我们构建了一些初始功能,你就会更好地理解它。
然后我们添加了一个用于显示错误消息的状态,并使用 ES6 解构语法来引用状态中的每个属性,如下所示:
const [errorMsg, setErrorMsg] = useState('');
const { bookname, author, price, quantity } = book;
从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
}));
}
因此用户无法在数量输入字段中输入任何十进制值。
对于price
switch case,我们检查小数点后是否只有两位数。因此,我们添加了正则表达式检查value.match(/^\d{1,}(\.\d{0,2})?$/)
。
如果价格值与正则表达式匹配,那么我们才会更新状态。
注意:对于quantity
和price
switch 情况,我们也像这样检查空值value === ''
。这是为了让用户在需要时完全删除输入的值。
如果没有该检查,用户将无法通过按 删除输入的值Ctrl + A + Delete
。
对于所有其他输入字段,将执行默认的 switch case,它将根据输入的值更新状态。
接下来,一旦我们提交表单,该handleOnSubmit
方法就会被调用。
在这个方法中,我们首先检查用户是否使用数组every
方法输入了所有详细信息:
const allFieldsFilled = values.every((field) => {
const value = `${field}`.trim();
return value !== '' && value !== '0';
});
数组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.';
}
请注意,要创建唯一的 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;
在这里,我们已经为各种组件设置了路由,例如BooksList
并AddBook
使用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'));
现在,通过从终端运行以下命令来启动 React App:
yarn start
当您访问http://localhost:3000/ 的应用程序时,您将看到以下屏幕。
如您所见,我们能够正确添加书籍并将其显示在控制台上。
但是我们不需要登录控制台,而是将其添加到本地存储。
如何为本地存储创建自定义钩子
本地存储非常棒。它允许我们轻松地在浏览器中存储应用程序数据,并且是存储数据的 Cookie 的替代方案。
使用本地存储的优点是数据将永久保存在浏览器缓存中,直到我们手动删除它,这样即使刷新页面后我们也可以访问它,您可能知道,一旦我们刷新页面,存储在 React 状态中的数据就会丢失。
本地存储有很多用例,其中之一就是存储购物车商品,这样即使我们刷新页面也不会被删除。
要将数据添加到本地存储,我们setItem
通过提供键和值来使用方法:
localStorage.setItem(key, value)
键和值都必须是字符串。但我们也可以通过使用
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;
在这里,我们使用了一个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;
}
key
然后稍后,如果或有任何变化value
,我们将更新本地存储:
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
然后我们返回value
存储在本地存储中的setValue
内容,并调用它来更新本地存储数据。
如何使用本地存储钩子
现在,让我们使用这个useLocalStorage
钩子,以便我们可以从本地存储中添加或删除数据。
打开AppRouter.js
文件并使用useLocalStorage
组件内的钩子:
import useLocalStorage from '../hooks/useLocalStorage';
const AppRouter = () => {
const [books, setBooks] = useLocalStorage('books', []);
return (
...
)
}
现在,我们需要将books
和setBooks
作为道具传递给AddBook
组件,以便我们可以将书添加到本地存储。
因此,从此代码更改路线:
<Route component={AddBook} path="/add" />
到下面的代码:
<Route
render={(props) => (
<AddBook {...props} books={books} setBooks={setBooks} />
)}
path="/add"
/>
在这里,我们使用渲染道具模式来传递 React 路由器传递的默认道具以及books
和setBooks
。
查看我的免费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;
现在打开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;
首先,我们使用 ES6 解构语法来访问组件中的history
、books
和props。setBooks
history
prop 会被 React Router 自动传递给文件中提到的每个组件<Route />
,并且我们会传递文件中的books
props 。setBooks
AppRouter.js
我们将所有添加的书籍存储在一个数组中,因此,在handleOnSubmit
方法内部,我们setBooks
通过传递一个数组来调用该函数,首先添加一本新添加的书籍,然后将所有已添加的书籍分散到books
数组中,如下所示:
setBooks([book, ...books]);
在这里,我book
首先添加新添加的书籍,然后展开已添加的书籍books
,因为我希望在稍后显示书籍列表时首先显示最新的书籍。
但如果您愿意的话,可以像这样更改顺序:
setBooks([...books, book]);
这会将新添加的书籍添加到所有已添加的书籍的末尾。
我们可以使用扩展运算符,因为我们知道这是一个数组,因为我们已经将其初始化为文件中的books
空数组,如下所示:[]
AppRouter.js
const [books, setBooks] = useLocalStorage('books', []);
然后,一旦通过调用该方法将书籍添加到本地存储setBooks
,在该方法内部,我们将使用该方法handleOnSubmit
将用户重定向到页面:Books List
history.push
history.push('/');
现在,让我们检查是否能够将书籍保存到本地存储。
如您所见,该书已正确添加到本地存储中,可以从 chrome dev tools 的应用程序选项卡中确认。
如何在UI上显示已添加的书籍
现在,让我们在菜单下的UI上显示添加的书籍Books List
。
打开AppRouter.js
文件并将books
和setBooks
作为道具传递给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;
这里我们只是改变了与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;
现在,打开该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;
在这个文件中,我们循环books
使用数组map
方法并将它们作为 prop 传递给Book
组件。
请注意,我们还将该handleRemoveBook
函数作为道具传递,以便我们能够删除任何书籍。
在函数内部handleRemoveBook
,我们setBooks
使用数组filter
方法调用函数来仅保留与提供的书籍不匹配的书籍id
。
const handleRemoveBook = (id) => {
setBooks(books.filter((book) => book.id !== id));
};
现在,如果您通过访问http://localhost:3000/检查应用程序,您将能够在 UI 上看到添加的书籍。
让我们添加另一本书来验证整个流程。
如您所见,当我们添加一本新书时,我们会被重定向到列表页面,在该页面我们可以删除该书,并且该书会立即从 UI 以及本地存储中删除。
而且当我们刷新页面时,数据也不会丢失。这就是本地存储的强大之处。
如何编辑书籍
现在,我们已经有了书籍的添加和删除功能,让我们添加一种编辑书籍的方法。
打开Book.js
并更改以下代码:
<Button variant="primary">Edit</Button>{' '}
到此代码:
<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
Edit
</Button>{' '}
在这里,我们添加了一个处理程序,当我们单击编辑按钮时,onClick
将用户重定向到路线。/edit/id_of_the_book
但是我们无法访问组件history
中的对象,Book
因为history
prop 仅传递给中提到的组件<Route />
。
我们在组件Book
内部渲染组件BooksList
,以便我们只能访问组件history
内部BooksList
,然后我们可以将其作为 prop 传递给Book
组件。
但与此相反,React 路由器提供了一种使用useHistory
钩子的简单方法。
useHistory
在文件顶部添加钩子的导入Book.js
:
import { useHistory } from 'react-router-dom';
并在Book
组件内部调用useHistory
钩子。
const Book = ({
id,
bookname,
author,
price,
quantity,
date,
handleRemoveBook
}) => {
const history = useHistory();
...
}
现在我们可以访问组件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;
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;
这里,对于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>
第一个 Route 是针对EditBook
组件的。这里的路径定义为/edit/:id
where :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;
现在,让我们检查应用程序的编辑功能。
如您所见,我们已成功编辑本书。让我们了解一下它的工作原理。
首先,在AppRouter.js
文件内部我们有这样的路由:
<Route
render={(props) => (
<EditBook {...props} books={books} setBooks={setBooks} />
)}
path="/edit/:id"
/>
在文件内部Book.js
,我们有这样的编辑按钮:
<Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
Edit
</Button>
因此,每当我们单击任何书籍的“编辑”按钮时,我们都会通过传递要编辑的书籍的 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;
一旦我们得到它id
,我们就使用数组find
方法从提供的匹配书籍列表中找出特定的书籍id
。
const bookToEdit = books.find((book) => book.id === id);
我们将这本特定的书BookForm
作为book
道具传递给组件:
<BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />
在组件内部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 : ''
});
这里,我们检查book
prop 是否存在。如果存在,我们就使用作为 prop 传递的书籍详情,否则,我们将每个属性初始化为空值 ('')。
每个输入元素都提供了一个value
prop,我们可以像这样从状态中设置它:
<Form.Control
...
value={bookname}
...
/>
但我们可以稍微改进一下组件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 : ''
});
到此代码:
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 : ''
};
});
由于这一变化,设置状态的代码将不会在应用程序每次重新渲染时执行。但它只会在组件挂载时执行一次。
请注意,组件的重新渲染发生在每次状态或属性改变时。
如果您检查应用程序,您会发现它运行起来和以前一样,没有任何问题。但我们只是稍微提升了应用程序的性能。
如何使用 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;
现在,在AppRouter.js
文件内部,导入上面导出的上下文。
import BooksContext from '../context/BooksContext';
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>
);
};
在这里,我们将渲染道具模式转换回正常路线,并将整个Switch
块添加到组件内,BooksContext.Provider
如下所示:
<BooksContext.Provider value={{ books, setBooks }}>
<Switch>
...
</Switch>
</BooksContext.Provider>
在这里,我们通过传递我们想要在路由中提到的组件内访问的数据来为BooksContext.Provider
组件提供一个prop。value
所以现在,每个声明为 Route 一部分的组件都将能够通过上下文 APIbooks
访问。setBooks
现在,打开BooksList.js
文件并删除已被解构的books
和setBooks
道具,因为我们不再直接传递道具。
在文件顶部BooksContext
添加导入:useContext
import React, { useContext } from 'react';
import BooksContext from '../context/BooksContext';
并在handleRemoveBook
函数上方添加以下代码:
const { books, setBooks } = useContext(BooksContext);
在这里,我们使用钩子从中取出books
和道具。setBooks
BooksContext
useContext
您的整个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;
现在,在文件中进行类似的更改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;
请注意,这里我们仍然对 prop 使用了解构history
。我们只是从解构语法中删除了books
and 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;
如果您检查该应用程序,您会发现它的工作方式与以前完全相同,但我们现在使用的是 React Context API。
如果您想详细了解 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