やったこと
- 投稿の検索画面を作成し、検索結果を表示できるようにした。
- reduxを利用して検索結果を管理。(ページを移動して戻ってくるときに、前の検索結果を表示させるようにしたかった)
- ページネーションの実装にkaminariを使っている
- フォームの実装はredux-form
成果物
実装手順(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
- 前に記述したときと大まかには変わりませんが、doneFetchとかsearchWordといった引数の数が増えているので、そこが注意ですかね。ページネーションと並び替えに対応した投稿一覧画面とAPIの実装【初学者のReact×Railsアプリ開発 第10回】
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,
})