Функция поиска  —  одна из самых важных особенностей программного приложения. Поисковые сайты типа Google и DuckDuckGo помогают миллионам пользователей бороздить просторы Интернета и за считанные секунды находить то, что они ищут.

Было бы неплохо включить функцию поиска в приложение на React. Но как быть, если не хочется настраивать отдельный сервер исключительно для работы с поиском. Есть ли способ добавить простой поиск в приложение на React?

Да, такой способ существует. Fuse.js  —  это мощная, легковесная поисковая библиотека, предназначенная для ведения поиска во внешней части приложения. Библиотека использует метод нечёткого поиска, который находит не только точные совпадения, но и строки, приблизительно соответствующие заданному шаблону.

Метод нечёткого поиска замечателен тем, что позволяет приложению возвращать близкие совпадения, даже если в шаблоне поиска допущена ошибка в написании слова или имеет место неправильный порядок слов в предложении.

Что будем создавать

Это руководство поможет вам создать с помощью Fuse.js простое приложение на React с собственной функцией поиска. Приложение будет предоставлять подборку книг для разработчиков программного обеспечения с возможностью применения фильтра функции поиска.

Вот как будет выглядеть итоговый результат:

Мои любимые книги. Отлично подойдут и для разработчиков программного обеспечения

Демоверсия проекта, а также его компоненты выложены на Bit.dev. Изучите её и установите эти компоненты в свой проект.

Первым делом необходимо выполнить начальную загрузку нового приложения на React с помощью Create React App:

npx create-react-app react-fusejs-example

Завершив загрузку, начнём прописывать данные книг в виде массива объектов. Эти данные будут передаваться в приложение и отображаться в виде списка карточек:

Создание списка книг

Создадим новый файл books.json и список книг. Данные будут иметь следующие свойства:

  • Строка title для названий книг.
  • Строка author для автора.
  • Строка image для атрибута src изображения книги.
  • url-адрес для ссылки на покупку или просмотр книги.

Вот файл на случай, если захотите его скопировать:

[
  {
    "title": "Steve Jobs",
    "image": "https://images-na.ssl-images-amazon.com/images/I/41dKkez-1rL._SX326_BO1,204,203,200_.jpg",
    "author": "Walter Isaacson",
    "url": "https://www.amazon.com/Steve-Jobs-Walter-Isaacson/dp/1451648537"
  },
  {
    "title": "Zero to One",
    "image": "https://images-na.ssl-images-amazon.com/images/I/4137OkbPQ4L._SX331_BO1,204,203,200_.jpg",
    "author": "Peter Thiel, Blake Masters",
    "url": "https://www.amazon.com/Zero-One-Notes-Startups-Future/dp/0804139296"
  },
  {
    "title": "The Pragmatic Programmer",
    "image": "https://images-na.ssl-images-amazon.com/images/I/51cUVaBWZzL._SX380_BO1,204,203,200_.jpg",
    "author": "David Thomas, Andrew Hunt",
    "url": "https://www.amazon.com/Pragmatic-Programmer-journey-mastery-Anniversary/dp/0135957052"
  },
  {
    "title": "The Unicorn Project",
    "image": "https://images-na.ssl-images-amazon.com/images/I/51A4T36jisL._SX334_BO1,204,203,200_.jpg",
    "author": "Gene Kim",
    "url": "https://www.amazon.com/Unicorn-Project-Developers-Disruption-Thriving/dp/1942788762"
  },
  {
    "title": "The Passionate Programmer",
    "image": "https://images-na.ssl-images-amazon.com/images/I/51m3yzmDFCL._SX331_BO1,204,203,200_.jpg",
    "author": "Chad Fowler",
    "url": "https://www.amazon.com/Passionate-Programmer-Remarkable-Development-Pragmatic-ebook/dp/B00AYQNR5U"
  },
  {
    "title": "Hatching Twitter",
    "image": "https://m.media-amazon.com/images/I/51YUkI5ZQ-L.jpg",
    "author": "Nick Bilton",
    "url": "https://www.amazon.com/Hatching-Twitter-Story-Friendship-Betrayal-ebook/dp/B00CDUVSQ0"
  },
  {
    "title": "How Google Works",
    "image": "https://images-na.ssl-images-amazon.com/images/I/31Xc+yFta0L._SX327_BO1,204,203,200_.jpg",
    "author": "Eric Schmidt, Jonathan Rosenberg",
    "url": "https://www.amazon.com/How-Google-Works-Eric-Schmidt/dp/1455582328"
  },
  {
    "title": "Elon Musk",
    "image": "https://m.media-amazon.com/images/I/51tw6UjHpDL.jpg",
    "author": "Ashlee Vance",
    "url": "https://www.amazon.com/Elon-Musk-SpaceX-Fantastic-Future-ebook/dp/B00KVI76ZS"
  },
  {
    "title": "Six Easy Pieces",
    "image": "https://m.media-amazon.com/images/I/51E53HCUKVL.jpg",
    "author": "Richard P. Feynman",
    "url": "https://www.amazon.com/Six-Easy-Pieces-Essentials-Explained-ebook/dp/B004OVEYNU"
  },
  {
    "title": "Sapiens",
    "image": "https://m.media-amazon.com/images/I/51Sn8PEXwcL.jpg",
    "author": "Yuval Noah Harari",
    "url": "https://www.amazon.com/Sapiens-Humankind-Yuval-Noah-Harari-ebook/dp/B00ICN066A"
  }
]

