Skip to content

Commit

Permalink
feat(prop_api_update): introducing API for 2.0.0
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
* Replace `currentLength` with more descriptive `itemCount` prop
* New render prop `renderItem` to completement `children` as render prop
* New prop `items` as a unique identifier/symbol to provide an easier integration of reusable lists
* TypeScript Module definitions
  • Loading branch information
Rendez authored and Luis Merino committed May 9, 2018
1 parent 3f80bb9 commit 276ca7b
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 194 deletions.
108 changes: 55 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ $ npm install --save intersection-observer
Next create a `<List>` and two instance methods as props `children` and `itemRenderer`:

```jsx
import React from 'react';
import React, { Component } from 'react';
import List from '@researchgate/react-intersection-list';

export default class MyList extends React.Component {
export default class InfiniteList extends Component {
itemsRenderer = (items, ref) => (
<ul className="list" ref={ref}>
{items}
Expand All @@ -49,18 +49,14 @@ export default class MyList extends React.Component {
itemRenderer = (index, key) => <li key={key}>{index}</li>;

render() {
return (
<List currentLength={1000} itemsRenderer={this.itemsRenderer}>
{this.itemRenderer}
</List>
);
return <List itemCount={1000} itemsRenderer={this.itemsRenderer} renderItem={this.itemRenderer} />;
}
}
```

Note that `<List>` is a `PureComponent` so it can keep itself from re-rendering. It's highly recommended to pass
referenced methods for `children` and `itemsRenderer` (in this case instance methods), so that it can successfully
shallow compare props.
Note that `<List>` is a `PureComponent` so it can keep itself from re-rendering. It's highly recommended to avoid
creating new functions for `renderItem` and `itemsRenderer` so that it can successfully shallow compare props on
re-render.

## Why React Intersection List?

Expand Down Expand Up @@ -93,59 +89,65 @@ property isn't found, then `window` will be used as the `root` instead.

The `sentinel` element is by default detached from the list when the current size reaches the available length, unless
you're using `awaitMore`. In case your list is in memory and you rely on the list for incremental rendering only, the
default detaching behavior suffices. If you're loading items asynchoronously on-demand, make sure to switch `awaitMore`
once you reach the total length.
default detaching behavior suffices. If you're loading more items in an asynchoronous way, make sure you switch
`awaitMore` once you reach the total length (bottom of the list).

### FAQ

Q: Why am I receiving too many `onIntersection` callbacks

We extend `React.PureComponent`, so IF the parent component re-renders, and the _props_ passed to your `<List />` don't
hold the same reference anymore, the list re-renders and may accidentally be re-attaching the `sentinel`.

Q: Do I always need to assign the `ref`?
The prop `itemCount` must be used if the prop `items` is not provided, and viceversa. Calculating the list size is done
by adding the current size and the page size until the items' length is reached.

Yes, this callback is used to start up the `IntersectionObserver`.

Q: What's the `threshold` value, and why does it need a _unit_?
### FAQ

The `threshold` value is the amount of space needed before the `sentinel` intersects with the root. The prop is
<details>
<summary>Why am I receiving too many `onIntersection` callbacks</summary>
We extend `PureComponent`. That means, if the parent component re-renders and the _props_ passed to your `<List />` don't
hold the same reference anymore, the list re-renders and we accidentally restart the `IntersectionObserver` of the `Sentinel`.
</details>
<br />
<details>
<summary>Do I always need to assign the `ref`?</summary>
Yes, the ref callback will be used as the `root` and is forwarded to the `IntersectionObserver` within the `Sentinel`.
</details>
<br />
<details>
<summary>What's the `threshold` value, and why does it need a _unit_?</summary>
The `threshold` value is the amount of space needed before the `sentinel` intersects with the root. The prop is
transformed into a valid `rootMargin` property for the `IntersectionObserver`, depending on the `axis` you select. As a
sidenote, we believe that a percentage unit works best for responsive layouts.

