やったこと
- Reactでの投稿一覧画面の実装と並び替えに対応するためのRails APIの実装
- ラジオボタンの変更によるAPIからの投稿の取得と表示
- reduxを使った表示する投稿の状態管理
- material-ui-flat-paginationを用いたページネーションの実装
成果物
Rails APIの実装手順
route.rb: ルートの編集
route.rb
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
namespace :v1 do
get 'posts', to: 'posts#index'
get 'posts_suki', to: 'posts#suki_index'
get 'posts_allcount', to: 'posts#all_count_index'
end
end
end
posts_controller
- API側のページネーションの実装として、kaminariを用いています。
- ポストは1ページあたり10個ずつ返すようにしています。新着順や投票数順など、order('...')で、postsテーブルのどのカラムで並び替えするかを記述しています。
- page_lengthは、React側でページ数を何ページまで表示するかを確定させるために必要な情報です。46個の投稿なら5ページまでなど...
posts_controller.rb
def index
posts = Post.page(params[:page] ||= 1).per(10).order('created_at DESC')
page_length = Post.page(1).per(10).total_pages
json_data = {
'posts': posts,
'page_length': page_length,
}
render json: { status: 'SUCCESS', message: 'Loaded posts', data: json_data}
end
def suki_index
posts = Post.page(params[:page] ||= 1).per(10).order('suki_count DESC')
page_length = Post.page(1).per(10).total_pages
json_data = {
'posts': posts,
'page_length': page_length,
}
render json: { status: 'SUCCESS', message: 'Loaded posts', data: json_data}
end
def all_count_index
posts = Post.page(params[:page] ||= 1).per(10).order('all_count DESC')
page_length = Post.page(1).per(10).total_pages
json_data = {
'posts': posts,
'page_length': page_length,
}
render json: { status: 'SUCCESS', message: 'Loaded posts', data: json_data}
end
React実装手順
App.js
- ルートの編集です。
App.js
import PostsList from './containers/PostsList';
<Auth>
<Switch>
<Route exact path="/" component={Home} />
<Route path='/create' component={Create} />
<Route path='/postslist' component={PostsList} />
</Switch>
</Auth>
PostsList.js
- ここでは一部のコードのみ紹介します。
- 表示させるポストなどの情報は、Redux(PostListReducer)で管理しています。
- componentdidmountで、初期描画の際に表示させるポストの取得を行っています。前回描画時の情報を保存しておくためにreduxでの状態管理を行っています。
- handleChangeらラジオボタンの変更に対応しています。
- handlePaginationClickは、ページリンクの変更に対応しています。offsetは1ページ目なら0、2ページ目をクリックしたときは10、3ページ目なら20...です。この情報で、APIで何ページ目の情報をもらうか確定させています。
Postslist.js
//module import, css部分は省略
class PostsList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handlePaginationClick = this.handlePaginationClick.bind(this);
}
componentDidMount() {
const { PostsListReducer } = this.props;
if (PostsListReducer.selected === "新着順") {
this.props.actions.getPostsList("", PostsListReducer.offset, "新着順")
} else if (PostsListReducer.selected === "スキが多い順") {
this.props.actions.getPostsList("_suki", PostsListReducer.offset, "スキが多い順")
} else if (PostsListReducer.selected === "投票数が多い順") {
this.props.actions.getPostsList("_allcount", PostsListReducer.offset, "投票数が多い順")
}
}
handleChange(e) {
if (e.target.value === "新着順") {
this.props.actions.getPostsList("", 0, "新着順")
} else if (e.target.value === "スキが多い順") {
this.props.actions.getPostsList("_suki", 0, "スキが多い順")
} else if (e.target.value === "投票数が多い順") {
this.props.actions.getPostsList("_allcount", 0, "投票数が多い順")
}
}
handlePaginationClick(offset) {
const { PostsListReducer } = this.props;
if (PostsListReducer.selected === "新着順") {
this.props.actions.getPostsList("", offset, "新着順")
} else if (PostsListReducer.selected === "スキが多い順") {
this.props.actions.getPostsList("_suki", offset, "スキが多い順")
} else if (PostsListReducer.selected === "投票数が多い順") {
this.props.actions.getPostsList("_allcount", offset, "投票数が多い順")
}
}
PostsList.js(render)
- 続いて、レンダーの部分です。
- PostListReducerの情報を使って、表示を制御しています。
- 各ポストには、リンク("/posts/post.id")を貼って、詳細ページに飛べるようにしています。
- RadioGroupタグと、Paginationタグの設定が多少頭を使います。
PostsList.js
render() {
const { CurrentUserReducer } = this.props;
const { PostsListReducer } = this.props;
const { classes } = this.props;
return (
<Scrollbars>
<div className={classes.container}>
<FormControl component="fieldset">
<FormLabel component="legend"></FormLabel>
<RadioGroup aria-label="position" name="position" value={PostsListReducer.selected} onChange={this.handleChange} row>
<FormControlLabel
value="新着順"
control={<Radio color="primary" />}
label="新着順"
labelPlacement="end"
/>
<FormControlLabel
value="スキが多い順"
control={<Radio color="primary" />}
label="スキが多い順"
labelPlacement="end"
/>
<FormControlLabel
value="投票数が多い順"
control={<Radio color="primary" />}
label="投票数が多い順"
labelPlacement="end"
/>
</RadioGroup>
</FormControl>
<ul className={classes.ul}>
{PostsListReducer.items.map((post) => (
<Link to={"/posts/" + post.id} className={classes.link}>
<li className={classes.li} key={post.id}>
<div className={classes.licontent}>
<h3 className={classes.lih3}>{post.content}</h3>
</div>
</li>
</Link>
))}
</ul>
<MuiThemeProvider theme={pagitheme}>
<CssBaseline />
<Pagination
limit={10}
offset={PostsListReducer.offset}
total={PostsListReducer.page_length * 10}
onClick={(e, offset) => this.handlePaginationClick(offset)}
/>
</MuiThemeProvider>
</div>
</Scrollbars>
)
}
}
actions/index.js
- ここでは、APIから投稿を取得しています。
- PostsListReducer.jsでstateを変更するためのactionの内容の記述とdispatchをしています。
index.js
export const getPostsList = (fetchlink, offset, selected) => {
return (dispatch) => {
dispatch(getPostsListRequest())
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/posts${fetchlink}?page=${page_url}`, {
headers: {
'access-token': auth_token,
'client': client_id,
'uid': uid
}
})
.then(response => dispatch(getPostsListSuccess(response.data.data.posts, offset, response.data.data.page_length, selected)))
.catch(error => dispatch(getPostsListFailure(error, offset, selected)))
};
};
export const getPostsListRequest = () => ({
type: 'GET_POSTSLIST_REQUEST',
})
export const getPostsListSuccess = (json, offset, page_length, selected) => ({
type: 'GET_POSTSLIST_SUCCESS',
items: json,
offset: offset,
page_length: page_length,
selected: selected,
})
export const getPostsListFailure = (error, offset, selected) => ({
type: 'GET_POSTSLIST_FAILURE',
items: error,
offset: offset,
selected: selected,
})
reducers/PostListReducer.js
- ここでreduxのstateの変更を行っています。
- initialStateに記述の通り、初期状態では新着順の1ページ目が表示されるようになっています。
PostListReducer.js
const initialState = {
isFetching: false,
items: [],
offset: 0,
page_length: 1,
selected: "新着順",
};
const PostsListReducer = (state = initialState, action) => {
switch (action.type) {
case 'GET_POSTSLIST_REQUEST':
return {
...state,
isFetching: true,
items: [],
offset: "",
page_length: "",
};
case 'GET_POSTSLIST_SUCCESS':
return {
...state,
isFetching: false,
items: action.items,
offset: action.offset,
page_length: action.page_length,
selected: action.selected,
};
case 'GET_POSTSLIST_FAILURE':
return {
...state,
isFetching: false,
error: action.error,
selected: action.selected,
offset: action.offset,
};
default:
return state;
}
};
export default PostsListReducer;
reducers/rootReducer.js
- rootReducerにPostsListReducerを追加しています。
rootReducer.js
import { combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'
import { routerReducer } from 'react-router-redux'
import CurrentUserReducer from './CurrentUserReducer'
import PostsListReducer from './PostsListReducer'
const rootReducer = combineReducers({
CurrentUserReducer,
form: formReducer,
router: routerReducer,
PostsListReducer
})
export default rootReducer