react-virtualized とは?
- Reactで大量のリストアイテムを描画するとパフォーマンスが悪くなるという問題があります
- react-virtualizedはそういうときにパフォーマンス維持するためのライブラリです
- React公式ドキュメントでも紹介されています
- パフォーマンス最適化のページ(React Optimizing Performance)
公式デモページ
https://bvaughn.github.io/react-virtualized/
リポジトリ
https://github.com/bvaughn/react-virtualized
作者のプレゼンスライド
https://bvaughn.github.io/forward-js-2017/#/17/0
経緯とか理論とかがわかる
本記事の趣旨
いざreact-virtualizedを使ってみると結構APIが複雑でハマりがたくさんあります。
端的にいうと各コンポーネントの組み合わせパターンが多くドキュメントからは察せない、あるいはIssueやExampleにしか情報がないなどがあり撃沈します。
そこでライブラリのドキュメントとIssueを散々読み漁ることになるわけですが、かなりつらいので、タイムライン型のコンポーネントを作るにあたって自分が得た知見を以下に記します。
本記事のexample
TwitterとかFBのタイムラインみたいなコンポーネント
https://codesandbox.io/embed/github/1natsu172/react-virtualized_twitter_timeline-example/tree/master/
いわゆる無限スクロールのリスト型コンポーネントです。
またDynamicHeight(リストのアイテムとなるRowの高さが固定ではない)というめんどくさい構成です。
使用しているreact-virtualizedコンポーネント
- InfiniteLoader
- WindowScroller
- AutoSizer
- List
- CellMeasurer
- CellMeasurerCache
この構成なのでGrid
とかTable
とかCollection
など上記構成以外のコンポーネントを組み合わせるときはまた違うハマりがあるだろうなと思います
*注意書き*
執筆時点のreact-virtualizedのバージョンは9.20.1
です。
またコード片は長くなるので一部のみにしたり必須のpropを省略したりしています。
完全な動くコードは上記CodeSandboxのデモのコードを参照してください
一部WindowsScrollerを用いる場合は不必要な知見がありますがハマる可能性もあるので一応記載しています。
また文中のRVは"react-virtualized"の略称です。
WindowScroller
+ List
してるのにwindowのスクロールにならない
Question: なぜかリストのコンテナ内がスクロールしてしまう…
Answer: List
のautoHeight
のPropsをTrueにする
<List autoHeight={true} ...{listProps} />
WindowScroller
+ List
でスクロール系のPropsが効かない
Question: List
のスクロールのpropsが発火しなかったり…
Answer: WindowScroller
からscrollTop
, isScrolling
などのpropsをList
コンポーネントにわたす
...
<WindowScroller>
{({ height, isScrolling, scrollTop, onChildScroll }) => (
<AutoSizer disableHeight={true}>
{({ width }) => (
<List
isScrolling={isScrolling} // ← これ
onScroll={onChildScroll} // ← これ
scrollTop={scrollTop} // ← これ
{...ListProps}
/>
)}
</AutoSizer>
)}
</WindowScroller>
...
参考になった WindowScroller + InfiniteLoader + AutoSizer な Plunker
CellMeasurer
の内側で画像が読み込まれたときにrowの高さが再度変わるがどうすればよい?
Question: 画像読み込み時にrowの高さを再計算する必要がありそうだがどうすれば?
Answer: List
の deferredMeasurementCache
に CellMeasurerCache
をわたしつつ、 function as childに measure
が生えてるのでそれを <img onLoad={} />
時に発火させる
長いし意味わからん。
ようするに
-
List
にCellMeasurerCache
を渡して - 画像のonLoad時に
CellMeasurer
から生えてるmeasure
メソッドを発火させる
...
<List
deferredMeasurementCache={this._renderCache} // ← わたす
rowRenderer={this._renderRow}
{...ListProps}
/>
...
...
_renderRow = ({ index, key, parent, style }) => (
<CellMeasurer {...CellMeasurerProps} >
// ↓ measureがあるので
{({ measure }) => (
<div style={style} className="row">
<Tweet
measure={measure} // ← わたす
/>
</div>
)}
</CellMeasurer>
)
...
// SFC
const Tweet = ({measure,...rest}) => (
<div className="tweetImage">
<img
src={...}
onLoad={measure} // ← ここでね
alt=""
/>
</div>
)
例ではTweetコンポーネントにバケツリレーしているけれど、measure
メソッドを使いたい場所は深いところにあるかもしれないので、React16以降ならContextAPIを使うとラクなれる気がする。
あと deferredMeasurementCache
は List
のドキュメントに載っていないが、実はこれはRVの Grid
コンポーネントのものなのでそっちのドキュメントに載っている…(後述 )1
InfiniteLoader
の loadMoreRows
が発火しない
Question: リストの最後までスクロールしたのに追加読み込みが実行されない…
Answer: リストアイテム数と rowCount
の指定数が同じだとNG
これはかなりややこしくて、Twitterのタイムラインのようなリストのアイテムの数がいくつになるか不透明なものの場合は、InfiniteLoader
の rowCount
を『現段階のリストアイテム数+1』としておく。
_rowCount = this.state.tweets.length
render() {
return (
<InfiniteLoader
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={this._rowCount + 1} // リストアイテム数 + 1
{...InfiniteProps}
>
{({ onRowsRendered, registerChild }) => (
<List
rowCount={this._rowCount + 1} // リストアイテム数 + 1
{...ListProps}
/>
)}
</InfiniteLoader>
)
}
なぜ +1
するのか?
InfiniteLoader
は isRowLoaded
で false
が返ってくるとそのrowのindex値を元に loadMoreRows
を実行するようになっている。
そこで +1
しておくことで、たとえば現時点のリストアイテムが20件の場合、21件目を読み込もうと試行するようになる。
けれどリストアイテムは20件しかなく当然21件目はないので isRowLoaded
で false
が返る
つまり
falseが返る → InfiniteLoader
的には 『追加読み込み分がある』と解釈される → loadMoreRows
が実行される
となる。
余談として、Twitterタイムラインのようなリストアイテム数が不明な場合はこの +1
戦法を使うけれど、アイテム数が事前にわかっていたりクエリパラメータに次のindexを渡したりする必要があるときは以下の公式ドキュメントにあるように読み込み済みのrowのindexをメモ化したりトラッキングする必要がある。
本稿では前提が変わるのでこれ以上は解説しないですが……そういうパターンもあるということです。
loadMoreRows
中はLoadingインジケーターを表示したい
Question: 無限スクロールで次のコンテンツのロード中にはLoadingインジケーターを表示したい。読み込むコンテンツがなくなったらLoading表示は消したい…
Answer: isLoading
フラグと hasMore
フラグを立てて rowCount
を調整する
前項でいうところの +1
した結果 false
が返ってくるrowがある = "追加読み込み分ありと解釈できる" という挙動を利用する。
つまり
-
isRowLoaded
でtrue
が返ってくるならコンテンツが読み込み済みであると考えられる-
false
が返ってくるならloadingインジケーターを出せばOK
-
class extends React.Component {
state = {
tweets: [],
isLoading: false,
hasMore: true
}
_isRowLoaded = ({ index }) => !!this.state.tweets[index]
_infiniteRowCount = () =>
!this.state.isLoading && this.state.hasMore
? this.state.tweets.length + 1
: this.state.tweets.length
_listRowCount = () =>
this.state.hasMore ? this.state.tweets.length + 1 : this.state.tweets.length
render() {
return (
<InfiniteLoader
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={this._infiniteRowCount()} // ← これ
{...InfiniteProps}
>
{({ onRowsRendered, registerChild }) => (
<List
rowRenderer={this._renderRow}
rowCount={this._listRowCount()} // ← これ
{...ListProps}
/>
)}
</InfiniteLoader>
)
}
_renderRow = ({ index, key, parent, style }) => (
<CellMeasurer {...CellMeasurerProps}>
<div style={style} className="row">
{
// falseが返ってくる => +1した余分なrow
this._isRowLoaded({ index }) ? (
<Tweet {...tweet} />
) : (
<div>loading...</div> // ← インジケーター
)}
</div>
</CellMeasurer>
)
}
hasMore
フラグによって +1
するかしないかを調整している。+1
しなければInfiniteLoader
の loadMoreRows
は発火しないし、Listに余分なRowも表示されないので目的通りの挙動になる。
CodeSandboxのデモでは80件まで読み込むと hasMore
が false
になるようにしてあるので、Loadingインジケーターは消えて無限スクロールできないようになっている。
AutoSizer
使ってるけどheightの計算が効かない
Question: コンテナのheightが0になってなにも描画されない…
Answer: 親要素にheightを確保する必要がある
実はCSSが絡んでいる…
公式DocsにFAQがあり言及されているが見つけづらい。
RVの親要素にheightの指定をする、あるいはflex:1の指定をするとかして高さを確保する。
<div style={{ display: 'flex' }}>
<div style={{
flex: '1 1 auto',
// or
height: 600px;
}}>
<AutoSizer>
{({ height, width }) => (
<List {...ListProps} />
)}
</AutoSizer>
</div>
</div>
なお
WindowScroller
を利用する場合は高さはwindowのサイズになるためこの親の高さ確保は不要
その他知見
List
のアレコレは Grid
コンポーネントのアレコレだったりする
RVのドキュメントやexampleコードとかを読んでいると一見 List
のpropsに関係ないものが出てきたりする。
これはどういうことなのかというと List
コンポーネントは Grid
コンポーネントを内部で利用してrowの描画を行っているからで、propsはすべて Grid
コンポーネントへ渡されている。
List
のドキュメントの最初の行で実は言及がされている
This component renders a windowed list (rows) of elements. It uses a Grid internally to render the rows and all props are relayed to that inner Grid. That means that List also accepts Grid props in addition to the props shown below.
なのでもし List
を使っていてなにかあれば Grid
の問題だったりする。List
についてissueやStackOverFlowで検索するときも Grid
も視野にいれて検索すると解決したりする(かもしれない)
参考になるURLなど
- RV作者によるTwitter/FBライクなタイムライン型リストのexampleリポジトリ
- https://github.com/bvaughn/tweets
- nowにホスティングされていたらしきものは404になっている…
- StackOverFlowのRVタグ
- https://stackoverflow.com/questions/tagged/react-virtualized
- エッジなケースやRVの組み合わせ問題による質問と解答はこっちにあることが多い
- 公式ドキュメント
-
https://github.com/bvaughn/react-virtualized/tree/master/docs
- なんかあったらとりあえず穴が空くほど読む
-
公式デモページで実際に動いてるソースコードはどこ?
- リポジトリの/source配下に
<コンポーネント名>.example.js
がありそれがそう - 例: https://github.com/bvaughn/react-virtualized/blob/master/source/InfiniteLoader/InfiniteLoader.example.js
- リポジトリの/source配下に
-
https://github.com/bvaughn/react-virtualized/tree/master/docs
以上です
とにかく頑張ってハマりから抜け出す必要があります…
本稿はRVを使ってTwitter/FBライクな無限スクロールのリストを作るという目的に沿ったハマり集ですが、他の形式のRVを用いたコンポーネント作成の際にもなにか参考になればと思います。
DynamicHeightなInfiniteLoader + WindowScroller + ……etc という本構成のような動くデモは自分が探した限り見つからなかったので同様のものを考えている方の参考にもなれば幸いです
-
List
のアレコレはGrid
コンポーネントのアレコレだったりする ↩