Q: I am getting a console warning when I first load the list

> The sentinel detected a viewport with a bigger size than the size of its items...
</details>
<br />
<details>
<summary>I am getting a console warning when I first load the list</summary>
<blockquote>The sentinel detected a viewport with a bigger size than the size of its items...</blockquote>
The prop `pageSize` is `10` by default, so make sure you're not falling short on items when you first render the
component. The idea of an infinite scrolling list is that items overflow the viewport, so that users have the impression
that there're always more items available.

Q: Why doesn't the list render my updated list element(s)?

The list renders items based on its props. An update somewhere else in your app (or within your list item) might update
component. The idea of an infinite scrolling list is that items overflow the viewport, so that users have the impression that there're always more items available.
</details>
<br />
<details>
<summary>Why doesn't the list render my updated list element(s)?</summary>
The list renders items based on its props. An update somewhere else in your app (or within your list item) might update
your list element(s), but if your list's `currentLength` prop for instance, remains unchanged, the list prevents a
re-render. Updating the entire infinite list when one of its items has changed is far from optimal. Instead, update your
list items independently using internal state or something like react-redux's connect().

Q: Are you planning to implement a "virtual list mode" like react-virtualized?

Yes, there's already an [open issue](https://github.com/researchgate/react-intersection-list/issues/2) to implement a
mode using occlusion culling.
re-render. Updating the entire infinite list when one of its items has changed is far from optimal. Instead, update each item individually with some form of `connect()` function or observables.
</details>
<br />
<details>
<summary>Are you planning to implement a "virtual list mode" like react-virtualized?</summary>
Yes, there's already an [open issue](https://github.com/researchgate/react-intersection-list/issues/2) to implement a
mode using occlusion culling. It will be implemented in a future release. If you can't wait, you could help us out by opening a Pull Request :)
</details>

### Props

| property | type | default | description |
| ---------------- | ------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `children` | `(index: number, key: number) => React.Element` | `(index, key) => <div key={key}>{index}</div>` | render function as children;<br />gets call once for each item. |
| `itemsRenderer` | `(items: Array(React.Element), ref: HTMLElement) => React.Element` | `(items, ref) => <div ref={ref}>{items}</div>` | render function for the list's<br />root element, often returning a scrollable element. |
| `currentLength` | `number` | `0` | item count to render. |
| `awaitMore` | `boolean` | | if true keeps the sentinel from detaching. |
| `onIntersection` | `(size: number, pageSize: number) => void` | | invoked when the sentinel comes into view. |
| `threshold` | `string` | `100px` | value in absolute `px` or `%`<br />as spacing before the sentinel hits the edge of the list's viewport. |
| `axis` | `string` | `y` | scroll direction: `y` == vertical and `x` == horizontal |
| `pageSize` | `number` | `10` | number of items to render each hit. |
| `initialIndex` | `number` | `0` | start position of iterator of items. |
| property | type | default | description |
| --------------------- | ------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `renderItem|children` | `(index: number, key: number) => React.Element` | `(index, key) => <div key={key}>{index}</div>` | render function as children or render props;<br />gets call once for each item. |
| `itemsRenderer` | `(items: Array(React.Element), ref: HTMLElement) => React.Element` | `(items, ref) => <div ref={ref}>{items}</div>` | render function for the list's<br />root element, often returning a scrollable element. |
| `itemCount|items` | `number | Array (or Iterable Object)` | `0` | item count to render. |
| `awaitMore` | `boolean` | | if true keeps the sentinel from detaching. |
| `onIntersection` | `(size: number, pageSize: number) => void` | | invoked when the sentinel comes into view. |
| `threshold` | `string` | `100px` | value in absolute `px` or `%`<br />as spacing before the sentinel hits the edge of the list's viewport. |
| `axis` | `string` | `y` | scroll direction: `y` == vertical and `x` == horizontal |
| `pageSize` | `number` | `10` | number of items to render each hit. |
| `initialIndex` | `number` | `0` | start position of iterator of items. |

