スマレジエンジニアyushiのブログ

スマレジエンジニアのブログ

【自作ブログ #11】検索機能の作成

自作ブログを作っています。

yushi-dev.hatenablog.com

技術スタック

  • TypeScript
  • React
  • Next.js
  • Styled Components
  • GitHub Pages

今回は検索機能を作ります。

今回作るもの

今回は下記の2つを実装します。

  • サイドメニューの検索フォーム
  • 検索画面

原則全画面で表示されているサイドメニューから検索ができます。

検索を実行すると、検索画面に遷移します。

検索画面では条件を変えて繰り返し検索ができます。

検索フォーム

検索フォームのコンポーネントを作成します。

input searchを含むformにデザイン適用します。

import { ChangeEventHandler, FormEventHandler } from 'react'
import styled from 'styled-components'

const Form = styled.form`
  display: flex;
  justify-content: space-between;
`

const SearchInput = styled.input`
  margin-right: 10px;
  padding: 7px;
  border: 1px solid #444;
  border-radius: 5px;
  font-size: 1.2rem;
`

type Props = {
  value: string
  onChange: ChangeEventHandler<HTMLInputElement>
  onSubmit: FormEventHandler<HTMLFormElement>
}

const SearchPostForm = ({ value, onChange, onSubmit }: Props) => {
  return (
    <Form onSubmit={onSubmit}>
      <SearchInput type="search" value={value} onChange={onChange} />
    </Form>
  )
}

export default SearchPostForm

続いて、このコンポーネントに対応したHookを作ります。
検索文字列の保持・検索文字列の変更イベントの他、submit時の検索画面への遷移も含んでいます。
遷移の際に、クエリストリング q として検索文字列を渡します。

import { useRouter } from 'next/router'
import { ChangeEventHandler, FormEventHandler, useState } from 'react'

const useSearchPostForm = () => {
  const router = useRouter()

  const [searchText, setSearchText] = useState('')

  const onChangeSearchText: ChangeEventHandler<HTMLInputElement> = (e) => {
    setSearchText(e.target.value)
  }

  const onSubmitSearchText: FormEventHandler<HTMLFormElement> = (e) => {
    e.preventDefault()
    router.push({
      pathname: '/posts/search',
      query: {
        q: searchText,
      },
    })
  }

  return {
    searchText,
    setSearchText,
    onChangeSearchText,
    onSubmitSearchText,
  }
}

export default useSearchPostForm

検索画面

検索を実行する画面を作成します。

具体的には、検索文字列によるタイトルの絞り込みをします。

検索のためのタイトル一覧と、ページ遷移のためのslug一覧を取得しておきます。

export async function getStaticProps() {
  return {
    props: {
      matters: await postService.getMatters(),
      slugs: await postService.getSlugs(),
    },
  }
}

next/routerを用いて検索文字列をクエリストリングより取り出します。

  const q: string = (() => {
    if (typeof router.query.q === 'undefined') {
      return ''
    }

    if (Array.isArray(router.query.q)) {
      return router.query.q.join('')
    }

    return router.query.q
  })()

先ほど作成したHookの中身を取り出します。

  const { searchText, setSearchText, onChangeSearchText, onSubmitSearchText } =
    useSearchPostForm()

クエリストリングの指定があれば、input search に適用します。

  // 初回定義時には、qは空文字列になるため、setTextで更新する必要がある.
  // 更新による無限ループを避けるため、useEffectで囲う.
  useEffect(() => {
    setSearchText(q)
  }, [setSearchText, q])

検索(絞り込み)を行います。

  const searchedList = matters
    .filter(({ title }) => title.includes(searchText))
    .sort((a, b) => (a.date < b.date ? -1 : 1))
    .map(({ title, slug }) => (
      <li key={slug}>
        <Link href={'/posts/' + slug}>
          <a>{title}</a>
        </Link>
      </li>
    ))

最後にこれらを画面表示します。

  return (
    <MainContainer matters={matters}>
      <section>
        <H1>検索</H1>
        <Content>
          <StyledSearchPostForm
            value={searchText}
            onChange={onChangeSearchText}
            onSubmit={onSubmitSearchText}
          />
          <ul>{searchedList}</ul>
        </Content>
      </section>
    </MainContainer>
  )

これにて検索機能が完成しました。

Pull Request

https://github.com/nek0meshi/blog/pull/13

あとがき

今後は本文検索も組み込みたいですね。