豆约翰习惯将掌握某一技术分为5个层次:初窥门径,小试牛刀,渐入佳境,得心应手,玩转自如
本篇属于React框架中的第1层次即初窥门径
本文翻译自:
https://www.taniarascia.com/crud-app-in-react-with-hooks/
在React- Hooks中引入了一个新概念。钩子是类的替代方法。如果您以前使用过React,那么您将熟悉简单的(功能性)组件和类组件。
简单组件
const Example = () => { return <div>I'm a simple component</div> }
类组件
class Example extends Component { render() { return <div>I'm a class component</div> } }
直到现在,类的许多可用功能(例如生命周期方法和状态)才对简单组件可用。新的Hooks提案添加了所有这些功能以及更多功能。
我想尝试一下Hooks,看看没有任何类的应用看起来如何,但是我还没有看到任何示例,所以我决定自己做一个。我创建了一个简单的CRUD(创建,读取,更新,删除)应用程序,该应用程序使用了Hooks,没有使用类,并且为其他想学习如何使用它们的人创建了本教程。
如果您不知道如何在React中制作一个简单的CRUD应用程序,无论您使用类还是钩子,这篇文章也将对您有所帮助。
先决条件
为了遵循本教程,您需要具备HTML,CSS和JavaScript / ES6的基础知识。您还应该了解React的基础知识,可通过阅读React入门来学习。
目标
在本教程中,我们将制作一个简单的CRUD应用程序。它将有用户,您将能够添加,更新或删除用户。我们将不使用任何React类,而是在功能组件上使用状态挂钩和效果挂钩。如果您一路迷路,请务必检查完成项目的来源。
创建React应用
我们将从使用create-react-app(CRA)安装项目开始。
npx create-react-app react-hooks
然后运行npm i
。
现在,您已经准备好使用React。
最初设定
首先,从不需要的样板中清除所有文件。删除一切从/src
文件夹除外App.js
,index.js
和index.css
。
对于index.css
,我只是从Primitive复制并粘贴CSS,Primitive是我制作的一个简单CSS样板,因为此应用程序的重点是在React上工作,而不在乎设计。这个CSS样板只是添加了一些合理的默认值和一个简单的网格,因此我们可以开始制作原型。
在中index.js
,我们将通过删除对Service Workers的引用来简化它。
index.js
import React from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './App' ReactDOM.render(<App />, document.getElementById('root'))
在中App.js
,我将为而制作一个简单的功能组件,App
而不是一个类。
App.js
import React from 'react' const App = () => { return ( <div className="container"> <h1>CRUD App with Hooks</h1> <div className="flex-row"> <div className="flex-large"> <h2>Add user</h2> </div> <div className="flex-large"> <h2>View users</h2> </div> </div> </div> ) } export default App
现在,我们有了该应用程序的初始设置和框架。
State vs. Hook State
如果我们看一个非常简单的带有状态的类组件和一个带有Hook状态的功能组件的示例,我们可以看到相同点和不同点。使用类状态,您将获得一个主状态对象,然后使用类和上的方法进行更新setState()
。
我将快速制作一些示例代码,就好像它是一个图书馆,并且您有带有状态的书一样。
类组件状态示例
class App extends Component { initialState = { title: '', available: false, } state = initialState updateBook = book => { this.setState({ title: book.title, available: book.available }) } }
有了Hook状态,每种状态类型都有一个getter和setter方法(可以随意设置),并且我们显然创建函数而不是方法。
挂钩状态示例
const App = () => { const initialBookState = { title: '', available: false, } const [book, setBook] = useState(initialBookState) const updateBook = book => { setBook({ title: book.title, available: book.available }) } }
我不会深入了解钩子与类组件之间的基本原理,因为您可以在React的Hooks简介中了解所有内容。我将向您展示如何与他们一起创建功能实用的应用程序。
设置视图
我们要做的第一件事是为视图创建一些示例数据和一个表格以显示它。创建一个名为tables
in 的新目录src
,以及一个名为的文件UserTable.js
。我们将制作表格的骨架。
表/UserTable.js
import React from 'react' const UserTable = () => ( <table> <thead> <tr> <th>Name</th> <th>Username</th> <th>Actions</th> </tr> </thead> <tbody> <tr> <td>Name data</td> <td>Username data</td> <td> <button className="button muted-button">Edit</button> <button className="button muted-button">Delete</button> </td> </tr> </tbody> </table> ) export default UserTable
现在,只需导入文件并添加新组件即可。
App.js
import React from 'react' import UserTable from './tables/UserTable' const App = () => { return ( <div className="container"> <h1>CRUD App with Hooks</h1> <div className="flex-row"> <div className="flex-large"> <h2>Add user</h2> </div> <div className="flex-large"> <h2>View users</h2> <UserTable /> </div> </div> </div> ) } export default App
让我们引入一些随机的虚拟数据和useState
从React导入。
App.js
import React, { useState } from 'react' import UserTable from './tables/UserTable' const App = () => { const usersData = [ { id: 1, name: 'Tania', username: 'floppydiskette' }, { id: 2, name: 'Craig', username: 'siliconeidolon' }, { id: 3, name: 'Ben', username: 'benisphere' }, ] const [users, setUsers] = useState(usersData) return ( <div className="container"> <h1>CRUD App with Hooks</h1> <div className="flex-row"> <div className="flex-large"> <h2>Add user</h2> </div> <div className="flex-large"> <h2>View users</h2> <UserTable users={users} /> </div> </div> </div> ) } export default App
Props 的工作原理与以前一样。我们将通过发送的用户数据进行映射,并显示每个用户的属性,如果没有用户,则显示一条消息。编辑和删除按钮尚未连接到任何东西,因此它们不会做任何事情。
UserTable.js
import React from 'react' const UserTable = props => ( <table> <thead> <tr> <th>Name</th> <th>Username</th> <th>Actions</th> </tr> </thead> <tbody> {props.users.length > 0 ? ( props.users.map(user => ( <tr key={user.id}> <td>{user.name}</td> <td>{user.username}</td> <td> <button className="button muted-button">Edit</button> <button className="button muted-button">Delete</button> </td> </tr> )) ) : ( <tr> <td colSpan={3}>No users</td> </tr> )} </tbody> </table> ) export default UserTable
稍后我们将介绍编辑和删除按钮。现在已经设置了基本视图,让我们开始添加功能。
添加新用户
我们将设置表单以添加新用户。
我们可以做的第一件事是创建实际功能,该功能会将新用户添加到状态中。我们setUsers
自动提供了来自的功能useState
,因此我们将使用它来更新用户状态。
由于我们没有使用可能具有自动递增ID的真实API和数据库,因此,我将手动增加新用户的ID。该函数将一个user
对象作为参数,并将它们添加到users
对象数组中。该...users
代码确保所有先前的用户都保留在数组中。
App.js
const addUser = user => { user.id = users.length + 1 setUsers([...users, user]) }
我们将为此创建一个组件,因此,我将继续在顶部添加对该组件的引用,并将该组件插入“添加用户”标题下。我们可以addUser()
通过作为Props 。当我们将其作为参考时,请确保不要包括括号- <AddUserForm addUser={addUser} />
而不是<AddUserForm addUser={addUser()} />
。
App.js
import React, { useState } from 'react' import UserTable from './tables/UserTable' import AddUserForm from './forms/AddUserForm' const App = () => { const usersData = [ { id: 1, name: 'Tania', username: 'floppydiskette' }, { id: 2, name: 'Craig', username: 'siliconeidolon' }, { id: 3, name: 'Ben', username: 'benisphere' }, ] const [users, setUsers] = useState(usersData) const addUser = user => { user.id = users.length + 1 setUsers([...users, user]) } return ( <div className="container"> <h1>CRUD App with Hooks</h1> <div className="flex-row"> <div className="flex-large"> <h2>Add user</h2> <AddUserForm addUser={addUser} /> </div> <div className="flex-large"> <h2>View users</h2> <UserTable users={users} /> </div> </div> </div> ) } export default App
现在,我们必须创建一个可用于添加新用户的表单。让我们创建一个forms
子目录,其中包含一个名为的文件AddUserForm.js
。
AddUserForm.js
import React, { useState } from 'react' const AddUserForm = props => { return ( <form> <label>Name</label> <input type="text" name="name" value="" /> <label>Username</label> <input type="text" name="username" value="" /> <button>Add new user</button> </form> ) } export default AddUserForm
目前,该表单为空,由于我们的值字符串为空,因此您无法向其中添加任何值,提交按钮也不会执行任何操作。
就像以前一样,我们将要设置一些状态,只是该状态只是临时的,以便跟踪添加用户表单中当前的内容。
我将使用这些空值创建一个初始状态,并将用户状态设置为空值。在变量中具有初始状态很有用,因为在提交表单后,我们可以将其返回为初始的空值。
AddUserForm.js
const initialFormState = { id: null, name: '', username: '' } const [user, setUser] = useState(initialFormState)
现在,我们将创建一个函数来更新表单中的状态。event
总是传递给on
DOM中的任何事件,因此您将看到它作为函数的参数。对象解构将使我们能够轻松地从表单中获取name
(key)value
。最后,我们将像在App
组件上一样对用户进行设置,除了这次我们使用计算的属性名称来动态设置名称(使用[name]
)和值。
const handleInputChange = event => { const { name, value } = event.target setUser({ ...user, [name]: value }) }
如果您不了解正在传递的内容,请尝试
console.log(event)
在输入处理功能中进行尝试。
现在,我们从状态对象中提取值,并在onChange
事件中引用我们的函数。
<form> <label>Name</label> <input type="text" name="name" value={user.name} onChange={handleInputChange} /> <label>Username</label> <input type="text" name="username" value={user.username} onChange={handleInputChange} /> <button>Add new user</button> </form>
最后要注意的是实际上将表单提交回App
组件。当我们使用传递函数时props
,我们将使用道具来访问该函数。我将编写一个onSubmit
函数,我们将防止触发默认表单提交。我添加了一点验证,以确保不能提交空值,并将用户发送到add函数。最后,成功提交后,我将使用设置器将表单重置为其初始值。
<form onSubmit={event => { event.preventDefault() if (!user.name || !user.username) return props.addUser(user) setUser(initialFormState) }} >
幸运的是,这段代码非常简单,因为我们不必担心异步API调用。
这是我们的全部AddUserForm
内容。
AddUserForm.js
import React, { useState } from 'react' const AddUserForm = props => { const initialFormState = { id: null, name: '', username: '' } const [user, setUser] = useState(initialFormState) const handleInputChange = event => { const { name, value } = event.target setUser({ ...user, [name]: value }) } return ( <form onSubmit={event => { event.preventDefault() if (!user.name || !user.username) return props.addUser(user) setUser(initialFormState) }} > <label>Name</label> <input type="text" name="name" value={user.name} onChange={handleInputChange} /> <label>Username</label> <input type="text" name="username" value={user.username} onChange={handleInputChange} /> <button>Add new user</button> </form> ) } export default AddUserForm
删除用户
我们要解决的下一个问题是删除用户,这是最简单的功能。
在addUser
中的下面App.js
,我们将创建deleteUser
,它将获取用户ID并将其从用户数组中过滤出来。
const deleteUser = id => { setUsers(users.filter(user => user.id !== id)) }
我们通过Props将该功能传递给UserTable
。
<UserTable users={users} deleteUser={deleteUser} />
现在我们要做的UserTable.js
就是确保删除按钮调用该函数。
<button onClick={() => props.deleteUser(user.id)} className="button muted-button"> Delete </button>
现在,您可以删除部分或全部用户。
更新用户
难题的最后一步是引入更新现有用户的功能。这类似于添加用户,除了我们必须能够识别正在编辑的用户。在类组件中,我们将使用componentDidUpdate
生命周期方法来实现这一点,但是现在我们将使用Effect Hook。该Effect Hook就像是componentDidMount
和componentDidUpdate
合并。
我们要构造的方式是,当为用户选择“编辑”操作时,“添加用户”表单将变为“编辑用户”表单,并且将使用所选用户的数据预先填充该表单。您可以取消编辑模式,也可以提交更改,这将更新所选用户并退出编辑模式。
让我们开始。在中App.js
,我们要做的第一件事是使状态为是否打开编辑模式。它将开始为false。
App.js
const [editing, setEditing] = useState(false)
由于不知道正在编辑的对象,因此我们将为表单创建初始的空状态,就像添加表单一样。
const initialFormState = { id: null, name: '', username: '' }
我们需要一种查看和更新正在编辑的当前用户的人的方法,因此我们会将空用户应用于currentUser
状态。
const [currentUser, setCurrentUser] = useState(initialFormState)
在用户上选择“编辑”后,它应打开编辑模式并设置当前用户,我们将在此editRow
功能中执行此操作。
const editRow = user => { setEditing(true) setCurrentUser({ id: user.id, name: user.name, username: user.username }) }
现在,只需将该函数传递给UserTable
我们就可以了deleteUser
。
<UserTable users={users} editRow={editRow} deleteUser={deleteUser} />
在中UserTable.js
,我们将user
对象发送过来。
UserTable.js
<button onClick={() => { props.editRow(user) }} className="button muted-button" > Edit </button>
现在我们已完成所有设置-有一个用于编辑模式的开关,以及一个按钮,该按钮将在翻转编辑模式开关的同时使当前用户进入状态。
让我们创建在提交编辑表单时将调用的实际函数。与delete(通过ID筛选出用户)或add(将用户追加到数组)不同,update函数需要映射到数组,并更新与通过的ID相匹配的用户。
这意味着我们将使用两个参数-已更新的用户对象和id-并且将使用三元操作来映射用户并找到我们要更新的参数。
App.js
const updateUser = (id, updatedUser) => { setEditing(false) setUsers(users.map(user => (user.id === id ? updatedUser : user))) }
我们只需要自己制作编辑表单即可。
创建forms/EditUserForm.js
。大部分将与添加表单相同。到目前为止,唯一的区别是我们将直接currentUser
通过props 设置状态。还有一个取消按钮,可以简单地关闭编辑模式。
EditUserForm.js
import React, { useState } from 'react' const EditUserForm = props => { const [user, setUser] = useState(props.currentUser) const handleInputChange = event => { const { name, value } = event.target setUser({ ...user, [name]: value }) } return ( <form onSubmit={event => { event.preventDefault() props.updateUser(user.id, user) }} > <label>Name</label> <input type="text" name="name" value={user.name} onChange={handleInputChange} /> <label>Username</label> <input type="text" name="username" value={user.username} onChange={handleInputChange} /> <button>Update user</button> <button onClick={() => props.setEditing(false)} className="button muted-button"> Cancel </button> </form> ) } export default EditUserForm
现在,我们必须将编辑表单放入App.js
,并创建一个切换以显示添加或编辑表单。
首先,引入组件。
App.js
import EditUserForm from './forms/EditUserForm'
然后创建切换。我们将使用三元运算来检查editing
状态是否为true。如果为true,则显示编辑表单。如果为false,则显示添加表单。确保将我们创建的所有功能传递给编辑组件。
App.js
<div className="flex-large"> {editing ? ( <div> <h2>Edit user</h2> <EditUserForm setEditing={setEditing} currentUser={currentUser} updateUser={updateUser} /> </div> ) : ( <div> <h2>Add user</h2> <AddUserForm addUser={addUser} /> </div> )} </div>
好的,因此此时单击“编辑”按钮应该可以切换编辑模式,并且您应该能够更新用户。但是,我们完成了吗?
使用Effect Hook
如果您稍作尝试,则会注意到一个问题。二,实际上。如果您开始编辑一个用户,然后尝试切换到另一用户,则不会发生任何事情。为什么?好了,该组件已经打开,并且尽管父级上的状态已更改,但尚未注册到props。
这就是Effect Hook的位置。我们想让EditUserForm
组件知道道具已经更改,这是我们之前使用所做的componentDidUpdate
。
第一步是引入useEffect
。
EditUserForm.js
import React, { useState, useEffect } from 'react'
EditUserForm.js
useEffect(() => { setUser(props.currentUser) }, [props])
在效果挂钩中,我们创建了一个回调函数,该函数user
使用正在发送的新道具更新状态。之前,我们需要进行比较if (prevProps.currentUser !== this.state.currentUser)
,但是有了Effect Hook,我们就可以[props]
通过它来告知我们正在观看props。
使用
[props]
数组类似于使用componentDidUpdate
。如果您正在执行类似的一次性事件componentDidMount
,则可以传递一个空数组([]
)。
现在,如果您尝试更改要编辑的用户,它将可以正常工作!
我说这里有两个问题,另一个问题是您可以在当前正在编辑用户的同时删除它。我们可以通过添加setEditing(false)
到中的deleteUser
函数来解决此问题App.js
。
就是这样。我们有一个完整的CRUD应用程序,利用React State和Effect Hook