Теперь, когда есть данные о книгах, нужен компонент, который будет принимать эти данные и отображать их в виде простой карточки в приложении на React:

Компонент из одной карточки

Создание компонента «Card»

У компонента Card будет четыре свойства для обработки передаваемых в него данных:

<Card  
  image="https://images-na.ssl-images-amazon.com/images/I/41dKkez-1rL._SX326_BO1,204,203,200_.jpg"
  title="Steve Jobs"
  author="Walter Isaacson"
  url="https://www.amazon.com/Steve-Jobs-Walter-Isaacson/dp/1451648537"
 />

Вот JSX-структура компонента:

const Card = ({image, title, author, url}) => {
  return (
    <div className="CardWrapper">
      <div className="ColImg">
        <img className="Img" src={image} alt={title} />
      </div>
      <div className="ColDetail">
        <div className="Header">
          <div className="BookTitle">{title}</div>
        </div>
        <div className="Description">{author}</div>
        <a className="Link" href={url}>
          Learn more
        </a>
      </div>
    </div>
  );
};

.BookTitle {
  font-size: 20px;
}

.Description {
  color: #757575;
  font-size: 14px;
  margin-bottom:10px;
}

.Link {
  font-size: 14px;
}

А вот и стилевое оформление компонента:

.CardWrapper {
  flex: 0 1 300px;
  margin: 12px;
  overflow: hidden;
  padding: 16px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.05),
    0 0px 10px rgba(0, 0, 0, 0.08);
  border-radius: 5px;
  flex-wrap: wrap;
  min-width: 300px;
}.

Header {
  width: 100%;
  display: flex;
  margin-bottom: 10px;
}.

ColImg {
  width: 30%;
  float: left;
}

.ColDetail {
  width: 70%;
  float: left;
}

.Img {
  height: 100px;
}

Можете просто загрузить компонент из этих коллекций на Bit:

npm install @bit/nsebhastian.react_fusejs.card

Затем просто импортируем компонент в файл App.js. Нужно лишь применить map() к массиву JSON в компоненте Card. Добавим <div> с именем класса Container, чтобы улучшить интерфейс:

import React from "react";
import "./App.css";
import books from "./books.json";
import Card from "@bit/nsebhastian.react_fusejs.card";function App() {
  return (
    <div className="Container">
      {books.map((item) => (
        <Card {...item} key={item.name} />
      ))}
    </div>
  );
}export default App;

Вот CSS для класса Container:

.Container {
  width: 80%;
  margin: 0 auto;
  margin-top: 45px;
  display: flex;
  flex-wrap: wrap;
}

Список карточек завершён. Перейдём теперь к созданию компонента поиска Search.

Компонент Search состоит всего лишь из одного текстового ввода с двумя свойствами:

  • placeholder для замещающего текста в поле ввода;
  • функция onChange, которая запускается, когда пользователь вводит что-то в поле ввода.

Напишем JSX-элемент для этого компонента:

const SearchBar = ({onChange, placeholder}) => {
  return (
    <div className="Search">
      <span className="SearchSpan">
        <FaSearch />
      </span>
      <input
        className="SearchInput"
        type="text"
        onChange={onChange}
        placeholder={placeholder}
      />
    </div>
  );
};

Затем напишем для него CSS:

.Search {
  width: 400px;
  margin: 0 auto;
  position: relative;
  display: flex;
}

.SearchSpan {
  width: 15%;
  border: 1px solid #1C76D2;
  background: #1C76D2;
  padding-top: 4px;
  text-align: center;
  color: #fff;
  border-radius: 5px 0 0 5px;
  font-size: 20px;
}

.SearchInput {
  width: 85%;
  border: 3px solid #1C76D2;
  border-left: none;
  padding: 5px;
  border-radius: 0 5px 5px 0;
  outline: none;
}

