ページネーションと並び替えに対応した投稿一覧画面とAPIの実装【初学者のReact×Railsアプリ開発 第10回】

#やったこと
* Reactでの投稿一覧画面の実装と並び替えに対応するためのRails APIの実装
* ラジオボタンの変更によるAPIからの投稿の取得と表示
* reduxを使った表示する投稿の状態管理
* material-ui-flat-paginationを用いたページネーションの実装

#成果物
![qkb5r-7d5kg.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/367226/cdec2d86-b5c8-d4f1-30f9-c51e83303614.gif)

#Rails APIの実装手順
###route.rb: ルートの編集
```ruby: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ページまでなど...

```ruby: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
* ルートの編集です。

```javascript: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で何ページ目の情報をもらうか確定させています。

```javascript: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タグの設定が多少頭を使います。

```javascript: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をしています。

```javascript: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ページ目が表示されるようになっています。

```javascript: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を追加しています。

```javascript: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
```