在 React 中构建实时搜索过滤器:分步指南

2025-05-24

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

您可以随意将“search-filter”替换为您喜欢的项目名称。这应该足以创建项目。但是,如果您在命令行界面中遇到任何选项,请确保在提示选择框架时选择“React”,并在提示选择变体时选择“Javascript”。

现在,导航到项目目录并通过执行以下命令安装所需的包:

cd search-filter
npm install
Enter fullscreen mode Exit fullscreen mode

之后,您可以运行命令npm run dev。您应该能够打开项目http://localhost:5173/并看到类似以下内容:

步骤1

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

这样,我们就实现了一个函数式输入框。我们将其值设置为searchItem状态,该状态会在事件处理函数的作用下,每当用户输入内容时更新onChange。我们也可以简单地在输入框中直接使用该setSearchItem函数,但我们这样做是因为我们很快会在处理函数中添加更多功能。

你应该有类似这样的内容:

第 2 步

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

太好了,现在我们有一个项目列表,我们将其存储在users变量中,然后我们使用 map 方法循环遍历列表并使用li标签呈现每个项目。

你应该看到类似这样的内容:

步骤3

步骤 4:构建过滤功能

太棒了!我们有了输入框和物品,现在让我们进入最有趣的部分👀我们需要根据用户在输入框中输入的内容来更改要渲染的物品列表。为此,我们需要做三件事:

  1. 添加一个状态来保存已过滤的项目,并将其设置为用户变量,
  2. 在事件处理程序中,我们需要根据用户正在写入的内容过滤项目,并将结果设置为过滤项目状态
  3. 渲染过滤后的项目而不是用户变量

那么让我们开始吧:

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

我以前常犯的一个错误是,在处理程序函数中过滤项目时,我使用了过滤项目状态(filteredUsers)而不是包含所有项目的变量(users),这导致过滤器在您第一次开始输入时出现不必要的行为,但如果您删除一些字符,项目就不会再次被过滤,如果您删除所有字符,您就不会再看到整个项目列表。

发生这种情况的原因是,当组件首次加载时,filteredUsers初始状态包含所有用户,但是当您开始输入时,它会更新为与搜索输入值匹配的新用户数组,然后它再也无法访问完整的用户列表。

因此,在进行过滤时,始终使用包含所有项目的变量进行过滤非常重要。

现在运行得很好,你应该得到如下结果:

步骤4

但在现实生活中,更常见的情况是您需要从 API 获取项目列表,然后过滤这些项目,所以让我们看看如何做到这一点。

步骤 5:从 API 获取项目并进行筛选

让我们使用DummyJSON API 来获取用户。我们需要在组件加载时获取用户(可以使用useEffectfor 来实现),将他们保存到状态中,然后渲染到列表中。嗯,我们已经有了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
  )
}
Enter fullscreen mode Exit fullscreen mode

一切都很好,但是……为什么组件首次加载时看不到任何项目?😱 答案是,我们正在映射状态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

}
Enter fullscreen mode Exit fullscreen mode

太棒了!问题解决了🙌🏼现在我们拥有功能齐全的项目过滤功能,数据来自 API 🎉

步骤5

您可以看到,如果没有用户与我们在输入中写的内容匹配,我还添加了“未找到用户”消息,您可以通过编辑呈现用户的部分来完成此操作,并使其如下所示:

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

但是您可能会注意到,在首次呈现页面时,“未找到项目”消息会短暂闪烁,我们可以通过添加加载状态来解决这个问题,这样,如果正在获取用户,您可以显示“正在加载...”消息或微调器,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>
      }
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

步骤 6:稍微重构(奖励)

现在一切正常,但如果我们想在其他地方复用输入和列表怎么办?我们可以稍微重构一下,让组件可复用,这样就可以避免重复代码。让我们一步一步看看如何做到这一点。

ItemList 组件

首先components在 下创建一个新文件夹src。我们先来处理项目列表,因此创建一个名为ItemList.jsxunder 的新文件src/components,并将渲染项目的部分(仅包含映射,不包含loadingerror状态检查)复制到新创建的文件中。现在,要渲染为列表的项目将通过 props 获取,因此我们将变量更改为filteredUsersitems使其更通用。新组件应如下所示:

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

很好,现在让我们在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} />}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

输入组件

现在让我们进行输入,创建一个名为的新文件Input.jsxsrc/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
Enter fullscreen mode Exit fullscreen mode

现在,在这个新组件上,我们无法访问状态searchItemhandleInputChange函数,因此会抛出错误。我们可以将这两个值作为 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
Enter fullscreen mode Exit fullscreen mode

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

完美!过滤功能应该还能正常工作。

API 调用

我们还可以将 API 调用重构为自定义钩子。在 文件夹hooks下创建一个新文件夹src,然后在其中创建一个名为 的新文件useGetUsers.jsx。现在,我们将apiUsersloading、 和error状态,以及useEffect进行 API 调用的 移动到新的钩子中。然后,我们从钩子中返回 、 和 状态。我们也可以将状态重命名usersloading因为这个新文件中没有其他相关变量。errorapiUsersusers

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

您可以看到,我们还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
Enter fullscreen mode Exit fullscreen mode

结论

哇!这可真是让人头大,对吧?😅 就这样!现在你知道了如何在 React 中创建实时搜索过滤器,并使用来自 API 的条目!🤘🏼 而且,如果你完成了额外的步骤,你还学到了一些将应用程序拆分成可重用组件和自定义钩子的技巧。虽然说实话,对于这么小的应用程序来说,这种重构可能有点过头了,但我这样做只是为了写这篇文章😅

希望本指南对您有所帮助!如果您有任何不明白的地方,或者发现可以改进的地方,欢迎在评论区留言。祝您编程愉快!🤘🏼

文章来源:https://dev.to/alais29dev/building-a-real-time-search-filter-in-react-a-step-by-step-guide-3lmm
PREV
OOP 的 SOLID 原则
NEXT
开发工具:面向开发人员的 Markdown 编辑器🔥 The Lab 🧪