ツクログネット

【Gatsby + Contentfulブログ】記事の検索結果をリアルタイムで一覧表示する

eye catch

記事一覧ページを作成する

記事一覧ページはGatsby+Contentfulブログの記事一覧をもっと見るボタンで進めるで作成したものと同じものを使用します。

検索フォームを表示する

検索フォームを表示します。

まず、InputTextコンポーネントを作成します。

InputText.js
import React from 'react'
import styled from 'styled-components'

const StyledInputText = styled.input`
		border: 1px solid var(--text-color);
		border-radius: .25em;
		padding: 1em;
`

const InputText = ({ className, ...props }) => {
	return (
		<StyledInputText 
			className={className}
			type="text"
			{...props}
		/>
	);
}

export default InputText

記事の検索結果はリアルタイムに一覧表示するため、検索フォームはinput(type="text")要素のみ使用します。

次に、作成したInputTextコンポーネントをページに表示します。今回は記事一覧の上に表示します。

blog_page.js
import React from 'react'
import Layout from "../components/layout"
import ArticleSummaries from '../components/ArticleSummaries'
import AutoPager from '../components/AutoPager'
import InputText from '../components/InputText'

import useAutoPager from '../hooks/useAutoPager'

const BlogPage = (props) => {
  const autoPager = useAutoPager(props.pageContext.articleSummaries, 4)

  return (
    <Layout>
      <InputText
				className="search"
				type="text"
				//onChange={handleChange}
				//onFocus={handleFocus}
				//onBlur={handleBlur}
				//value={textValue}
			  placeholder="記事を検索"
			/>
      <ArticleSummaries 
		      className="article-summaries"
				  articleSummaries={autoPager.articleSummaries}
			  />
        <AutoPager className="more-link" handleClick={autoPager.seeMore} limit={autoPager.limit}/>
    </Layout>
  );
};

export default BlogPage

これで検索フォームが表示されました。現時点ではまだ文字を入力しても何も起こりません。

フォームの入力と同時に検索結果を一覧表示する

フォームの入力値と一致する記事を一覧表示する 検索フォームに文字を入力すると同時に検索結果が一覧表示される機能を実装します。

はじめに、フォームに入力した文字がステートに保存されるようにします。

blog_page.js
// 省略

const BlogPage = (props) => {
  // 省略

  const [textValue, setTextValue] = useState("")

	const handleChange = useCallback(e => {
		setTextValue(e.target.value)
	}, [])

  return (
    <Layout>
      <InputText
				className="search"
				type="text"
				onChange={handleChange}
				value={textValue}
			  placeholder="記事を検索"
			/>
      <!-- 省略 -->
    </Layout>
  );
};

export default BlogPage

handleChange関数をInputTextコンポーネントのonChange属性に、textValueステートをvalue属性に設定することで、フォームに文字を入力するとhandleChange関数が実行されて、入力した文字がtextValueステートに保存されます。

次に、入力した文字と記事タイトルの一部が一致する記事のみが一覧表示されるようにします。

まず、検索後の記事概要データが入る配列をステートfilteredArticleSummariesとして定義します。初期値はprops.pageContext.articleSummariesです。

blog_page.js
// 省略

const BlogPage = (props) => {
  // 省略

  const [
		filteredArticleSummaries,
		setFilteredArticleSummaries
	] = useState(props.pageContext.articleSummaries)

  return (
    // 省略
  );
};

export default BlogPage

次に、フォームに文字が入力されたときに、入力した文字と記事タイトルの一部が一致する記事のデータのみを持った新たな配列を生成し、filteredArticleSummariesステートに上書きします。

blog_page.js
// 省略

const BlogPage = (props) => {
  // 省略

  useEffect(() => {
		const f = textValue ? props.pageContext.articleSummaries.filter(a => 
			textValue.trim().length > 0 &&
			a.title.toLowerCase().includes(textValue.toLowerCase().trim())
		) : props.pageContext.articleSummaries

		setFilteredArticleSummaries(f)
	}, [textValue, props.pageContext.articleSummaries])

  return (
    // 省略
  );
};

export default BlogPage

フォームに文字を入力するとuseEffectフックの第二引数の配列のtextValueステートが変更されるため、第一引数の関数が実行されます。

この関数で、props.pageContext.articleSummaries配列のfilterメソッドを呼び出し、入力した文字中身の要素であるオブジェクトのtitleプロパティの値(以下記事タイトル)と入力した文字textValueをincludesメソッドで比較します。記事タイトルのどこかに入力した文字が含まれている場合、その記事タイトルを持つオブジェクトのみを持った新しい配列を生成し、それをfilteredArticleSummariesステートに上書きします。

尚、入力した文字の語頭と語尾の空白は入力値とは認めず比較の対象としないように、予めtrimメソッドで両端の空白を取り除きます。

また、大文字と小文字は区別しないように、記事タイトルと入力した文字をtoLowerCaseメソッドで小文字に変換した状態で比較を行います。

filteredArticleSummariesは、useAutoPagerフックを通したあとにArticleSummariesコンポーネントに渡します。

blog_page.js
// 省略

const BlogPage = (props) => {
  // 省略

  const filteredArticleSummariesPager = useAutoPager(filteredArticleSummaries, 4)

  return (
    <Layout>
      <!-- 省略 -->   
						<>
							<ArticleSummaries
								className="article-summaries"
								articleSummaries={filteredArticleSummariesPager.articleSummaries}
							/>
							<AutoPager
								className="more-link"
								limit={filteredArticleSummariesPager.limit}
								handleClick={filteredArticleSummariesPager.seeMore}
							/>
						</>
    </Layout>
  );
};

export default BlogPage

これでフォームの入力と同時に検索結果が一覧表示されるようになりました。

記事一覧を通常時と検索時で分けて表示する

今のままでは、一つのdisplayedCountで配列をsliceしているため、記事の検索前にもっと見るボタンを押して記事一覧を追加表示すると、同時に検索結果のほうの記事一覧も、もっと見るボタンを押していないにもかかわらず追加表示された状態で表示されてしまいます。

この問題を解決するために、記事一覧を通常時と検索時で共有せずに個別に用意して表示します。

blog_page.js
// 省略

const BlogPage = (props) => {
  // 省略

  const articleSummariesPager = useAutoPager(props.pageContext.articleSummaries, 4)
  const filteredArticleSummariesPager = useAutoPager(filteredArticleSummaries, 4)

  return (
    <Layout>
      <!-- 省略 -->   
      {
					textValue.trim().length > 0 ?
						<>
							<ArticleSummaries
								className="article-summaries"
								articleSummaries={filteredArticleSummariesPager.articleSummaries}
							/>
							<AutoPager
								className="more-link"
								limit={filteredArticleSummariesPager.limit}
								handleClick={filteredArticleSummariesPager.seeMore}
							/>
						</>
          : 
						<>
							<ArticleSummaries 
					      className="article-summaries"
					      articleSummaries={articleSummariesPager.articleSummaries}
					    />
					    <AutoPager
								className="more-link"
								limit={articleSummariesPager.limit}
								handleClick={articleSummariesPager.seeMore}
							/>
						</>
       }
    </Layout>
  );
};

export default BlogPage

useAutoPagerフックを通常時の記事一覧と検索時の記事一覧用に2回呼び出しています。

また、ArticleSummariesコンポーネントとAutoPagerコンポーネントを、通常時と検索時で2つ用意します。

textValue.trim().length > 0の条件、つまりフォームに文字が入力されていれば検索時、そうでなければ通常時ということです。

以上でこの問題は解決しました。