LoginSignup
5
3

More than 3 years have passed since last update.

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

Posted at

やったこと

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

成果物

qkb5r-7d5kg.gif

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
5
3
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
5
3