perf: Use progressive rendering on PaginatedList component (#1156)

* Use progressive rendering on PaginatedList component
Move drafts and starred views to paginated

* heading
This commit is contained in:
Tom Moor
2020-01-13 18:17:56 -08:00
committed by GitHub
parent 5b78cb8963
commit 22230c25e5
4 changed files with 63 additions and 126 deletions

View File

@@ -1,12 +1,9 @@
// @flow
import * as React from 'react';
import { observable, action } from 'mobx';
import { observer } from 'mobx-react';
import { Waypoint } from 'react-waypoint';
import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore';
import Document from 'models/Document';
import DocumentList from 'components/DocumentList';
import { ListPlaceholder } from 'components/LoadingPlaceholder';
import DocumentPreview from 'components/DocumentPreview';
import PaginatedList from 'components/PaginatedList';
type Props = {
documents: Document[],
@@ -18,79 +15,20 @@ type Props = {
@observer
class PaginatedDocumentList extends React.Component<Props> {
isInitiallyLoaded: boolean = false;
@observable isLoaded: boolean = false;
@observable isFetchingMore: boolean = false;
@observable isFetching: boolean = false;
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
componentDidMount() {
this.isInitiallyLoaded = !!this.props.documents.length;
this.fetchResults();
}
componentDidUpdate(prevProps: Props) {
if (prevProps.fetch !== this.props.fetch) {
this.fetchResults();
}
}
fetchResults = async () => {
this.isFetching = true;
const limit = DEFAULT_PAGINATION_LIMIT;
const results = await this.props.fetch({
limit,
offset: this.offset,
...this.props.options,
});
if (
results &&
(results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT)
) {
this.allowLoadMore = false;
} else {
this.offset += DEFAULT_PAGINATION_LIMIT;
}
this.isLoaded = true;
this.isFetching = false;
this.isFetchingMore = false;
};
@action
loadMoreResults = async () => {
// Don't paginate if there aren't more results or were in the middle of fetching
if (!this.allowLoadMore || this.isFetching) return;
this.isFetchingMore = true;
await this.fetchResults();
};
render() {
const { empty, heading, documents, fetch, options, ...rest } = this.props;
const showLoading =
this.isFetching && !this.isFetchingMore && !this.isInitiallyLoaded;
const showEmpty = !documents.length && !showLoading;
const showList =
(this.isLoaded || this.isInitiallyLoaded) && !showLoading && !showEmpty;
return (
<React.Fragment>
{showEmpty && empty}
{showList && (
<React.Fragment>
{heading}
<DocumentList documents={documents} {...rest} />
{this.allowLoadMore && (
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
)}
</React.Fragment>
<PaginatedList
items={documents}
empty={empty}
heading={heading}
fetch={fetch}
options={options}
renderItem={item => (
<DocumentPreview key={item.id} document={item} {...rest} />
)}
{showLoading && <ListPlaceholder count={5} />}
</React.Fragment>
/>
);
}
}

View File

@@ -11,6 +11,7 @@ import { ListPlaceholder } from 'components/LoadingPlaceholder';
type Props = {
fetch?: (options: ?Object) => Promise<void>,
options?: Object,
heading?: React.Node,
empty?: React.Node,
items: any[],
renderItem: any => React.Node,
@@ -22,6 +23,7 @@ class PaginatedList extends React.Component<Props> {
@observable isLoaded: boolean = false;
@observable isFetchingMore: boolean = false;
@observable isFetching: boolean = false;
@observable renderCount: number = DEFAULT_PAGINATION_LIMIT;
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
@@ -30,6 +32,12 @@ class PaginatedList extends React.Component<Props> {
this.fetchResults();
}
componentDidUpdate(prevProps: Props) {
if (prevProps.fetch !== this.props.fetch) {
this.fetchResults();
}
}
fetchResults = async () => {
if (!this.props.fetch) return;
@@ -48,6 +56,7 @@ class PaginatedList extends React.Component<Props> {
this.offset += limit;
}
this.renderCount += limit;
this.isLoaded = true;
this.isFetching = false;
this.isFetchingMore = false;
@@ -55,34 +64,47 @@ class PaginatedList extends React.Component<Props> {
@action
loadMoreResults = async () => {
// Don't paginate if there aren't more results or were in the middle of fetching
// Don't paginate if there aren't more results or were currently fetching
if (!this.allowLoadMore || this.isFetching) return;
this.isFetchingMore = true;
await this.fetchResults();
// If there are already cached results that we haven't yet rendered because
// of lazy rendering then show another page.
const leftToRender = this.props.items.length - this.renderCount;
if (leftToRender > 1) {
this.renderCount += DEFAULT_PAGINATION_LIMIT;
}
// If there are less than a pages results in the cache go ahead and fetch
// another page from the server
if (leftToRender <= DEFAULT_PAGINATION_LIMIT) {
this.isFetchingMore = true;
await this.fetchResults();
}
};
render() {
const { items, empty } = this.props;
const { items, heading, empty } = this.props;
const showLoading =
this.isFetching && !this.isFetchingMore && !this.isInitiallyLoaded;
const showEmpty = !items.length || showLoading;
const showList = (this.isLoaded || this.isInitiallyLoaded) && !showLoading;
const showEmpty = !items.length && !showLoading;
const showList =
(this.isLoaded || this.isInitiallyLoaded) && !showLoading && !showEmpty;
return (
<React.Fragment>
{showEmpty && empty}
{showList && (
<React.Fragment>
{heading}
<ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{items.map(this.props.renderItem)}
{items.slice(0, this.renderCount).map(this.props.renderItem)}
</ArrowKeyNavigation>
{this.allowLoadMore && (
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
<Waypoint key={this.renderCount} onEnter={this.loadMoreResults} />
)}
</React.Fragment>
)}

View File

@@ -4,10 +4,9 @@ import { observer, inject } from 'mobx-react';
import Heading from 'components/Heading';
import CenteredContent from 'components/CenteredContent';
import { ListPlaceholder } from 'components/LoadingPlaceholder';
import Empty from 'components/Empty';
import PageTitle from 'components/PageTitle';
import DocumentList from 'components/DocumentList';
import PaginatedDocumentList from 'components/PaginatedDocumentList';
import Subheading from 'components/Subheading';
import InputSearch from 'components/InputSearch';
import NewDocumentMenu from 'menus/NewDocumentMenu';
@@ -20,28 +19,22 @@ type Props = {
@observer
class Drafts extends React.Component<Props> {
componentDidMount() {
this.props.documents.fetchDrafts();
}
render() {
const { isLoaded, isFetching, drafts } = this.props.documents;
const showLoading = !isLoaded && isFetching;
const showEmpty = isLoaded && !drafts.length;
const { fetchDrafts, drafts } = this.props.documents;
return (
<CenteredContent column auto>
<PageTitle title="Drafts" />
<Heading>Drafts</Heading>
{showEmpty ? (
<Empty>Youve not got any drafts at the moment.</Empty>
) : (
<React.Fragment>
<Subheading>Documents</Subheading>
<DocumentList documents={drafts} showDraft={false} showCollection />
{showLoading && <ListPlaceholder />}
</React.Fragment>
)}
<PaginatedDocumentList
heading={<Subheading>Documents</Subheading>}
empty={<Empty>Youve not got any drafts at the moment.</Empty>}
fetch={fetchDrafts}
documents={drafts}
showDraft={false}
showCollection
/>
<Actions align="center" justify="flex-end">
<Action>
<InputSearch />

View File

@@ -3,11 +3,10 @@ import * as React from 'react';
import { observer, inject } from 'mobx-react';
import CenteredContent from 'components/CenteredContent';
import { ListPlaceholder } from 'components/LoadingPlaceholder';
import Empty from 'components/Empty';
import PageTitle from 'components/PageTitle';
import Heading from 'components/Heading';
import DocumentList from 'components/DocumentList';
import PaginatedDocumentList from 'components/PaginatedDocumentList';
import InputSearch from 'components/InputSearch';
import Tabs from 'components/Tabs';
import Tab from 'components/Tab';
@@ -22,29 +21,16 @@ type Props = {
@observer
class Starred extends React.Component<Props> {
componentDidMount() {
this.props.documents.fetchStarred();
}
render() {
const {
isLoaded,
isFetching,
starred,
starredAlphabetical,
} = this.props.documents;
const { fetchStarred, starred, starredAlphabetical } = this.props.documents;
const { sort } = this.props.match.params;
const showLoading = !isLoaded && isFetching;
const showEmpty = isLoaded && !starred.length;
return (
<CenteredContent column auto>
<PageTitle title="Starred" />
<Heading>Starred</Heading>
{showEmpty ? (
<Empty>Youve not starred any documents yet.</Empty>
) : (
<React.Fragment>
<PaginatedDocumentList
heading={
<Tabs>
<Tab to="/starred" exact>
Recently Updated
@@ -53,15 +39,13 @@ class Starred extends React.Component<Props> {
Alphabetical
</Tab>
</Tabs>
<DocumentList
documents={
sort === 'alphabetical' ? starredAlphabetical : starred
}
showCollection
/>
</React.Fragment>
)}
{showLoading && <ListPlaceholder />}
}
empty={<Empty>Youve not starred any documents yet.</Empty>}
fetch={fetchStarred}
documents={sort === 'alphabetical' ? starredAlphabetical : starred}
showCollection
/>
<Actions align="center" justify="flex-end">
<Action>
<InputSearch />