#はじめに
全文検索を可能にするalgoliaには、フロントエンド向けにInstantSearchという非常に便利なパッケージを提供してくれています。InstantSearchでは、SearchBox
やHits
といった、検索に必要なUIを提供してくれているため、特にカスタマイズすることなく利用することは可能です。しかし、独自のデザインテーマの中でalgoliaの検索を使いたい場合、検索ボックスや検索結果リストをカスタマイズしたい場合あります。そのため、今回はあえて提供されているUIを使わずに、Material-UIのベースにInstantSearchのconnector
というHOCを使ってオリジナルの検索UIを作成します。
#作成するUI
- 検索ボックス(SearchBox)
- 検索結果表示(Hits)
#使用するパッケージ
- algoliasearch
- material-ui/core
- react-instantsearch-dom
#前提知識
- ract→Higher-Order-Components(HOC)を使います
- typescript
- algolia
- material-ui
#方針
InstantSearchではカスタムUIを作成する方法を複数提供してくれていますが、今回はconnectorというHOCを利用します。このHOCを利用することで、作成するコンポーネントに対して、algoliaのクエリ情報や検索結果をpropsとして得ることができます。
#手順
##Rootコンポーネント
RootとなるコンポーネントではInstantSearch
の内側に各UIのコンポーネントを配置します。これはカスタムの有無関わらず必要な手順となります。
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として得ることができます。
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として得ることができます。
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
にはハイライトの対象としたいデータのフィールド名を指定します。
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<アイテム>>
となります。
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に置いています。