ほぼ日刊サービス開発日誌

React, firebase, 機械学習など

sponsored

Twitterみたいに各タブが無限スクロールするコンポーネントの実装【React】

f:id:serendipity4u:20180506113735p:plain

ツイッターの様な、

「タイムライン」「お気に入り」「フォロワー」

みたいなタブが横並びになっておりそれぞれに無限スクロールが可能なユーザープロフィールページは、どうコンポーネント分けるのがベストなのか? について悩んでしまいました。

これ、よくあるパターンだと思うんですが、ググってもあまり参考記事が見つからなくてですね・・・。

無限スクロール単体や、タブ単体だったら記事はたくさん見つかるんです。参考にしたのは、

無限スクロール

codepen.io

タブの実装

totutotu.hatenablog.com

(↑偶然友達の戸塚くんのブログが当たった。)

じゃあ 無限スクロール+タブは?

水平タブ AND 無限スクロールの実装はなくてですね...

自己流でやったのがこの記事です。

ツイッターと同じテイストで、

「タイムライン(ItemList ) 」「フォローしている人 ( UserList ) 」「お気に入り ( ItemList ) 」のような並びになっています。

まずタブです。

selected の propsで選択されたタブのインデックスを保持しています。

<Tabs selected={0} >

          <Pane label="Board">
    contents 1
          </Pane>

          <Pane label="Following">
    contents 2
          </Pane>

          <Pane label="Pin">
    contents 3
          </Pane>

</Tabs>

そして Paneコンポーネントの中で、 selected indexに該当する {this.props.children}returnするという流れで、詳しくは参考記事の通りです。

この Pane の中身、つまりそれぞれのタブのコンテンツに関して、

  1. それぞれのタブの中のコンポーネントにスクロール関数を持たせるのか

  2. それともタブのラッパーにスクロール関数を持たせるのか?

が論点でした。

結論から言うと、 1 の方式を取りました。

最初は、2の方がエレガントだと思いました。

スクロールに対するリスナー

window.addEventListener('scroll', this.onScroll, false);

を、重複して書くことがないから、というのが理由でした。

しかしながら、その方式だと、

  • 親コンポーネントから子コンポーネントの fetchNexPage() 的な関数を呼ぶ場合、子供コンポーネントに本当にその関数があるか保証しきれないので、親コンポーネントが子供のメソッドを呼ぶ設計は避けたい
  • 上記を避けるため、stateとして記事一覧やユーザー一覧を親コンポーネントが持ち、propsとして子供コンポーネントに渡す方式にすると、それぞれのタブの中身の次のページを呼ぶURLまで親コンポーネントが保持しなきゃいけなくなってしまうので、タブという「機能」、記事やユーザー一覧を表示・保持する「機能」が混在してしまう。
  • タブの数だけ、現在のページ数を保持しなくてはいけなくなる(今回は、latest_items_page, favorite_items_page, followers_page) 。

という点で、デメリットが生じました。

そこで、以下の様に、

<Tabs selected={0} >
          <Pane label="Board">
            <ItemList kind="latest_items" user={user}></ItemList>
          </Pane>

          <Pane label="Following">
            <UserList kind="following_users" user={user}></UserList>
          </Pane>

          <Pane label="Favorite">
            <ItemList kind="favorite_items" user={user}></ItemList>
          </Pane>
</Tabs>

記事一覧をリスト表示する<ItemList>,ユーザー一覧をリスト表示する <UserList>というコンポーネントを作成し、

  • そこに「どのユーザーのリストなのか?」を示す user={user}
  • 「どんな種類のリストなのか?」を示す kind="favorite_items"

と言う2つのpropsを渡してあげる設定にしました。

これによって、

  • というコンポーネントは、アイテム一覧を出し、スクロールすると fetchNextArticles(this.props.kind)を発動する
  • というコンポーネントは、ユーザー一覧を出し、スクロールすると fetchNextUsers(this.props.kind)を発動する
  • 次のページを取得するURLは、「お気に入り」「最新の記事」など記事に関するものなら、の中に定義されている
  • 次のページを取得するURLは、「フォロー」「フォロワー」などユーザーに関するものなら、の中に定義されている
  • ItemList, UserLIst が内部のstateとして取得済みページ数を保持する。保持するページの種類は1つだけで良い。

という直感的な法則性(Itemのことは全てItemListに、Userのことは全てUserListに書いてある)ができたうえに、

ユーザープロフィールページだけでなく、タイムラインページでも、<ItemList kind="timeline_items">を渡してあげれば、スムーズにItemListが利用できる様になり、コンポーネントの独立性を保証することができました。

例えば、「次のページを取得するURLをItemLIstが保持する」というのは、以下の様に、

class ItemList extends React.Component {
...

  fetchItems(kind) {
    {/* kindによってアイテム一覧の内部のデータを出し分けます */}

    if (kind == 'timeline_items'){
      this.fetchTimelineItems();
    } else if (kind == 'user_items')  {
      this.fetchUserItems();
    } else if (kind == 'pinned_items') {
      this.fetchPinnedItems()
    }

  }
}

タブ関連のラッパー親コンポーネントから渡された kind によって、次のアイテムを取得するajaxリクエストが含まれた関数を使い分ける、ということです。

ベストプラクティスが別にあれば、コメントにてご教授いただけると幸いです。