LoginSignup
10
7

More than 3 years have passed since last update.

Algolia InstantSearchのカスタムUIをReact+typescript+Material-UIで作成する

Posted at

はじめに

全文検索を可能にするalgoliaには、フロントエンド向けにInstantSearchという非常に便利なパッケージを提供してくれています。InstantSearchでは、SearchBoxHitsといった、検索に必要なUIを提供してくれているため、特にカスタマイズすることなく利用することは可能です。しかし、独自のデザインテーマの中でalgoliaの検索を使いたい場合、検索ボックスや検索結果リストをカスタマイズしたい場合あります。そのため、今回はあえて提供されているUIを使わずに、Material-UIのベースにInstantSearchのconnectorというHOCを使ってオリジナルの検索UIを作成します。

作成するUI

  • 検索ボックス(SearchBox)
  • 検索結果表示(Hits)

React App.gif

使用するパッケージ

  • algoliasearch
  • material-ui/core
  • react-instantsearch-dom

前提知識

方針

InstantSearchではカスタムUIを作成する方法を複数提供してくれていますが、今回はconnectorというHOCを利用します。このHOCを利用することで、作成するコンポーネントに対して、algoliaのクエリ情報や検索結果をpropsとして得ることができます。

手順

Rootコンポーネント

RootとなるコンポーネントではInstantSearchの内側に各UIのコンポーネントを配置します。これはカスタムの有無関わらず必要な手順となります。

index.tsx
import React from 'react';
import algoliasearch from 'algoliasearch';
import { InstantSearch } from 'react-instantsearch-dom';
import SearchBox from './SearchBox'; //後述
import Hits from './Hits';//後述

const searchClient = algoliasearch('algoliaのapp-id','algoliaのapi-key');

const Search: React.FC = () => {
    return (<InstantSearch searchClient={searchClient} indexName='index名'>
        <SearchBox />{/* 検索ボックス */}
        <Hits />{/* 検索結果 */}
    </InstantSearch>)
}

export default Search;

InstantSearchは最新のクエリをstateとして内包するコンポーネントに伝搬するため、必ずInstantSearchの内側に各コンポーネントを置く必要があります。

検索ボックス

connectSearchBoxというHOCを使います。これによって検索クエリの更新を行うrefineと最新の検索文字列を保持するcurrentRefinementをpropsとして得ることができます。

SearchBox.tsx
import React from 'react';
import { connectSearchBox } from 'react-instantsearch-dom';
import { SearchBoxProvided } from 'react-instantsearch-core';
import InputBase from '@material-ui/core/InputBase';
import SearchIcon from '@material-ui/icons/Search';
import { makeStyles } from '@material-ui/core/styles';

const useStyle = makeStyles({
    root: {
        backgroundColor: '#6578FF',
        padding : '0.5rem 3rem'
    },
    inputWrapper: {
        display : 'flex',
        alignItems : 'center',
        backgroundColor: '#92A1FF',
        borderRadius: '5px',
        padding: '0.5rem 1rem',
        maxWidth : '40vw',
    },
    icon: {
        opacity : '0.6',
        marginRight : '0.5rem',
    }
})

const SearchBox: React.FC<SearchBoxProvided> = ({
    refine,
    currentRefinement,
}) => {
    const classes = useStyle();
    return (
        <div className={classes.root}>
            <div className={classes.inputWrapper}>
                <SearchIcon className={classes.icon}/>
                <InputBase placeholder='Search messages' value={currentRefinement} onChange={(e) => {
                    refine(e.target.value);
                }} />
            </div>
        </div>
    )
};

export default connectSearchBox(SearchBox);

このように実装することで、検索ボックスの文字列を更新するたびに検索結果が更新されます。

検索結果

ここでは、検索結果をリストで表示するHitsと、Hits内で使用するHighlightコンポーネントを作成します。

Hits

connectHitsというHOCを使うことで、検索結果の配列(hits)をpropsとして得ることができます。

