検索画面の実装【初学者のReact✗Railsアプリ開発 第12回】

#やったこと
* 投稿の検索画面を作成し、検索結果を表示できるようにした。
* reduxを利用して検索結果を管理。（ページを移動して戻ってくるときに、前の検索結果を表示させるようにしたかった）
* ページネーションの実装にkaminariを使っている
* フォームの実装はredux-form

#成果物
![8gqb6-4o8vr.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/367226/02efa3ae-3f51-9f30-6d64-c01cbe752891.gif)

#実装手順（Rails API）
###posts_controller
* 検索を行うコアとなる処理の記述を行っています。
* content LIKE?の使い方を初めて学びました。
* %をつけると、あいまい検索になる。無いと、完全一致。検索ワードはクエリでもらってる。
* ページネーションを使っているので、page_length（何ページまであるか）も返しています。
* 
```ruby:posts_controller
      def search
        posts = Post.where("content LIKE ?", "%#{params[:q]}%").page(params[:page] ||= 1).per(10).order('created_at DESC')
        page_length = Post.where("content LIKE ?", "%#{params[:q]}%").page(params[:page] ||= 1).per(10).total_pages
        json_data = {
          posts: posts,
          page_length: page_length,
        }
        render json: { status: 'SUCCESS', message: 'Loaded the posts', data: json_data}

      end
```

###route.rb（ルートの編集）
* 追加します。

```ruby:route.rb
 get 'search', to: 'posts#search'
```

#実装手順（React）
###Search.js(render)
* 大きく分けて、3つの部品（検索フォーム、検索結果表示部分、ページネーション部分）に分けてレンダリングしている。
* 検索結果表示部分をthis.renderResults()で、初回訪問かどうか(doneFetch)、結果が存在しているかどうか（noResults)で制御している。


```javascript:Search.js

class SearchPage extends React.Component {
  render() {
    const { SearchResultsReducer } = this.props;

    const { classes } = this.props;

    return (
      <div>
        <h3>テーマを検索する</h3>
        <SearchForm onSubmit={this.searchPost} />

        {this.renderResults(SearchResultsReducer.noResults, SearchResultsReducer.doneFetch)}
        <MuiThemeProvider theme={pagitheme}>
          <CssBaseline />
          <Pagination
            limit={10}
            offset={SearchResultsReducer.offset}
            total={SearchResultsReducer.page_length * 10}
            onClick={(e, offset) => this.handlePaginationClick(offset)}
          />
        </MuiThemeProvider>
      </div>
    )

  }
}

```

###Search.js(function)
* doneFetchの取り扱いが頭を使いました。結局、reduxで管理するのが良いと思います。ページ遷移するだけではreduxのstateは変更されないから。
* 表示の制御は少し頭を使いました。

```javascript:Search.js

class SearchPage extends React.Component {

  constructor(props) {
    super(props);
    this.searchPost = this.searchPost.bind(this);

  }

  componentDidMount() {
    const { form } = this.props;
    const { SearchResultsReducer } = this.props;

    this.props.actions.getSearchResults(SearchResultsReducer.searchWord, SearchResultsReducer.offset, SearchResultsReducer.doneFetch);
  }
  searchPost = values => {
    const { form } = this.props;
    this.props.actions.getSearchResults(form.SearchForm.values.notes, 0, true);
  }

  handlePaginationClick(offset) {
    const { form } = this.props;
    this.props.actions.getSearchResults(form.SearchForm.values.notes, offset, true);
  }

  renderResults(noResults, doneFetch) {
    const { SearchResultsReducer } = this.props;
    const { classes } = this.props;

    if (!noResults && doneFetch) {
      return (
        <ul className={classes.ul}>
          {SearchResultsReducer.items.map((post) => (
            <Link className={classes.link} to={"/posts/" + post.id}>
              <li className={classes.li} key={post.id}>
                <div className={classes.licontent}>
                  <h3 className={classes.lih3}>{post.content}</h3>
                </div>
              </li>
            </Link>
          ))}
        </ul>
      )
    } else if (!doneFetch) {
      return (
        <h3>検索ワードを入力してください</h3>
      )

    } else {
      return (
        <h3>検索結果はありません。</h3>
      )
    }
  }


  }
}

```

