Skip to content

Commit f67ab3d

Browse files
author
Luis Merino
committed
feat(hasMore): prop that allows to bypass the length-size check
1 parent dce5a14 commit f67ab3d

File tree

10 files changed

+237
-50
lines changed

10 files changed

+237
-50
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<!-- ### An Infinite Scroller List Component -->
1010

11-
**React Intersection List** builds on top of **[React Intersection Observer](https://github.com/researchgate/react-intersection-observer)**, using `IntersectionObservers` to deliver a high-performance and smooth scrolling experience, even on low-end devices.
11+
**React Intersection List** builds on top of **[React Intersection Observer](https://github.com/researchgate/react-intersection-observer)**, using a [sentinel](https://en.wikipedia.org/wiki/Sentinel_value) in the DOM to deliver a high-performance and smooth scrolling experience, even on low-end devices.
1212

1313
## Getting Started
1414

@@ -72,6 +72,8 @@ Traditional solutions to this problem rely on throttled `scroll` event callbacks
7272

7373
- **length**: `number` | default: `0` (size of the list - the number of total items)
7474

75+
- **hasMore**: `bool` | default: `false` (if true forces the sentinel to observe)
76+
7577
- **threshold**: `string` | default: `100px` (specify using units _px_ or _%_ without negative values)
7678

7779
- **axis**: `'x' | 'y'` | Default: `0`

docs/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
## Documentation
22

3-
[TODO]
3+
Welcome to the code docs section! Here you'll find some implementation use cases:
4+
5+
* Visit our [examples](https://researchgate.github.io/react-intersection-list/) page
6+
* Take a look at the [recipes](./recipes) page

docs/docs/components/AsyncList/index.js

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,63 @@ const PAGE_SIZE = 20;
55

66
export default class extends React.Component {
77
state = {
8-
page: 0,
8+
currentPage: 0,
99
isLoading: false,
1010
repos: [],
1111
};
1212

13-
updateRepos(repos) {
13+
feedList(repos) {
1414
this.setState({
1515
isLoading: false,
1616
repos: [...this.state.repos, ...repos],
17+
hasMore: repos.length > 0,
1718
});
1819
}
1920

2021
handleLoadMore = () => {
2122
if (this.state.isLoading) {
2223
return;
2324
}
24-
const nextPage = this.state.page + 1;
25+
const currentPage = this.state.currentPage + 1;
26+
2527
this.setState({
2628
isLoading: true,
27-
page: nextPage,
29+
currentPage,
2830
});
2931

30-
const etag = sessionStorage.getItem('etag');
31-
let headers = {};
32-
if (etag) {
33-
headers = {
34-
'If-None-Match': etag,
35-
};
32+
const url = 'https://api.github.com/users/researchgate/repos';
33+
const qs = `?type=public&per_page=${PAGE_SIZE}&page=${currentPage}`;
34+
35+
const headers = {
36+
'Accept-Encoding': '',
37+
};
38+
const ifNoneMatch = sessionStorage.getItem('etag');
39+
if (ifNoneMatch) {
40+
headers['if-none-match'] = ifNoneMatch.match(/(?:\d|[a-z])+/)[0];
3641
}
37-
fetch(`https://api.github.com/users/researchgate/repos?type=public&per_page=${PAGE_SIZE}&page=${nextPage}`, {
38-
headers,
39-
})
42+
43+
let hasError = false;
44+
45+
fetch(url + qs, { headers })
4046
.then(response => {
41-
if (response.status !== 200) {
42-
throw new Error(`Failed with status code: ${response.status}`);
43-
}
44-
const ifNoneMatch = response.headers['If-None-Match'];
45-
if (ifNoneMatch) {
46-
sessionStorage.setItem('etag', ifNoneMatch);
47+
if (!(response.status !== 200)) {
48+
const etag = response.headers.get('etag');
49+
if (etag) {
50+
sessionStorage.setItem('etag', etag);
51+
}
52+
} else {
53+
hasError = true;
4754
}
4855
return response.json();
4956
})
5057
.then(repos => {
51-
this.updateRepos(repos.filter(repo => repo.fork === false && repo.language));
58+
if (hasError) {
59+
throw new Error(repos.message);
60+
}
61+
this.feedList(repos.filter(repo => repo.fork === false && repo.language));
5262
})
5363
.catch(err => {
54-
throw err;
64+
console.error(err); // eslint-disable-line
5565
});
5666
};
5767

@@ -65,7 +75,7 @@ export default class extends React.Component {
6575
const repo = this.state.repos[index];
6676
return (
6777
<div key={key}>
68-
{repo.name} - {repo.language}
78+
<strong>{repo.name}</strong>&nbsp;&nbsp;&lt;{repo.language}&gt;
6979
</div>
7080
);
7181
};
@@ -79,11 +89,11 @@ export default class extends React.Component {
7989
<div>
8090
{this.state.isLoading && <div className="loading">Loading</div>}
8191
<List
82-
length={this.state.repos.length}
83-
pageSize={PAGE_SIZE}
8492
itemsRenderer={this.renderItems}
93+
hasMore={this.state.hasMore}
94+
length={this.state.repos.length}
8595
onIntersection={this.handleLoadMore}
86-
threshold="0px"
96+
pageSize={PAGE_SIZE}
8797
>
8898
{this.renderItem}
8999
</List>

docs/docs/components/style.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ body {
4949
height: 333px;
5050
}
5151

52-
.list > * {
52+
.list > div {
5353
background-color: #4b9deb;
5454
color: #fff;
5555
margin: 15px;

docs/docs/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import 'intersection-observer'; // eslint-disable-line import/no-extraneous-dependencies
2+
import 'whatwg-fetch'; // eslint-disable-line import/no-extraneous-dependencies
23
import React from 'react';
34
import { storiesOf } from '@storybook/react'; // eslint-disable-line import/no-extraneous-dependencies
45
import InfiniteList from './components/InfiniteList';
56
import AsyncList from './components/AsyncList';
67
import Axis from './components/Axis';
78
import './components/style.css';
89

9-
storiesOf('Infinite', module)
10+
storiesOf('Examples', module)
1011
.add('Synchoronous', InfiniteList)
1112
.add('Asynchoronous', () => <AsyncList />)
1213
.add('X-Axis', Axis);

docs/recipes/README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
## Recipes
2+
3+
### Asynchonous Repo List
4+
5+
When the sentinel comes into view, you can use the callback to load data, create the next items, and attach them. For this case we're loading Github repositories with pagination. We assume that we don't know the total `length` and we'll want to keep fetching until the (unknown) end of the list. The solution here is to provide a computed boolean value to `hasMore`, in order to tell the sentinel to await more.
6+
7+
```jsx
8+
import React from 'react';
9+
import List from '@researchgate/react-intersection-list';
10+
11+
const PAGE_SIZE = 20;
12+
13+
export default class extends React.Component {
14+
state = {
15+
currentPage: 0,
16+
isLoading: false,
17+
repos: [],
18+
};
19+
20+
feedList = (repos) => {
21+
this.setState({
22+
isLoading: false,
23+
hasMore: repos.length > 0,
24+
repos: [...this.state.repos, ...repos],
25+
});
26+
};
27+
28+
handleLoadMore = () => {
29+
const currentPage = this.state.currentPage + 1;
30+
31+
this.setState({
32+
isLoading: true,
33+
currentPage,
34+
});
35+
36+
const url = 'https://api.github.com/users/researchgate/repos';
37+
const qs = `?type=public&per_page=${PAGE_SIZE}&page=${currentPage}`;
38+
39+
fetch(url + qs)
40+
.then(response => response.json())
41+
.then(this.feedList)
42+
.catch(err => {
43+
throw err;
44+
});
45+
};
46+
47+
renderItems = (items, ref) => (
48+
<div className="list" ref={ref}>
49+
{items}
50+
</div>
51+
);
52+
53+
renderItem = (index, key) => {
54+
const repo = this.state.repos[index];
55+
return (
56+
<div key={key}>
57+
<strong>{repo.name}</strong>
58+
</div>
59+
);
60+
};
61+
62+
componentDidMount() {
63+
this.handleLoadMore();
64+
}
65+
66+
render() {
67+
return (
68+
<div>
69+
{this.state.isLoading && <div className="loading">Loading</div>}
70+
<List
71+
itemsRenderer={this.renderItems}
72+
hasMore={this.state.hasMore}
73+
length={this.state.repos.length}
74+
onIntersection={this.handleLoadMore}
75+
pageSize={PAGE_SIZE}
76+
>
77+
{this.renderItem}
78+
</List>
79+
</div>
80+
);
81+
}
82+
}
83+
```
84+
85+
If it's possible to get the total `length` in advance, we won't need `hasMore` and the `pageSize` will be used to paginate results until we reach the bottom of the list.
86+
87+
### Infinite Synchronous List
88+
89+
```jsx
90+
import React from 'react';
91+
import List from '@researchgate/react-intersection-list';
92+
93+
export default () => (
94+
<List length={Infinity}>
95+
{(index, key) => <div key={key}>{index}</div>}
96+
</List>
97+
);
98+
```
99+
100+
### Can I submit a new recipe?
101+
102+
Yes, of course!
103+
104+
1. Fork the code repo.
105+
2. Create your new recipe in the correct subfolder within `./docs/docs/components/` (create a new folder if it doesn't already exist).
106+
3. Make sure you have included a README as well as your source file.
107+
4. Submit a PR.
108+
109+
_If you haven't yet, please read our [contribution guidelines](https://github.com/researchgate/react-intersection-list/blob/master/.github/CONTRIBUTING.md)._
110+
111+
### What license are the recipes released under?
112+
113+
By default, all newly submitted code is licensed under the MIT license.
114+
115+
### How else can I contribute?
116+
117+
Recipes don't always have to be code - great documentation, tutorials, general tips and even general improvements to our examples folder are greatly appreciated.

package.json

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,29 @@
3232
"react": "^15.4.0 || ^16.0.0",
3333
"react-dom": "^15.4.0 || ^16.0.0",
3434
"react-test-renderer": "^15.6.1",
35-
"style-loader": "^0.18.2"
35+
"style-loader": "^0.18.2",
36+
"whatwg-fetch": "^2.0.3"
3637
},
37-
"files": ["lib"],
38+
"files": [
39+
"lib"
40+
],
3841
"homepage": "https://github.com/researchgate/react-intersection-list#readme",
39-
"keywords": ["Intersection", "Observer", "react", "component", "list", "infinite", "scrollable", "researchgate"],
42+
"keywords": [
43+
"Intersection",
44+
"Observer",
45+
"react",
46+
"component",
47+
"list",
48+
"infinite",
49+
"scrollable",
50+
"researchgate"
51+
],
4052
"license": "MIT",
4153
"lint-staged": {
42-
"{src,docs/docs}/**/*.js": ["eslint --fix", "git add"]
54+
"{src,docs/docs}/**/*.js": [
55+
"eslint --fix",
56+
"git add"
57+
]
4358
},
4459
"main": "lib/js/index.js",
4560
"module": "lib/es/index.js",
@@ -53,14 +68,14 @@
5368
},
5469
"jest": {
5570
"rootDir": "src",
56-
"testMatch": ["**/__tests__/**/*.spec.js"]
71+
"testMatch": [
72+
"**/__tests__/**/*.spec.js"
73+
]
5774
},
5875
"scripts": {
5976
"build": "rm -rf lib && npm run build:js && npm run build:es",
60-
"build:js":
61-
"cross-env BABEL_ENV=production BABEL_OUTPUT=cjs babel src --out-dir lib/js --ignore __tests__ --copy-files",
62-
"build:es":
63-
"cross-env BABEL_ENV=production BABEL_OUTPUT=esm babel src --out-dir lib/es --ignore __tests__ --copy-files",
77+
"build:js": "cross-env BABEL_ENV=production BABEL_OUTPUT=cjs babel src --out-dir lib/js --ignore __tests__ --copy-files",
78+
"build:es": "cross-env BABEL_ENV=production BABEL_OUTPUT=esm babel src --out-dir lib/es --ignore __tests__ --copy-files",
6479
"build:storybook": "build-storybook --output-dir docs",
6580
"format": "eslint --fix {src,docs/docs}/**/*.js",
6681
"lint": "eslint {src,docs/docs}/.",

0 commit comments

Comments
 (0)