react-virtualizedで膨大なリストの表示と状態復元

React(web)で膨大な数が見込まれるリスト表示にはreact-virtualized使っていくのがベターっぽいのでメモ。
virtualScroll実装は普通にjQueryやスタンドアローンのライブラリでもありそうなので、DOMをめっちゃappendしていくような処理があれば導入を検討してもいいかも。

用途

  • 無限スクロール実装等の初期レンダリング負荷削減
  • SPA時のリストDOM復元/スクロール位置復元の処理高速化
  • virtualDOMの差分計算の簡略化

要するにブラウザに優しい。

コンテナとなる親divの高さを動的に拡張していく実装の備忘録。

import { List  , AutoSizer , WindowScroller} from 'react-virtualized';
class HugeList extends React.Component<{dataList : Array<{}>} , {}>{
    constructor(props) {
        super(props);
    }

    noRowsRenderer(){
        return <div>noRow</div>
    }

    renderRow({style , index}) {
        return (
            <div style={style} key={index}>
                リストの一行
            </div>
        );
    }

    render() {

        return (
            <div >
                <WindowScroller
                    scrollElement={null}
                >
                    {({ height, isScrolling, onChildScroll, scrollTop }) => {
                        return (
                            <AutoSizer disableHeight>
                                {({ _height, width }) => (
                                    <List
                                        autoHeight
                                        width={width}
                                        height={height}
                                        rowCount={this.props.dataList.length}
                                        rowHeight={160}
                                        isScrolling={isScrolling}
                                        onScroll={onChildScroll}
                                        scrollTop={scrollTop}
                                        noRowsRenderer={this.noRowsRenderer.bind(this)}
                                        overscanRowCount={2}
                                        rowRenderer={this.renderRow.bind(this)}
                                    />
                                )}
                            </AutoSizer>
                        );
                    }}
                </WindowScroller>
            </div>
        );
    }
}

rowHeightに固定のサイズを与えているが、行ごとに動的に当ててもイケる。

rowHeight={(item , index)=> /* itemに応じたheight設定をreturn */ }

リストだけでなく、Masonryレイアウトも対応しているっぽい。

SPA時の位置復元例(高階関数で)

各URLのエンドポイントとなるコンテナに刺すHoC。
EndpointHoc.tsx


const instanceHistory : Array<string>= []; const savedInstanceMemory : {[key:string] : InstanceState<any>} = {} const EndpointHoc : any = (WrappedComponent : any )=> { return class extends React.PureComponent<RouteComponentProps<{}> , any >{ childRef; action : string; savedInstanceState : InstanceState<any> = { instanceID : "VIEW_HISTORY_INSTANCE_" + Math.random(), pathname : this.props.location.pathname, documentHeight : 0, scrollY : 0, extra : {}, action : "REPLACE" } pathname : string; isPOP(){ if (this.props.history.action != "POP" ){ return false; } if (instanceHistory.length < 2){ return false; } if (savedInstanceMemory[instanceHistory[1]] == null){ return false; } if (savedInstanceMemory[instanceHistory[1]].pathname != this.props.location.pathname){ return false; } return true; } constructor(props) { super(props); this.action = this.props.history.action; this.pathname = this.props.location.pathname; if (lastpath != this.props.location.pathname){ if (this.isPOP()){ console.log("@POP!!!!!") const prevInstanceID = instanceHistory.shift(); delete savedInstanceMemory[prevInstanceID]; const instanceID = instanceHistory[0]; this.savedInstanceState = savedInstanceMemory[instanceID]; this.savedInstanceState.action = "POP"; setTimeout(()=>{ window.scrollTo(0,this.savedInstanceState.scrollY ); } , 16) } else { console.log("@PUSH!") window.scrollTo(0,0); this.savedInstanceState.action = "PUSH"; instanceHistory.unshift(this.savedInstanceState.instanceID); savedInstanceMemory[this.savedInstanceState.instanceID] = this.savedInstanceState; } } lastpath = this.props.location.pathname; } documentHeight() { return Math.max( document.documentElement.clientHeight, document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight ); } componentWillUnmount(){ this.savedInstanceState = {...this.savedInstanceState , scrollY : window.scrollY , documentHeight : this.documentHeight(), pathname : this.props.location.pathname, extra : {} }; savedInstanceMemory[this.savedInstanceState.instanceID] = this.savedInstanceState; } onSaveExtra(lastState : object){ this.savedInstanceState = {...this.savedInstanceState, extra : lastState} savedInstanceMemory[this.savedInstanceState.instanceID] = this.savedInstanceState; } render() { const addProp = { style : { minHeight : this.savedInstanceState.documentHeight } } return ( <div {...addProp} > <WrappedComponent {...this.props} ref={ref => this.childRef = ref} savedInstanceState={this.savedInstanceState} onSaveExtra={this.onSaveExtra.bind(this)} /> </div> ); } } } export interface EndpointHocProps <T> extends RouteComponentProps<{}> { savedInstanceState : InstanceState<T> onSaveExtra : (savedState : T)=>void } export default EndpointHoc;

ついでにwindow.historyのPUSH/POP時にインスタンスのEndpointにスクロール位置復元や任意のstateの保存をし、復元時にpropsとして渡す例。(react-router-v4の場合)
URLのback時にbodyの高さと位置とを復元する。
この辺はブラウザの機能の車輪再発明感がパないので何かいい実装ないかな。。。

Read More