###SearchResultsReducer.js
* どのタイミングでdoneFetchとnoResultsの状態を変更するかでレンダリング結果が変わってきます。そこに頭を使いました。

```javascript:SearchResultsReducer.js

const initialState = {
  isFetching: false,
  items: [],
  offset: "",
  page_length: "",
  noResults: false,
  searchWord: "",
  doneFetch: false,
};

const SearchResultsReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'GET_SEARCHRESULTS_REQUEST':
      return {
        ...state,
        isFetching: true,

      };
    case 'GET_SEARCHRESULTS_SUCCESS':
      if (action.items.length === 0) {
        return {
          ...state,
          isFetching: false,
          items: action.items,
          offset: action.offset,
          page_length: action.page_length,
          noResults: true,
          searchWord: action.searchWord,
          doneFetch: action.doneFetch,
          searchWord: action.searchWord,
        };
      } else {
        return {
          ...state,
          isFetching: false,
          items: action.items,
          offset: action.offset,
          page_length: action.page_length,
          noResults: false,
          doneFetch: action.doneFetch,
          searchWord: action.searchWord,

        };
      }

    case 'GET_SEARCHRESULTS_FAILURE':
      return {
        ...state,
        isFetching: false,
        error: action.error,
        searchWord: action.searchWord,
        doneFetch: action.doneFetch,

      };
    default:
      return state;
  }
};

export default SearchResultsReducer;
```

###actions/index.js
* 前に記述したときと大まかには変わりませんが、doneFetchとかsearchWordといった引数の数が増えているので、そこが注意ですかね。[ページネーションと並び替えに対応した投稿一覧画面とAPIの実装【初学者のReact×Railsアプリ開発 第10回】](https://qiita.com/dl10yr/items/66bb28307dcb61d2b4ab#actionsindexjs)

```javascript:index.js
export const getSearchResults = (keyword, offset, doneFetch) => {
  return (dispatch) => {
    dispatch(getSearchResultsRequest())
    const auth_token = localStorage.auth_token
    const client_id = localStorage.client_id
    const uid = localStorage.uid
    const page_url = offset / 10 + 1

    return axios.get(process.env.REACT_APP_API_URL + `/api/v1/search?q=${keyword}&page=${page_url}`, {
      headers: {
        'access-token': auth_token,
        'client': client_id,
        'uid': uid
      }
    })
      .then(response => dispatch(getSearchResultsSuccess(response.data.data.posts, keyword, offset, response.data.data.page_length, doneFetch)))
      .catch(error => dispatch(getSearchResultsFailure(error, keyword, doneFetch)))
  };
};

export const getSearchResultsRequest = () => ({
  type: 'GET_SEARCHRESULTS_REQUEST',
})


export const getSearchResultsSuccess = (json, keyword, offset, page_length, doneFetch) => ({
  type: 'GET_SEARCHRESULTS_SUCCESS',
  items: json,
  offset: offset,
  page_length: page_length,
  searchWord: keyword,
  doneFetch: doneFetch,
})

export const getSearchResultsFailure = (error, keyword, doneFetch) => ({
  type: 'GET_SEARCHRESULTS_FAILURE',
  items: error,
  searchWord: keyword,
  doneFetch: doneFetch,

})
```

###rootReducer.js, SearchForm.js
* 省略します。
* [ページネーションと並び替えに対応した投稿一覧画面とAPIの実装【初学者のReact×Railsアプリ開発 第10回】](https://qiita.com/dl10yr/items/66bb28307dcb61d2b4ab)
* [redux-formを使って投稿画面を実装する【初学者のReact×Railsアプリ開発 第7回】](https://qiita.com/dl10yr/items/0db8e591cfe62f59bcb5)