Воспользуемся этим компонентом, выложенным на Bit. Установим его с помощью NPM:

npm install @bit/nsebhastian.react_fusejs.search-bar

Затем импортируем компонент в файл App.js. Поместим компонент <SearchBar> и остальную часть кода в один элемент <div>:

import React from "react";
import "./App.css";
import books from "./books.json";
import Card from "@bit/nsebhastian.react_fusejs.card";
import SearchBar from "@bit/nsebhastian.react_fusejs.search-bar";function App() {
  return (
    <div>
      <h1 className="Title">My Favorite books</h1>
      <SearchBar
        placeholder="Search"
        onChange={(e) => console.log(e.target.value)}
       />      <div className="Container">
        {books.map((item) => (
          <Card {...item} key={item.name} />
        ))}
      </div>
    </div>
  );
}

Добавим единый CSS в App.css для выравнивания по центру элемента <h1> с классом Title:

.Title {
  text-align: center;
}

Теперь все компоненты для этого демоприложения готовы. Остаётся только интегрировать Fuse.js в приложение на React.

Интеграция Fuse.js в приложение на React

Для начала интеграции Fuse.js в приложение на React нужно выполнить в проекте NPM-установку соответствующего пакета:

npm install fuse.js

Затем нужно импортировать библиотеку и создать новый экземпляр Fuse.js. Экземпляр принимает два задаваемых параметра:

  • data для выполнения поиска. Это может быть строковый или объектный массив.
  • Дополнительные опции для изменения поискового поведения.

Это означает, что в качестве первого аргумента нужно передать массив books. А второго  —  массив keys, содержащий title и author. Так функция поиска не будет искать строки image и url.

import Fuse from "fuse.js";

const fuse = new Fuse(books, {
  keys: ["title", "author"],
});

Далее нужно вызвать функцию fuse.search() с любым поисковым шаблоном. Поиск вернёт список совпадений, и снова в виде массива:

const matches = fuse.search("Elon Musk")

// возвращает [{ item: object }]

Подумаем теперь о поисковом поведении, подходящем для приложения. Этого должно быть достаточно:

  • когда пользователь вводит что-то в строку поиска, приложение выполняет поиск по массиву;
  • когда есть какое-то совпадение, приложение возвращает только те книги, которые соответствуют поисковому шаблону;
  • когда совпадений нет, приложение возвращает пустой экран;
  • когда пользователь очищает строку поиска, приложение показывает все данные без применения фильтра.

Для достижения такого поведения нужно поместить массив books в состояние React:

const [data, setData] = useState(books);

Когда пользователь вводит что-то в строку поиска, должна запускаться функция, которая выполнит поиск. Назовём эту функцию searchData и передадим в неё входное значение:

<SearchBar
  placeholder="Search"
  onChange={(e) => searchData(e.target.value)}
/>

Пора создать функцию. Первым делом проверим поисковый шаблон, переданный в функцию. Если он пуст, поместим состояние обратно в массив books и завершим выполнение функции с помощью оператора return:

const searchData = (pattern) => {
  if (!pattern) {
    setData(books);
    return;
  }

Затем выполним поиск по заданному шаблону:

const fuse = new Fuse(data, {
  keys: ["title", "author"],
});

const result = fuse.search(pattern);

Создадим новый массив matches для хранения любого совпадения, возвращаемого функцией Fuse. Если результат пустой, данные устанавливаются в виде пустого массива:

const matches = [];
if (!result.length) {
  setData([]);
} else {
  result.forEach(({item}) => {
    matches.push(item);
  });
  setData(matches);
}

Если result непустой, вызываем функцию forEach для итеративного обхода по result и добавляем каждый элемент в массив. Затем обновим состояние с помощью массива matches:

if (!result.length) {
  setData([]);
} else {
  result.forEach(({item}) => {
    matches.push(item);
  });
  setData(matches);
}

И вот приложение готово. Если вы пропустили какой-то этап, сравните свой код с демоверсией.

Теперь вы можете найти книгу в своём списке любимых книг. Красота!

Заключение

Каким бы ни было ваше приложение, важно помочь его пользователям найти то, что они ищут. Fuse.js  —  это простая в использовании поисковая библиотека, не требующая никакой настройки бэкенда, а её метод нечёткого поиска использует функцию поиска с защитой от ошибок.

Спасибо за внимание!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Nathan Sebhastian: Add a Simple Search Function to React App without a Server

Предыдущая статья5 основных рекурсивных задач на собеседованиях по программированию
Следующая статьяСовременное хранилище работает быстро - это API мешают делу