React + TypeScript + vte.cxで簡単なWebアプリを作ってみた③ 条件検索機能編

# はじめに

今まで作ったアプリケーションに今回は登録されているデータを条件によって検索できる機能を実装していきました。

# 今回作ったアプリケーション
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/660329/562c4dc5-cce1-ad6b-5a3f-3671023e2752.png)

一つの画面の中に条件フォームを作り、絞りこみボタンを押すとその条件に合致した情報を画面上に表示する機能です。

例えば以下の画像では「好きな住居形態」という項目で「森」を選んだ結果、合致するゴリラとhatakeyamaが画面に表示されました。

![スクリーンショット 2020-09-23 9.40.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/660329/76a3eb82-f531-0aed-abd1-d3cc3a10772d.png)

前回のページネーションがちゃんと作動しているのでページネーション番号もちゃんと表示されています。
また絞る条件は一項目ではなく複数項目選ぶことができます。

# 今回使用するコンポーネント

条件検索のロジックを持ったUserInfoFilterコンポーネントと条件検索で帰ってきた値を格納するUserInfoコンポーネントを今回使っていきます。

# UserInfoFilter.tsx(条件検索するコンポーネント)


```UserInfoFilter.tsx
import * as React from 'react'
import { useContext } from 'react'
import { Store } from './App'
import axios from 'axios'
import Form from './Form'
import { displayPage } from './UserInfo'

const UserInfoFilter = (props: any) => {
    const { dispatch } = useContext(Store)

    //Formコンポーネントが持っているusers情報をコールバック関数で受け取っている
    const searchData = async (name: string, gender: string, age: number, address: string, password: string, email: string, postNumber: string, likeResidenceType: string, position: string, language: string) => {
        const nameParameter = name ? 'users.name=' + name + '&' : ''
        const genderParameter = gender ? 'users.gender=' + gender + '&' : ''
        const ageParameter = age ? 'users.age=' + age + '&' : ''
        const addressParameter = address ? 'users.address=' + address + '&' : ''
        const passwordParameter = password ? 'users.password=' + password + '&' : ''
        const emailParameter = email ? 'users.email=' + email + '&' : ''
        const postNumberParameter = postNumber ? 'users.post_number=' + postNumber + '&' : ''
        const likeResidenceTypeParameter = likeResidenceType ? 'users.like_residence_type=' + likeResidenceType + '&' : ''
        const positionParameter = position ? 'users.position=' + position + '&' : ''
        const languageParameter = language ? 'users.language=' + language + '&' : ''

        // ajaxパラメータの中身
        const searchParams = nameParameter + genderParameter + ageParameter + addressParameter + passwordParameter + emailParameter + postNumberParameter + likeResidenceTypeParameter + positionParameter + languageParameter

        // 親に渡すparams
        const passParams = {
            name,
            gender,
            age,
            address,
            password,
            email,
            postNumber,
            likeResidenceType,
            position,
            language
        }
        try {
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            // pageindexの作成
            await axios.get(
                '/d/users?' + searchParams + '_pagination=1,4&l=' + displayPage
            )
                .then(async (res) => {
                    //条件検索時の総取得件数
                    console.log(res.data.feed.subtitle)
                    props.click(passParams, res.data.feed.subtitle)
                })
                .then(() => {
                    dispatch({ type: 'HIDE_INDICATOR' })
                })
        } catch (e) {
            alert('error:' + e)
            dispatch({ type: 'HIDE_INDICATOR' })
        }
    }

    return (
        <>
            <h3>絞り込み検索</h3>
            <Form click={searchData} submitName={'絞り込む'} />
        </>
    )
}

export default UserInfoFilter

```

このコンポーネントの流れは

```tsx
return (
        <>
            <h3>絞り込み検索</h3>
            <Form click={searchData} submitName={'絞り込む'} />
        </>
    )
```

フォームコンポーネントにpropsでsearchDataメソッドとFormコンポーネントの中のボタンに絞りこみという名前を渡しています。

ちなみにこの部分です。

![スクリーンショット 2020-09-23 10.29.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/660329/bd06426e-0fd0-8c6b-1b13-dc2ed6ba1716.png)

この絞りこみボタンを押すと、入力されたデータを親コンポーネント(UserInfo.tsx)に渡します。

入力された後にsearchDataメソッドが発火します。

