在本教程中,你将用React构建一个应用程序。并且你将学习如何从头开始创建一个真正可重用的自动建议组件。
这个应用程序将允许用户在一个国家列表中搜索一个国家。它将在用户所输入的国家的输入栏下面显示匹配的建议。
通过建立这个应用程序,你将学会:
- 如何创建一个可重复使用的组件
- 如何使用
useRef钩子来管理自动建议 - 如何创建一个自定义的可重复使用的钩子
- 如何有效地进行搜索
以及更多:
你可以在这里找到最终应用程序的实时演示。
下面是自动建议功能的工作演示:

那么,让我们开始构建这个应用程序吧。
设置项目
我们将使用create-react-app来初始化该项目。
我们将使用React Hooks语法来创建组件。因此,如果你不熟悉它,请查看我关于钩子的文章。
通过执行以下命令创建一个新的React项目:
npx create-react-app react-autosuggestion-app
一旦你创建了这个项目,删除src 文件夹中的所有文件,在src 文件夹中创建index.js,App.js,styles.css 文件。
同时在src 文件夹内创建components 和custom-hooks 文件夹。
通过在终端或命令提示符下运行以下命令来安装所需的依赖项:
yarn add axios@0.21.1 lodash@4.17.21 react-bootstrap@1.6.1 bootstrap@5.1.0
一旦这些文件安装完毕,打开src/styles.css 文件,并将该文件中的内容加入其中。
如何建立初始页面
在public 文件夹中创建一个新的countries.json 文件,并将该文件中的内容加入其中。
在components 文件夹中创建一个AutoComplete.js 文件,代码如下:
import React from 'react';
function AutoComplete({ isVisible, suggestions, handleSuggestionClick }) {
return (
<div className={`${isVisible ? 'show suggestion-box' : 'suggestion-box'}`}>
<ul>
{suggestions.map((country, index) => (
<li key={index} onClick={() => handleSuggestionClick(country)}>
{country}
</li>
))}
</ul>
</div>
);
}
export default AutoComplete;
在这个文件中,一旦用户在输入文本框中输入东西,我们就会向用户显示建议。
在custom-hooks 文件夹中创建一个useOutsideClick.js 文件,代码如下:
import { useState, useRef, useEffect } from 'react';
const useOutsideClick = () => {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef();
const handleOutsideClick = () => {
if (ref.current) {
setIsVisible(false);
}
};
useEffect(() => {
document.addEventListener('click', handleOutsideClick);
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, []);
return [ref, isVisible, setIsVisible];
};
export default useOutsideClick;
在这里,我们创建了一个自定义钩子,它将显示/隐藏建议框。
最初,我们声明了一个状态,通过设置值为false 来隐藏建议框:
const [isVisible, setIsVisible] = useState(false);
然后我们声明了一个引用:
const ref = useRef();
我们将从我们的自定义钩子中返回这个ref ,同时返回isVisible 和setIsVisible ,就像这样:
return [ref, isVisible, setIsVisible];
因此,在我们使用useOutsideClick 钩子的组件内部,我们可以使用这个参考值将其分配给建议框。因此,如果有多个输入字段,那么每个输入字段将有自己的建议框和隐藏和显示功能。
在handleOutsideClick 函数中,我们有以下代码:
const handleOutsideClick = () => {
if (ref.current) {
setIsVisible(false);
}
};
在这里,我们正在检查ref.current ,因为我们希望只有在建议框的引用可用时才调用setIsVisible ,而不是每次点击页面都调用。
然后我们添加了事件处理程序来调用handleOutsideClick 函数:
useEffect(() => {
document.addEventListener('click', handleOutsideClick);
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, []);
我们还通过在组件卸载后从useEffect 钩子返回一个函数来移除事件处理程序。
如何创建一个可重复使用的React组件
现在,在components 文件夹内创建一个InputControl.js 文件,代码如下:
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import _ from 'lodash';
import { Form } from 'react-bootstrap';
import AutoComplete from './AutoComplete';
import useOutsideClick from '../custom-hooks/useOutsideClick';
const InputControl = ({ name, label, placeholder }) => {
const [documentRef, isVisible, setIsVisible] = useOutsideClick();
const [suggestions, setSuggestions] = useState([]);
const [selectedCountry, setSelectedCountry] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const ref = useRef();
useEffect(() => {
ref.current = _.debounce(processRequest, 300);
}, []);
function processRequest(searchValue) {
axios
.get('/countries.json')
.then((response) => {
const countries = response.data;
const result = countries.filter((country) =>
country.toLowerCase().includes(searchValue.toLowerCase())
);
setSuggestions(result);
if (result.length > 0) {
setIsVisible(true);
} else {
setIsVisible(false);
}
setErrorMsg('');
})
.catch(() => setErrorMsg('Something went wrong. Try again later'));
}
function handleSearch(event) {
event.preventDefault();
const { value } = event.target;
setSearchTerm(value);
ref.current(value);
}
function handleSuggestionClick(countryValue) {
setSelectedCountry(countryValue);
setIsVisible(false);
}
return (
<Form.Group controlId="searchTerm">
<Form.Label>{label}</Form.Label>
<Form.Control
className="input-control"
type="text"
value={searchTerm}
name={name}
onChange={handleSearch}
autoComplete="off"
placeholder={placeholder}
/>
<div ref={documentRef}>
{isVisible && (
<AutoComplete
isVisible={isVisible}
suggestions={suggestions}
handleSuggestionClick={handleSuggestionClick}
/>
)}
</div>
{selectedCountry && (
<div className="selected-country">
Your selected country: {selectedCountry}
</div>
)}
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
</Form.Group>
);
};
export default InputControl;
在这个文件中,我们已经创建了一个可重复使用的组件,在组件中可以使用搜索和建议。
最初,我们引用了useOutsideClick 钩子:
const [documentRef, isVisible, setIsVisible] = useOutsideClick();
我们将从钩子返回的参考值存储在documentRef 变量中。
每当用户在文本框中输入东西时,我们就会进行API调用,以获得具有匹配搜索条件的国家列表。
但是为了避免在文本框中输入的每个字符都进行不必要的API调用,我们将使用lodash库的debounce方法。它让我们在用户停止输入后的300毫秒后才调用API,使用以下代码:
ref.current = _.debounce(processRequest, 300);
_.debounce 函数的调用返回一个我们存储在ref.current 变量中的函数。一旦300毫秒过去,我们将调用存储在那里的函数。
我们使用ref而不是普通变量,因为我们需要这个初始化在组件安装时只发生一次。当某些状态或道具发生变化时,普通变量的值会在组件的每次重新渲染中丢失。
我们通过传递用户输入的值,从handleSearch 函数中调用存储在ref.current 的函数。
所以一旦我们调用存储在ref.current 中的函数,processRequest 函数就会在幕后被调用。
processRequest 函数将自动接收传递给ref.current 函数的值。
在processRequest 函数中,我们进行一个API调用,以获得国家列表:
function processRequest(searchValue) {
axios
.get('/countries.json')
.then((response) => {
const countries = response.data;
const result = countries.filter((country) =>
country.toLowerCase().includes(searchValue.toLowerCase())
);
setSuggestions(result);
if (result.length > 0) {
setIsVisible(true);
} else {
setIsVisible(false);
}
setErrorMsg('');
})
.catch(() => setErrorMsg('Something went wrong. Try again later'));
}
在这里,一旦我们得到来自API的响应,我们将使用数组过滤方法,只过滤出符合所提供的搜索词的国家。
然后,我们使用setSuggestions(result) ,在建议状态下设置出国家列表。
接下来,我们检查结果数组的长度来显示或隐藏建议框。
如果你检查从该组件返回的JSX,它看起来像这样:
return (
<Form.Group controlId="searchTerm">
<Form.Label>{label}</Form.Label>
<Form.Control
className="input-control"
type="text"
value={searchTerm}
name={name}
onChange={handleSearch}
autoComplete="off"
placeholder={placeholder}
/>
<div ref={documentRef}>
{isVisible && (
<AutoComplete
isVisible={isVisible}
suggestions={suggestions}
handleSuggestionClick={handleSuggestionClick}
/>
)}
</div>
{selectedCountry && (
<div className="selected-country">
Your selected country: {selectedCountry}
</div>
)}
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
</Form.Group>
);
在这里,对于输入文本框,我们添加了一个handleSearch onChange handler,看起来像这样:
function handleSearch(event) {
event.preventDefault();
const { value } = event.target;
setSearchTerm(value);
ref.current(value);
}
我们用用户输入的值更新searchTerm 状态。然后我们通过传递用户输入的值来调用存储在ref.current 的函数。
调用ref.current 内部调用processRequest 函数,在这里我们实际上是在调用API。
然后,在输入文本框之后,我们添加了一个带有ref的div来显示建议:
<div ref={documentRef}>
{isVisible && (
<AutoComplete
isVisible={isVisible}
suggestions={suggestions}
handleSuggestionClick={handleSuggestionClick}
/>
)}
</div>
只有当isVisible 为真时,我们才会显示建议,这发生在我们从processRequest 函数中的API获得结果时。
在这里,我们将建议传递给AutoComplete 组件来显示。
一旦我们点击任何一个建议,handleSuggestionClick 函数就会被执行,从而更新selectedCountry 并隐藏建议:
function handleSuggestionClick(countryValue) {
setSelectedCountry(countryValue);
setIsVisible(false);
}
如何使用可重复使用的组件
现在,打开App.js 文件并在其中添加以下代码:
import React from 'react';
import { Form } from 'react-bootstrap';
import InputControl from './components/InputControl';
const App = () => {
return (
<div className="main">
<h1>React AutoSuggestion Demo</h1>
<div className="search-form">
<Form>
<InputControl
name="country"
label="Enter Country"
placeholder="Type a country name"
/>
</Form>
</div>
</div>
);
};
export default App;
现在,通过在终端或命令提示符中运行以下命令来启动该应用程序:
yarn start

正如你所看到的,一旦你从建议中选择任何值,所选的值就会显示在文本框的下面。
**注意:**我们已经创建了一个单独的InputControl 组件,显示输入字段和它的建议框。
因此,我们可以再次使用相同的InputControl 组件来显示另一个输入文本框中的建议,如下图所示:
import React from 'react';
import { Form } from 'react-bootstrap';
import InputControl from './components/InputControl';
const App = () => {
return (
<div className="main">
<h1>React AutoSuggestion Demo</h1>
<div className="search-form">
<Form>
<InputControl
name="country"
label="Enter Country"
placeholder="Type a country name"
/>
<InputControl
name="country"
label="Enter Country"
placeholder="Type a country name"
/>
</Form>
</div>
</div>
);
};
export default App;

正如你所看到的,我们已经为国家添加了另一个InputControl 组件,所以我们能够分别处理每个输入文本框的建议。
因此,如果你想为另一个文本框显示不同的建议,你只需向InputControl 组件传递一个额外的道具,并根据该道具在建议框中显示不同的结果。
总结
正如我们在本教程中所看到的,通过创建一个可重用的InputControl 组件和使用ref 来分别管理每个输入文本框的建议,我们能够创建一个真正可重用的组件来显示自动完成建议。
你可以在这个资源库中找到本教程的完整源代码,并在这里找到实时演示。
谢谢你的阅读!