如何从零开始创建一个真正可重用的React组件

119 阅读7分钟

在本教程中,你将用React构建一个应用程序。并且你将学习如何从头开始创建一个真正可重用的自动建议组件。

这个应用程序将允许用户在一个国家列表中搜索一个国家。它将在用户所输入的国家的输入栏下面显示匹配的建议。

通过建立这个应用程序,你将学会:

  • 如何创建一个可重复使用的组件
  • 如何使用useRef 钩子来管理自动建议
  • 如何创建一个自定义的可重复使用的钩子
  • 如何有效地进行搜索

以及更多:

你可以在这里找到最终应用程序的实时演示。

下面是自动建议功能的工作演示:

那么,让我们开始构建这个应用程序吧。

设置项目

我们将使用create-react-app来初始化该项目。

我们将使用React Hooks语法来创建组件。因此,如果你不熟悉它,请查看我关于钩子的文章

通过执行以下命令创建一个新的React项目:

npx create-react-app react-autosuggestion-app

一旦你创建了这个项目,删除src 文件夹中的所有文件,在src 文件夹中创建index.js,App.js,styles.css 文件。

同时在src 文件夹内创建componentscustom-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 ,同时返回isVisiblesetIsVisible ,就像这样:

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 来分别管理每个输入文本框的建议,我们能够创建一个真正可重用的组件来显示自动完成建议。

你可以在这个资源库中找到本教程的完整源代码,并在这里找到实时演示。

谢谢你的阅读!