```UserInfoFilter.tsx
const searchData = async (name: string, gender: string, age: number, address: string, password: string, email: string, postNumber: string, likeResidenceType: string, position: string, language: string) => {
        const nameParameter = name ? 'users.name=' + name + '&' : ''
        const genderParameter = gender ? 'users.gender=' + gender + '&' : ''
        const ageParameter = age ? 'users.age=' + age + '&' : ''
        const addressParameter = address ? 'users.address=' + address + '&' : ''
        const passwordParameter = password ? 'users.password=' + password + '&' : ''
        const emailParameter = email ? 'users.email=' + email + '&' : ''
        const postNumberParameter = postNumber ? 'users.post_number=' + postNumber + '&' : ''
        const likeResidenceTypeParameter = likeResidenceType ? 'users.like_residence_type=' + likeResidenceType + '&' : ''
        const positionParameter = position ? 'users.position=' + position + '&' : ''
        const languageParameter = language ? 'users.language=' + language + '&' : ''

        // ajaxパラメータの中身
        const searchParams = nameParameter + genderParameter + ageParameter + addressParameter + passwordParameter + emailParameter + postNumberParameter + likeResidenceTypeParameter + positionParameter + languageParameter

        // 親に渡すparams
        const passParams = {
            name,
            gender,
            age,
            address,
            password,
            email,
            postNumber,
            likeResidenceType,
            position,
            language
        }
        try {
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            // pageindexの作成
            await axios.get(
                '/d/users?' + searchParams + '_pagination=1,4&l=' + displayPage
            )
                .then(async (res) => {
                    //条件検索時の総取得件数
                    console.log(res.data.feed.subtitle)
                    props.click(passParams, res.data.feed.subtitle)
                })
                .then(() => {
                    dispatch({ type: 'HIDE_INDICATOR' })
                })
        } catch (e) {
            alert('error:' + e)
            dispatch({ type: 'HIDE_INDICATOR' })
        }
    }
```

```tsx
const nameParameter = name ? 'users.name=' + name + '&' : ''
```

三項演算子でFormから渡ってきたデータ項目があればページインデックスを作成するための加工をします。なければ空文字を返します。

条件検索を使ったインデックスを作成するためにはajax通信で以下のパラメーターを使います。

```
'/d/{エンドポイント名}?条件&_pagination={開始ページ、終了ページ}&l={1ページの表示件数}'
```

```tsx
// pageindexの作成
            await axios.get(
                '/d/users?' + searchParams + '_pagination=1,4&l=' + displayPage
            )
                .then(async (res) => {
                    //条件検索時の総取得件数
                    props.click(passParams, res.data.feed.subtitle)
                })
```

この部分ですね。
また、

```UserInfoFilter.tsx
props.click(passParams,res.data.feed.subtitle)
```

の部分で親に項目のデータと総件数(res.data.feed.subtitleで総件数が帰ってきます。)を渡しています。

# UserInfo.tsx(UserInfoFilterで貼ったインデックスをもとにデータをとってくるコンポーネント)