### Examples

Expand Down
11 changes: 5 additions & 6 deletions docs/docs/components/AsyncList/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import React, { Component } from 'react';
import List from '../../../../src';

const PAGE_SIZE = 20;

export default class extends React.Component {
export default class AsyncList extends Component {
state = {
awaitMore: true,
isLoading: false,
Expand Down Expand Up @@ -89,12 +89,11 @@ export default class extends React.Component {
<List
awaitMore={this.state.awaitMore}
itemsRenderer={this.renderItems}
currentLength={this.state.repos.length}
itemCount={this.state.repos.length}
onIntersection={this.handleLoadMore}
pageSize={PAGE_SIZE}
>
{this.renderItem}
</List>
renderItem={this.renderItem}
/>
</div>
);
}
Expand Down
4 changes: 3 additions & 1 deletion docs/docs/components/Axis/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ const itemsRenderer = (items, ref) => (
);

// eslint-disable-next-line react/no-multi-comp
export default () => <List axis="x" currentLength={Infinity} itemsRenderer={itemsRenderer} pageSize={40} />;
const Axis = () => <List axis="x" itemCount={Infinity} itemsRenderer={itemsRenderer} pageSize={40} />;

export default Axis;
4 changes: 3 additions & 1 deletion docs/docs/components/InfiniteList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ const itemsRenderer = (items, ref) => (
);

// eslint-disable-next-line react/no-multi-comp
export default () => <List currentLength={Infinity} itemsRenderer={itemsRenderer} pageSize={40} />;
const InfiniteList = () => <List itemCount={Infinity} itemsRenderer={itemsRenderer} pageSize={40} />;

export default InfiniteList;
34 changes: 18 additions & 16 deletions docs/recipes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

### Asynchonous Repo List

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 pass the prop `awaitMore:bool = true`, so that the sentinel awaits for more items.
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 pass the prop `awaitMore:bool =
true`, so that the sentinel awaits for more items.

```jsx
import React from 'react';
Expand All @@ -18,7 +21,7 @@ export default class extends React.Component {
repos: [],
};

feedList = (repos) => {
feedList = repos => {
this.setState({
awaitMore: repos.length > 0,
isLoading: false,
Expand All @@ -36,7 +39,7 @@ export default class extends React.Component {

const url = 'https://api.github.com/users/researchgate/repos';
const qs = `?type=public&per_page=${PAGE_SIZE}&page=${currentPage}`;

fetch(url + qs)
.then(response => response.json())
.then(this.feedList)
Expand Down Expand Up @@ -67,48 +70,47 @@ export default class extends React.Component {
<List
awaitMore={this.state.awaitMore}
itemsRenderer={this.renderItems}
currentLength={this.state.repos.length}
itemCount={this.state.repos.length}
onIntersection={this.handleLoadMore}
pageSize={PAGE_SIZE}
>
{this.renderItem}
</List>
renderItem={this.renderItem}
/>
</div>
);
}
}
```

If the total amount of items are prefetched and available, we won't need `awaitMore` and the `pageSize` will be used to paginate results until we reach the bottom of the list.
If the total amount of items are prefetched and available, we won't need `awaitMore` and the `pageSize` will be used to
paginate results until we reach the bottom of the list.

### Infinite Synchronous List

```jsx
import React from 'react';
import List from '@researchgate/react-intersection-list';

export default () => (
<List currentLength={Infinity}>
{(index, key) => <div key={key}>{index}</div>}
</List>
);
export default () => <List itemCount={Infinity}>{(index, key) => <div key={key}>{index}</div>}</List>;
```

### Can I submit a new recipe?

Yes, of course!

1. Fork the code repo.
2. Create your new recipe in the correct subfolder within `./docs/docs/components/` (create a new folder if it doesn't already exist).
2. Create your new recipe in the correct subfolder within `./docs/docs/components/` (create a new folder if it doesn't
already exist).
3. Make sure you have included a README as well as your source file.
4. Submit a PR.

_If you haven't yet, please read our [contribution guidelines](https://github.com/researchgate/react-intersection-list/blob/master/.github/CONTRIBUTING.md)._
_If you haven't yet, please read our
[contribution guidelines](https://github.com/researchgate/react-intersection-list/blob/master/.github/CONTRIBUTING.md)._

### What license are the recipes released under?

By default, all newly submitted code is licensed under the MIT license.

### How else can I contribute?

Recipes don't always have to be code - great documentation, tutorials, general tips and even general improvements to our examples folder are greatly appreciated.
Recipes don't always have to be code - great documentation, tutorials, general tips and even general improvements to our
examples folder are greatly appreciated.
44 changes: 15 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,23 @@
"lint-staged": "^7.0.0",
"prettier": "^1.8.2",
"raf": "^3.4.0",
"react": "^16.1.1",
"react-dom": "^16.1.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-test-renderer": "^16.1.1",
"rimraf": "^2.6.1",
"standard-version": "^4.2.0",
"style-loader": "^0.21.0",
"style-loader": "^0.19.0",
"typescript": "^2.6.2",
"validate-commit-msg": "^2.14.0",
"whatwg-fetch": "^2.0.3"
},
"files": [
"lib"
],
"files": ["lib", "types/index.d.ts"],
"types": "types/index.d.ts",
"homepage": "https://github.com/researchgate/react-intersection-list#readme",
"keywords": [
"Intersection",
"Observer",
"react",
"component",
"list",
"infinite",
"scrollable",
"researchgate"
],
"keywords": ["Intersection", "Observer", "react", "component", "list", "infinite", "scrollable", "researchgate"],
"license": "MIT",
"lint-staged": {
"{src,docs/docs}/**/*.js": [
"eslint --fix",
"git add"
]
"{src,docs/docs}/**/*.js": ["eslint --fix", "git add"]
},
"main": "lib/js/index.js",
"module": "lib/es/index.js",
Expand All @@ -76,24 +64,22 @@
},
"jest": {
"rootDir": "src",
"testMatch": [
"**/__tests__/**/*.spec.js"
],
"setupFiles": [
"raf/polyfill"
]
"testMatch": ["**/__tests__/**/*.spec.js"],
"setupFiles": ["raf/polyfill"]
},
"scripts": {
"build": "npm run build:js && npm run build:es",
"build:js": "cross-env BABEL_ENV=production BABEL_OUTPUT=cjs babel src --out-dir lib/js --ignore __tests__ --copy-files",
"build:es": "cross-env BABEL_ENV=production BABEL_OUTPUT=esm babel src --out-dir lib/es --ignore __tests__ --copy-files",
"build:js":
"cross-env BABEL_ENV=production BABEL_OUTPUT=cjs babel src --out-dir lib/js --ignore __tests__ --copy-files",
"build:es":
"cross-env BABEL_ENV=production BABEL_OUTPUT=esm babel src --out-dir lib/es --ignore __tests__ --copy-files",
"build:storybook": "build-storybook --output-dir docs",
"create-github-release": "conventional-github-releaser -p angular",
"clear": "rimraf ./lib",
"commitmsg": "validate-commit-msg",
"coverage": "yarn test -- --coverage",
"format": "eslint --fix {src,docs/docs}/**/*.js",
"lint": "eslint {src,docs/docs}/.",
"lint": "eslint {src,docs/docs}/. && tsc --project types",
"precommit": "yarn lint-staged && yarn test",
"prepare": "yarn clear && yarn build",
"prepublishOnly": "yarn test",
Expand Down
Loading

0 comments on commit 276ca7b

Please sign in to comment.