Функция поиска — одна из самых важных особенностей программного приложения. Поисковые сайты типа 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»
Компонент 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 — это простая в использовании поисковая библиотека, не требующая никакой настройки бэкенда, а её метод нечёткого поиска использует функцию поиска с защитой от ошибок.
Спасибо за внимание!
Читайте также:
- Продвинутые React Hooks: подробный разбор useEffect
- Как создавать собственные хуки на React
- React Colorful: минималистичная и быстрая альтернатива для React Color
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Nathan Sebhastian: Add a Simple Search Function to React App without a Server