Hits.tsx
import React from 'react';
import { HitsProvided, Hit } from 'react-instantsearch-core';
import { connectHits } from 'react-instantsearch-dom';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import { makeStyles } from '@material-ui/core/styles';
import { Message } from './types';
import Highlight from './Highlight'; // 後述

const useStyle = makeStyles({
    root: {
        overflowY: 'scroll',
        maxHeight : '90vh'
    },
    listItem: {
        borderBottom : '1px solid rgba(0,0,0,0.1)'
    }
})

const Hits: React.FC<HitsProvided<Hit<Message>>> = ({
    hits,
}) => {
    const classes = useStyle();
    return (
        <List className={classes.root}>
            {hits.map(hit => (
                <ListItem key={hit.objectID} button className={classes.listItem}>
                    <ListItemText secondary={<Highlight hit={hit} attribute='senderName'/>}>
                        <Highlight hit={hit} attribute='message'/>
                    </ListItemText>
                </ListItem>
            ))}
        </List>
    )
};

export default connectHits(Hits);

hitsの型はHitsProvided<Hit<アイテムの型>>となります。Hit<アイテムの型>には、アイテムの型に加えて、検索にヒットした際の情報が保存されています。これはハイライトなどに必須なので、typescriptを使う場合は忘れないようにしてください。

Highlight

connectHighlightというHOCを使うことで、highlight関数をpropsとして得ることができます。この関数はhitオブジェクトを引数に取り、マッチした(ハイライトする)部分とそうでない部分に結果を分解してくれます。また、attributeにはハイライトの対象としたいデータのフィールド名を指定します。

Highlight.tsx
import React from 'react';
import { HighlightProps } from 'react-instantsearch-core';
import { connectHighlight } from 'react-instantsearch-dom';
import { Message } from './types';

const HighlightHit: React.FC<HighlightProps<Message>> = ({
    hit,
    highlight,
    attribute
}) => {
    const parsedHit = highlight({
        highlightProperty: '_highlightResult',
        attribute,
        hit,
    });
    return (
        <React.Fragment>
            {parsedHit.map(
                (part, index) =>
                    part.isHighlighted ? (
                        <mark key={index}>{part.value}</mark>
                    ) : (
                            <span key={index}>{part.value}</span>
                        )
            )}
        </React.Fragment>
    )
};

export default connectHighlight(HighlightHit);

(参考) InfiniteHits

検索結果を一部ずつロードしたい場合、connectHitsの代わりにconnectInfiniteHitsというHOCを使うことができます。この場合propsはInfiniteHitsProvided<Hit<アイテム>>となります。

InfiniteHit.tsx
import React from 'react';
import { InfiniteHitsProvided, Hit } from 'react-instantsearch-core';
import { connectInfiniteHits } from 'react-instantsearch-dom';

・・・

const Hits: React.FC<InfiniteHitsProvided<Hit<Message>>> = ({
    hits,
    hasMore, // まだ読み込んでいないデータが存在する場合true
    refineNext // 次のデータ群を読み込む
}) => {
    const classes = useStyle();
    return (
        <List className={classes.root}>
            {hits.map(hit => (
                <ListItem key={hit.objectID} button className={classes.listItem}>
                    <ListItemText secondary={<Highlight hit={hit} attribute='senderName'/>}>
                        <Highlight hit={hit} attribute='message'/>
                    </ListItemText>
                </ListItem>
            ))}
            {hasMore && <Button className={classes.button} onClick={refineNext}>...More</Button>}
        </List>
    )
};

export default connectInfiniteHits(Hits);

カスタム可能なUI

他にも利用できるconnector公式ページもしくは、公式のGithubにて確認することができます。また、オリジナルのconnectorを作成することも可能で、その手順も公開されています。→Create Your Own Widgets

最後に

最後まで見ていただいありがとうございます。今回作成したコードはGithubに置いています。

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7