LoginSignup
4
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-15

やったこと

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

成果物

8gqb6-4o8vr.gif

実装手順(Rails API)

posts_controller

  • 検索を行うコアとなる処理の記述を行っています。
  • content LIKE?の使い方を初めて学びました。
  • %をつけると、あいまい検索になる。無いと、完全一致。検索ワードはクエリでもらってる。
  • ページネーションを使っているので、page_length(何ページまであるか)も返しています。
  • 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(ルートの編集)

  • 追加します。
route.rb
 get 'search', to: 'posts#search'

実装手順(React)

Search.js(render)

  • 大きく分けて、3つの部品(検索フォーム、検索結果表示部分、ページネーション部分)に分けてレンダリングしている。
  • 検索結果表示部分をthis.renderResults()で、初回訪問かどうか(doneFetch)、結果が存在しているかどうか(noResults)で制御している。
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は変更されないから。
  • 表示の制御は少し頭を使いました。
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の状態を変更するかでレンダリング結果が変わってきます。そこに頭を使いました。
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

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

4
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
7