```UserInfo.tsx
import * as React from 'react'
import { useState, useEffect, useContext, useRef } from 'react'
import axios from 'axios'
import UserList from './UserList'
import { Store } from './App'
import Pagination from './Pagination'
import UserInfoFilter from './UserInfoFilter'

interface SearchConditions {
    name?: string
    gender?: '男' | '女' | ''
    age?: number
    address?: string
    password?: string
    email?: string
    postNumber?: string
    likeResidenceType?: string
    position?: string
    language?: string
}

// 1ページに表示させる件数
export const displayPage = 5

const UserInfo = () => {
    // apiを叩いてgetしたデータをusersに格納する
    const [users, setUsers] = useState([])
    // 検索条件(UserInfoFilterから渡ってくる)
    const [searchConditions, setSearchConditions] = useState<SearchConditions>()
    // 総ページ数
    const [sumPageNumber, setSumPageNumber] = useState(0)

    // 現在見ているページネーション
    const [currentPage, setCurrentPage] = useState(1)

    const { dispatch } = useContext(Store)

    // コンポーネントマウント後に以下のページインデックスを作成する関数が実行される
    // 初期描画の実行
    useEffect(() => {
        getTotalFeedNumber()
        console.log('useEffect:getTotalFeedNumber')
    }, [])

    // 初期描画後
    // 最初にページインデックスを作成終了後、handlePaginateで1ページを指定している
    const mounted = useRef(false)
    useEffect(() => {
        if (mounted.current) {
            if (sumPageNumber === 0) {
                setUsers([])
                return
            }
            handlePaginate(1)
            console.log('useEffect:mounted.current=true')
        } else {
            mounted.current = true
            console.log('useEffect:mounted.current=false')
        }
    }, [sumPageNumber])

    //ページの取得処理
    let retryCount = 0
    // この処理をgetTotalFeedNumberを処理したときに実行したい

    //page番号を使ってAPIを叩く処理
    const handlePaginate = async (page: number) => {
        const nameParameter = searchConditions?.name ? 'users.name=' + searchConditions.name + '&' : ''
        const genderParameter = searchConditions?.gender ? 'users.gender=' + searchConditions.gender + '&' : ''
        const ageParameter = searchConditions?.age ? 'users.age=' + searchConditions.age + '&' : ''
        const addressParameter = searchConditions?.address ? 'users.address=' + searchConditions.address + '&' : ''
        const passwordParameter = searchConditions?.password ? 'users.password=' + searchConditions.password + '&' : ''
        const emailParameter = searchConditions?.email ? 'users.email=' + searchConditions.email + '&' : ''
        const postNumberParameter = searchConditions?.postNumber ? 'users.post_number=' + searchConditions.postNumber + '&' : ''
        const likeResidenceTypeParameter = searchConditions?.likeResidenceType ? 'users.like_residence_type=' + searchConditions.likeResidenceType + '&' : ''
        const positionParameter = searchConditions?.position ? 'users.position=' + searchConditions.position + '&' : ''
        const languageParameter = searchConditions?.language ? 'users.language=' + searchConditions.language + '&' : ''

        // リトライ回数
        const LIMIT_RETRY_COUNT = 10

        const searchParams = nameParameter + genderParameter + ageParameter + addressParameter + passwordParameter + emailParameter + postNumberParameter + likeResidenceTypeParameter + positionParameter + languageParameter

        try {
            console.log('handlePaginateが作動しました' + page)
            console.log(searchParams)
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            if (searchConditions) {
                await axios.get(
                    '/d/users?' + searchParams + 'n=' + page + '&l=' + displayPage
                ).then((res) => {
                    if (res && res.data && res.data.length) {
                        setUsers(res.data)
                    }
                    setCurrentPage(page)
                }).then(() => {
                    retryCount = 0
                    dispatch({ type: 'HIDE_INDICATOR' })
                })
            } else {
                await axios.get(`/d/users?n=${page}&l=${displayPage}`).then((res) => {
                    if (res && res.data && res.data.length) {
                        setUsers(res.data)
                    }
                    setCurrentPage(page)
                }).then(() => {
                    retryCount = 0
                    dispatch({ type: 'HIDE_INDICATOR' })
                })
            }

        } catch (e) {
            if (e.response.data.feed.title === 'This process is still in progress. Please wait.') {
                retryCount++
                console.log(retryCount)
                if (retryCount < LIMIT_RETRY_COUNT) {
                    handlePaginate(page)
                } else {
                    dispatch({ type: 'HIDE_INDICATOR' })
                    alert('error:' + e)
                    alert('Process error')
                }
            }

            if (e.response.data.feed.title === 'Please make a pagination index in advance.') {
                retryCount++
                console.log(retryCount)
                if (retryCount < LIMIT_RETRY_COUNT) {
                    handlePaginate(page)
                } else {
                    dispatch({ type: 'HIDE_INDICATOR' })
                    alert('error:' + e)
                    alert('Not create pagination index')
                }
            }
        }
    }

    // paginationIndexを作成する処理
    const getTotalFeedNumber = async () => {
        try {
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'

            await axios.get(`/d/users?_pagination=1,4&l=${displayPage}`).then((res) => {
                setSumPageNumber(res.data.feed.subtitle)
                console.log(res)
            }).then(() => {
                dispatch({ type: 'HIDE_INDICATOR' })
            })
        } catch (e) {
            dispatch({ type: 'HIDE_INDICATOR' })
            alert('error:' + e)
        }
    }

    const searchedPaginate = async (passedParams: SearchConditions, passedSumPageNumber: number) => {
        //子から渡される検索条件
        await setSearchConditions(passedParams)
        //子から渡される条件検索後のページ総件数
        await setSumPageNumber(passedSumPageNumber)
    }

    return (
        <>
            <UserInfoFilter click={searchedPaginate} />
            <h3>情報一覧</h3>
            <p>総件数:<span style={{ color: 'blue' }}>{sumPageNumber}</span>件</p>
            <p>現在<span style={{ color: 'blue' }}>{currentPage}</span>ページ目</p>
            <UserList info={users} />
            <Pagination sum={sumPageNumber} per={displayPage} onChange={e => handlePaginate(e.page)} />
            <button onClick={(e) => { e.preventDefault(), console.log(sumPageNumber) }}>sumPageNumberを調べる</button>
        </>
    )
}

export default UserInfo
```

UserInfoコンポーネントの子コンポーネントであるUserInfoFilter.tsxから渡される情報を引数として受け取り、```setSearchConditions```で値を格納します。

```UserInfo.tsx
const searchedPaginate = async (passedParams: SearchConditions, passedSumPageNumber: number) => {
        //子から渡される検索条件
        await setSearchConditions(passedParams)
        //子から渡される条件検索後のページ総件数
        await setSumPageNumber(passedSumPageNumber)
    }
```

```UserInfo.tsx
// 検索条件(UserInfoFilterから渡ってくる)
    const [searchConditions, setSearchConditions] = useState<SearchConditions>(
```

このsearchConditionsというものがなんのために必要なのかというと、ページネーションをする際に必要になってきます。

また,setSumPageNumberすることによってSumPageNumberの値が変わり、SumPageNumberを監視しているuseEffectによってPaginateメソッドが作動します。


handlePaginateメソッドの中で

```UserInfo.tsx
if (searchConditions) {
    await axios.get(
        '/d/users?' + searchParams + 'n=' + page + '&l=' + displayPage)
} else {
    await axios.get(`/d/users?n=${page}&l=${displayPage}`)
}
```

という部分があり、ページネーションをする際に毎回searchConditionsに値があれば条件検索をされたページネーションする流れになっています。
なければ通常通りの条件なしでのページネーションします。

# まとめ
①UserInfoFilterでインデックスを貼る
②検索条件があればUserInfoIndexで貼ったインデックスを参照してページネーションする
といった流れで条件検索をすることができました。