在 React 中构建实时搜索过滤器:分步指南
目录
1.简介
2.要求
3.步骤 1:设置项目
4.步骤 2:创建输入
5.步骤 3:渲染项目列表
6.步骤 4:构建过滤功能
7.步骤 5:从 API 获取项目并进行过滤
8.步骤 6:少量重构(奖励)
8.1. ItemList 组件
8.2.输入组件
8.3. API 调用
9.结论
介绍
当我开始使用 React 时,我遇到的一个常见挑战是实现实时搜索过滤功能。该功能会在用户输入时更新显示的项目,如果搜索过滤器为空,则会重新显示所有项目。因此,在本教程中,我将指导您完成在 React 中创建此功能的步骤。我们将从硬编码的项目列表开始,然后介绍从 API 获取的项目列表。
在本教程结束时,您将对如何构建这一宝贵功能有深入的理解。我们将首先在 App.jsx 中实现完整的功能,然后将其重构为可复用的组件。
让我们开始吧!
要求
- 已安装 Npm 和 Node.js
- React 基础知识
步骤1:设置项目
为了设置项目,我们将使用 Vite。因此,打开终端并执行以下命令:
npm create vite search-filter --template react
您可以随意将“search-filter”替换为您喜欢的项目名称。这应该足以创建项目。但是,如果您在命令行界面中遇到任何选项,请确保在提示选择框架时选择“React”,并在提示选择变体时选择“Javascript”。
现在,导航到项目目录并通过执行以下命令安装所需的包:
cd search-filter
npm install
之后,您可以运行命令npm run dev
。您应该能够打开项目http://localhost:5173/
并看到类似以下内容:
步骤 2:创建输入
现在,在您常用的 IDE(我个人使用 VSCode)中打开项目,然后找到该src/App.jsx
文件。删除其内容,从头开始。如果您不想使用默认样式,可以import './index.css'
从src/main.jsx
文件中删除该行。如果您想保留默认样式,也可以选择保留它。就我而言,我会删除它。
我们将首先构建其中的全部功能App,jsx
,然后将其重构为可重复使用的组件。
首先,让我们创建具有适当状态的搜索输入来控制其值:
// src/App.jsx
import { useState } from 'react'
function App() {
const [searchItem, setSearchItem] = useState('')
const handleInputChange = (e) => {
const searchTerm = e.target.value;
setSearchItem(searchTerm)
}
return (
<div>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
</div>
)
}
export default App
这样,我们就实现了一个函数式输入框。我们将其值设置为searchItem
状态,该状态会在事件处理函数的作用下,每当用户输入内容时更新onChange
。我们也可以简单地在输入框中直接使用该setSearchItem
函数,但我们这样做是因为我们很快会在处理函数中添加更多功能。
你应该有类似这样的内容:
步骤 3:渲染项目列表
接下来,让我们添加一个项目列表并将其呈现在输入下方。
// src/App.jsx
import { useState } from 'react'
const users = [
{ firstName: "John", id: 1 },
{ firstName: "Emily", id: 2 },
{ firstName: "Michael", id: 3 },
{ firstName: "Sarah", id: 4 },
{ firstName: "David", id: 5 },
{ firstName: "Jessica", id: 6 },
{ firstName: "Daniel", id: 7 },
{ firstName: "Olivia", id: 8 },
{ firstName: "Matthew", id: 9 },
{ firstName: "Sophia", id: 10 }
]
function App() {
const [searchItem, setSearchItem] = useState('')
const handleInputChange = (e) => {
const searchTerm = e.target.value;
setSearchItem(searchTerm)
}
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
<ul>
{users.map(user => <li key={user.id}>{user.firstName}</li>)}
</ul>
</>
)
}
export default App
太好了,现在我们有一个项目列表,我们将其存储在users
变量中,然后我们使用 map 方法循环遍历列表并使用li
标签呈现每个项目。
你应该看到类似这样的内容:
步骤 4:构建过滤功能
太棒了!我们有了输入框和物品,现在让我们进入最有趣的部分👀我们需要根据用户在输入框中输入的内容来更改要渲染的物品列表。为此,我们需要做三件事:
- 添加一个状态来保存已过滤的项目,并将其设置为用户变量,
- 在事件处理程序中,我们需要根据用户正在写入的内容过滤项目,并将结果设置为过滤项目状态
- 渲染过滤后的项目而不是用户变量
那么让我们开始吧:
// src/App.jsx
//... users variable and imports
function App() {
const [searchItem, setSearchItem] = useState('')
const [filteredUsers, setFilteredUsers] = useState(users)
const handleInputChange = (e) => {
const searchTerm = e.target.value;
setSearchItem(searchTerm)
const filteredItems = users.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filteredItems);
}
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
<ul>
{filteredUsers.map(user => <li key={user.id}>{user.firstName}</li>)}
</ul>
</>
)
}
export default App
我以前常犯的一个错误是,在处理程序函数中过滤项目时,我使用了过滤项目状态(filteredUsers
)而不是包含所有项目的变量(users
),这导致过滤器在您第一次开始输入时出现不必要的行为,但如果您删除一些字符,项目就不会再次被过滤,如果您删除所有字符,您就不会再看到整个项目列表。
发生这种情况的原因是,当组件首次加载时,filteredUsers
初始状态包含所有用户,但是当您开始输入时,它会更新为与搜索输入值匹配的新用户数组,然后它再也无法访问完整的用户列表。
因此,在进行过滤时,始终使用包含所有项目的变量进行过滤非常重要。
现在运行得很好,你应该得到如下结果:
但在现实生活中,更常见的情况是您需要从 API 获取项目列表,然后过滤这些项目,所以让我们看看如何做到这一点。
步骤 5:从 API 获取项目并进行筛选
让我们使用DummyJSON API 来获取用户。我们需要在组件加载时获取用户(可以使用useEffect
for 来实现),将他们保存到状态中,然后渲染到列表中。嗯,我们已经有了filteredUsers
状态,也就是我们用来渲染用户的状态,所以也许我们可以将从 API 获取的用户存储在那里,然后过滤这些用户,对吗?
嗯……不,这又是一个我以前常犯的错误😕。如果我们这样做,就会犯我上面提到的那个错误:组件首次加载时,状态filteredUsers
为空,然后useEffect
执行并从 API 获取用户,将其设置为filteredUsers
状态并渲染。现在,你拥有了完整的项目列表,filteredUsers
因此你可以使用它来进行过滤,但是当你开始输入时,该状态就会更新,并且它再也无法访问完整的用户列表。
那么我们该怎么办呢?解决方案是创建另一个状态,我们称之为apiUsers
,它将在组件首次加载时保存完整的用户列表,其行为类似于users
我们最初使用的变量。然后,我们使用apiUsers
,而不是 ,filteredUsers
在用户输入时过滤项目。我们不再需要之前的users
变量,所以我们删除它,并将filteredUsers
状态初始化为一个空数组。
// src/App.jsx
import { useState, useEffect } from 'react'
// We no longer need the users variable so you can remove it from here
function App() {
// add this state
const [apiUsers, setApiUsers] = useState([])
const [searchItem, setSearchItem] = useState('')
// set the initial state of filteredUsers to an empty array
const [filteredUsers, setFilteredUsers] = useState([])
// fetch the users
useEffect(() => {
fetch('https://dummyjson.com/users')
.then(response => response.json())
// save the complete list of users to the new state
.then(data => setApiUsers(data.users))
// if there's an error we log it to the console
.catch(err => console.log(err))
}, [])
const handleInputChange = (e) => {
const searchTerm = e.target.value;
setSearchItem(searchTerm)
// filter the items using the apiUsers state
const filteredItems = apiUsers.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filteredItems);
}
return (
// ... component rendering
)
}
一切都很好,但是……为什么组件首次加载时看不到任何项目?😱 答案是,我们正在映射状态filteredUsers
来渲染用户,而状态现在为空,所以在你开始输入之前不会显示任何用户。我们如何才能在组件首次加载时渲染完整的用户列表,并在输入时渲染筛选后的用户呢?
有一个简单的解决方案。之前我们filteredUsers
用完整的用户列表初始化状态,现在不能这样做了,因为完整的用户列表存储在状态中apiUsers
,而且状态一开始也是一个空数组,但当我们获取用户时它会更新,对吧?所以我们需要做的就是将用户存储在状态中,并且在从 API 获取用户时也存储apiUsers
在状态中。filteredUsers
// src/App.jsx
import { useState, useEffect } from 'react'
function App() {
// ...states
useEffect(() => {
fetch('https://dummyjson.com/users')
.then(response => response.json())
.then(data => {
setApiUsers(data.users)
// update the filteredUsers state
setFilteredUsers(data.users)
})
.catch(err => console.log(err))
}, [])
// ...handler and component rendering
}
太棒了!问题解决了🙌🏼现在我们拥有功能齐全的项目过滤功能,数据来自 API 🎉
您可以看到,如果没有用户与我们在输入中写的内容匹配,我还添加了“未找到用户”消息,您可以通过编辑呈现用户的部分来完成此操作,并使其如下所示:
// src/App.jsx
import { useState, useEffect } from 'react'
function App() {
// ...state, data fetching, handler
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
{filteredUsers.length === 0
? <p>No users found</p>
: <ul>
{filteredUsers.map(user => <li key={user.id}>{user.firstName}</li>)}
</ul>
}
</>
)
}
但是您可能会注意到,在首次呈现页面时,“未找到项目”消息会短暂闪烁,我们可以通过添加加载状态来解决这个问题,这样,如果正在获取用户,您可以显示“正在加载...”消息或微调器,error
如果获取不起作用,我们还可以添加一个状态来显示正确的错误消息。
// src/App.jsx
function App() {
const [apiUsers, setApiUsers] = useState([])
// initialize the loading state as true
const [loading, setLoading] = useState(true)
// initialize the error state as null
const [error, setError] = useState(null)
const [searchItem, setSearchItem] = useState('')
const [filteredUsers, setFilteredUsers] = useState([])
useEffect(() => {
fetch('https://dummyjson.com/users')
.then(response => response.json())
.then(data => {
setApiUsers(data.users)
setFilteredUsers(data.users)
})
.catch(err => {
console.log(err)
// update the error state
setError(err)
})
.finally(() => {
// wether we sucessfully get the users or not,
// we update the loading state
setLoading(false)
})
}, [])
//... on change handler
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
{/* if the data is loading, show a proper message */}
{loading && <p>Loading...</p>}
{/* if there's an error, show a proper message */}
{error && <p>There was an error loading the users</p>}
{/* if it finished loading, render the items */}
{!loading && !error && filteredUsers.length === 0
? <p>No users found</p>
: <ul>
{filteredUsers.map(user => <li key={user.id}>{user.firstName}</li>)}
</ul>
}
</>
)
}
步骤 6:稍微重构(奖励)
现在一切正常,但如果我们想在其他地方复用输入和列表怎么办?我们可以稍微重构一下,让组件可复用,这样就可以避免重复代码。让我们一步一步看看如何做到这一点。
ItemList 组件
首先components
在 下创建一个新文件夹src
。我们先来处理项目列表,因此创建一个名为ItemList.jsx
under 的新文件src/components
,并将渲染项目的部分(仅包含映射,不包含loading
和error
状态检查)复制到新创建的文件中。现在,要渲染为列表的项目将通过 props 获取,因此我们将变量更改为filteredUsers
,items
使其更通用。新组件应如下所示:
// src/components/ItemList.jsx
// get the items in the props
const ItemsList = ({items}) => {
return (
<>
{/* replace filteredUsers with items*/}
{items.length === 0
? <p>No users found</p>
: <ul>
{items.map(item => <li key={item.id}>{item.firstName}</li>)}
</ul>
}
</>
)
}
export default ItemsList
很好,现在让我们在App.jsx
文件中使用我们的新组件,用新组件替换正在渲染的项目列表并将其filteredUsers
作为 prop 的值传递items
。
// src/App.jsx
// ... other imports
import ItemList from './components/ItemsList'
function App() {
//... component logic
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
{loading && <p>Loading...</p>}
{error && <p>There was an error loading the users</p>}
{!loading && !error && <ItemList items={filteredUsers} />}
</>
)
}
输入组件
现在让我们进行输入,创建一个名为的新文件Input.jsx
,src/components
并将输入标签从复制src/App.jsx
到新组件,现在应该如下所示:
// src/components/Input.jsx
const Input = () => {
return (
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
)
}
export default Input
现在,在这个新组件上,我们无法访问状态searchItem
和handleInputChange
函数,因此会抛出错误。我们可以将这两个值作为 props 传递,但让我们做些更有趣的事情。让我们让元素input
能够在组件内部管理自己的值,因此我们创建一个新的状态来处理它,并创建一个新的处理函数来更新状态。
App
你可能会想,如果组件无法再访问输入值,我们该如何过滤它呢?答案是:用回调!我们可以将回调函数作为 prop 传递,它会在处理函数内部运行,并将输入值作为参数。所以,将Input.jsx
组件改成这样:
// src/components/Input.jsx
import { useState } from "react"
const Input = ({ onChangeCallback }) => {
// state to handle the input value
const [value, setValue] = useState('')
// new handler function that will update the state
// when the input changes
const handleChange = (e) => {
const inputValue = e.target.value;
setValue(inputValue)
// if the component receives a callback, call it,
// and pass the input value as an argument
onChangeCallback && onChangeCallback(inputValue)
}
return (
<input
type="text"
value={value}
onChange={handleChange}
placeholder='Type to search'
/>
)
}
export default Input
并更改App.jsx
文件以导入我们的新Input
组件并删除所有不必要的代码,记得将之前的处理函数App.jsx
作为 prop 传递给输入组件:
// src/App.jsx
import { useState, useEffect } from 'react'
// import the Input component
import Input from './components/Input'
function App() {
const [apiUsers, setApiUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// we no longer need the searchItem state so you can remove it
const [filteredUsers, setFilteredUsers] = useState([])
// ... useEffect code without changes here
// this is the previous handleInputChange function, I changed
// its name to better represent its new functionality of only
// filtering the items
const filterItems = (searchTerm) => {
// we previously set the input state here,
// you can remove that now
const filteredItems = apiUsers.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filteredItems);
}
return (
<>
{/* Use the new Input component instead of the input tag */}
<Input onChangeCallback={filterItems} />
{loading && <p>Loading...</p>}
{error && <p>There was an error loading the users</p>}
{!loading && !error && <ItemList items={filteredUsers} />}
</>
)
}
export default App
完美!过滤功能应该还能正常工作。
API 调用
我们还可以将 API 调用重构为自定义钩子。在 文件夹hooks
下创建一个新文件夹src
,然后在其中创建一个名为 的新文件useGetUsers.jsx
。现在,我们将apiUsers
、loading
、 和error
状态,以及useEffect
进行 API 调用的 移动到新的钩子中。然后,我们从钩子中返回 、 和 状态。我们也可以将状态重命名users
为loading
,因为这个新文件中没有其他相关变量。error
apiUsers
users
// src/hooks/useGetUsers.jsx
import { useState, useEffect } from 'react'
export const useGetUsers = () => {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('https://dummyjson.com/users')
.then(response => response.json())
.then(data => {
setUsers(data.users)
})
.catch(err => {
console.log(err)
setError(err)
})
.finally(() => {
setLoading(false)
})
}, [])
return { users, loading, error }
}
您可以看到,我们还setFiltered
从中删除了状态函数useEffect
,那么现在如何设置已过滤的用户呢?我们只需要对App.jsx
文件进行以下修改。
// src/App.jsx
import { useState, useEffect } from 'react'
import Input from './components/Input'
import ItemList from './components/ItemsList'
// import our new hook
import { useGetUsers } from './hooks/useGetUsers'
function App() {
// use our custom hook to get our users and
// the error and loading variables
const {users, loading, error} = useGetUsers()
const [filteredUsers, setFilteredUsers] = useState([])
useEffect(() => {
// check if the users are not empty, if so then the
// API call was successful and we can update our
// filteredUsers state
if (Object.keys(users).length > 0) {
setFilteredUsers(users)
}
}, [users]) // this effect should run when the users state gets updated
const filterItems = (searchTerm) => {
// we now use 'users' instead of 'apiUsers' to do the filtering
const filteredItems = users.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filteredItems);
}
// ... rest of the component stays the same
}
export default App
结论
哇!这可真是让人头大,对吧?😅 就这样!现在你知道了如何在 React 中创建实时搜索过滤器,并使用来自 API 的条目!🤘🏼 而且,如果你完成了额外的步骤,你还学到了一些将应用程序拆分成可重用组件和自定义钩子的技巧。虽然说实话,对于这么小的应用程序来说,这种重构可能有点过头了,但我这样做只是为了写这篇文章😅
希望本指南对您有所帮助!如果您有任何不明白的地方,或者发现可以改进的地方,欢迎在评论区留言。祝您编程愉快!🤘🏼
文章来源:https://dev.to/alais29dev/building-a-real-time-search-filter-in-react-a-step-by-step-guide-3lmm