├── LICENSE
├── README.md
├── index.js
└── package.json
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Josh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED
2 |
3 | Please use something like [react-virtualized](https://github.com/bvaughn/react-virtualized) for more efficient and supported features like this library provided
4 |
5 |
6 |
7 | # react-everscroll
8 | Performant large lists and infinite scrolling in React.
9 |
10 | **Large Lists** are problematic because the browser will render every dom element it is given, even if the element is hidden, consequently large lists in DOM will destroy browser performance. Everscroll solves this by rendering only a portion of your list at any given time, and recycling dom elements to render new list rows as you scroll.
11 |
12 | **Infinite Scrolling** becomes trivial, simply add new data to the index prop. Everscroll will invoke `onEndReached` prop so you know when to query for more data.
13 |
14 | ## Dependencies
15 | Assumes use of Babel or similar tool for ES6 to 5 transpiling and JSX compilation
16 | Component also currently depends on Immutable and lodash though these could be removed with a little effort (pull request if interested)
17 |
18 | ## Usage
19 | ```js
20 | function renderRow(ID){
21 | return
{ID}
22 | }
23 |
24 | } // content will be placed at the end of your list
31 | frontCap={} // frontCap will be placed at the start of your list
32 | />
33 | ```
34 |
35 | ## Yet Another . . .
36 | There are a number of well made react infinite list components. While we have not tried them all, most have some limitations such as requiring fixed height rows, poor performance, or not having a "reverse" mode.
37 |
38 | Everscroll seeks to address all of these issues with a small and intuitive api.
39 |
40 | If you have a use case that is not covered by the current API, make a feature request!
41 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | //@TODO remove lodash (currently used for throttling)
3 | var _ = require('lodash');
4 | var Immutable = require('immutable');
5 |
6 | var Everscroll = React.createClass({
7 |
8 | /**
9 | * Baseline State
10 | * @type {Object}
11 | */
12 | _initialState: {
13 | listOffset: 0,
14 | keyStart: 0,
15 | frontHeight: 0,
16 | backHeight: 0,
17 | },
18 |
19 | /**
20 | * Transient State
21 | */
22 | _prependOffset: 0,
23 | _targetCursorOffset: 0,
24 | _initScroll: true,
25 | _scrollAfterUpdate: false,
26 | _seekFront: false,
27 |
28 | /**
29 | * reset container scroll to top or bottom depending on direction
30 | */
31 | _initScrollTop: function () {
32 | this._initScroll = false;
33 | var containerEl = this.refs.root.getDOMNode();
34 | if (this.props.reverse) {
35 | containerEl.scrollTop = containerEl.scrollHeight - containerEl.clientHeight;
36 | } else {
37 | containerEl.scrollTop = 0;
38 | }
39 | },
40 |
41 | /**
42 | * determine ref name for cursor (middlemost rendered element on the screen)
43 | * @return {String}
44 | */
45 | _calcCursorRef: function() {
46 |
47 | //get container
48 | var containerEl = this.refs.root.getDOMNode();
49 | var threshold = containerEl.scrollTop + containerEl.clientHeight / 2;
50 |
51 | var clearThreshold = (ref) => {
52 | var node = this.refs[ref].getDOMNode();
53 | //If reverse subtract offsetheight (so threshold relative to bottom of node instead of top)
54 | return node.offsetTop > threshold - (this.props.reverse ? node.offsetHeight : 0);
55 | }
56 |
57 | var refRange = this._getRefRenderRange();
58 |
59 | var cursorRef =
60 | refRange
61 | .filter(ref => this.refs[ref])
62 | .takeUntil(clearThreshold)
63 | .last()
64 | ||
65 | refRange.first();
66 |
67 | return cursorRef;
68 |
69 | },
70 |
71 | /**
72 | * determine if new cursor is needed and set if necessary
73 | */
74 | _setCursor: function() {
75 |
76 | var cursorRef = this._calcCursorRef();
77 |
78 | if (this.state.cursorRef === cursorRef) {
79 | return;
80 | }
81 |
82 | this.setState({
83 | cursorRef: cursorRef,
84 | });
85 | },
86 |
87 | /**
88 | * get renderable ref Range in incremental order
89 | * Front of list to Back of list
90 | * @return {Immutable.Seq}
91 | */
92 | _getRefRange: function () {
93 | var rangeStart = this.state.listOffset;
94 | var rangeEnd = rangeStart + Math.min(this.props.renderCount, this.props.rowIndex.length);
95 |
96 | return Immutable.Range(rangeStart, rangeEnd)
97 | },
98 |
99 | /**
100 | * get renderable ref Range in render order
101 | * Top of container to Bottom of container
102 | * @return {Immutable.Seq}
103 | */
104 | _getRefRenderRange: function() {
105 | var refRange = this._getRefRange();
106 |
107 | return this.props.reverse ? refRange.reverse() : refRange
108 | },
109 |
110 | /**
111 | * recycled key Range for DOM reuse
112 | * @return {Immutable.List}
113 | */
114 | //@TODO determine if keys provide any real performance benefit
115 | _getKeyList: function() {
116 | var keyStart = this.state.keyStart;
117 | var basicRange = Immutable.Range(0, this.props.renderCount);
118 |
119 | return basicRange.slice(keyStart).toList().concat(basicRange.slice(0, keyStart).toList())
120 | },
121 |
122 | /**
123 | * call renderRowHanlder for given rowIndex value (ID)
124 | */
125 | _renderRow: function(ID, index){
126 | return this.props.renderRowHandler(ID, index);
127 | },
128 |
129 | _onEndReached: function() {
130 | setTimeout(this.props.onEndReached, 0)
131 | },
132 |
133 | /**
134 | * determine if render range needs to shift and update state accordingly
135 | */
136 | _handleScroll: function(){
137 |
138 | var {reverse, renderCount, rowIndex} = this.props
139 | var {listOffset, keyStart} = this.state
140 |
141 | if (rowIndex.length < renderCount) return;
142 |
143 | var cursorRef = this._calcCursorRef()
144 | var impliedListOffset = (cursorRef - renderCount / 2) | 0
145 |
146 | var minOffset = 0
147 | var maxOffset = Math.max(0, rowIndex.length - renderCount)
148 |
149 | var adjustedOffset = Math.min(Math.max(impliedListOffset, minOffset), maxOffset);
150 | var offsetShift = adjustedOffset - listOffset
151 | var adjustedKeyStart = (keyStart + offsetShift) % renderCount
152 |
153 | if (adjustedOffset === maxOffset && offsetShift > 0) {
154 | this._onEndReached()
155 | }
156 |
157 | var renderRange = this._getRefRenderRange();
158 | var refRange = this._getRefRange();
159 |
160 | //@TODO come up with a better way seed top and bottom spacesrs and maintain consistency during the scroll
161 | // consider caching rendered row heights rather than approximating
162 | var averageHeight = this.refs.renderRows.getDOMNode().scrollHeight / renderRange.count()
163 |
164 | var shift = adjustedOffset - listOffset
165 | var backSpacerHeight = (rowIndex.length - refRange.last() - 1 - offsetShift) * averageHeight
166 | var frontSpacerHeight = (refRange.first() + offsetShift) * averageHeight
167 |
168 | this.setState({
169 | cursorRef: cursorRef,
170 | listOffset: adjustedOffset,
171 | keyStart: adjustedKeyStart,
172 | frontHeight: frontSpacerHeight,
173 | backHeight: backSpacerHeight,
174 | })
175 | },
176 |
177 | propTypes: {
178 | rowIndex: React.PropTypes.array.isRequired,
179 | idKey: React.PropTypes.string,
180 | renderRowHandler: React.PropTypes.func,
181 | reverse: React.PropTypes.bool,
182 | renderCount: React.PropTypes.number,
183 | onEndReached: React.PropTypes.func,
184 | topCap: React.PropTypes.object,
185 | bottomCap: React.PropTypes.object
186 | },
187 |
188 | getDefaultProps: function() {
189 | return {
190 | idKey: 'ID',
191 | renderRowHandler: function(ID){
192 | return ({ID}
)
193 | },
194 | reverse: false,
195 | renderCount: 30,
196 | throttle: 16,
197 | loadBuffer: 30,
198 | onEndReached: function () {}
199 | }
200 | },
201 |
202 | getInitialState: function() {
203 | return this._initialState;
204 | },
205 |
206 | componentWillReceiveProps: function(nextProps){
207 | var oldRowIndex = this.props.rowIndex
208 | var newRowIndex = nextProps.rowIndex
209 |
210 | // reset scroll and render if we go from now rows to rows
211 | if(oldRowIndex && oldRowIndex.length === 0 && newRowIndex && newRowIndex.length > 0){
212 | this.setState(this._initialState);
213 | this._initScroll = true
214 | }
215 |
216 | // detect prepend and adjust listOffset if necessary
217 | else if (oldRowIndex[0] !== newRowIndex[0]) {
218 | var prependedMessageCount =
219 | Immutable
220 | .List(newRowIndex)
221 | .takeUntil(function (ref) {
222 | return oldRowIndex[0] === ref
223 | })
224 | .count()
225 |
226 | //@TODO discuss, how best to seek front, and how to set threshold
227 | var containerEl = this.refs.root.getDOMNode()
228 | var seekFrontThreshold = 20
229 | if( (!this.props.reverse && containerEl.scrollTop < seekFrontThreshold) || (this.props.reverse && containerEl.scrollTop + containerEl.offsetHeight > containerEl.scrollHeight-seekFrontThreshold) ){
230 | this._seekFront = true
231 | }
232 |
233 | //@TODO should detect if old message root is not in new row index and handle...
234 | this.setState({
235 | listOffset: this.state.listOffset + (this._seekFront ? 0 : prependedMessageCount),
236 | cursorRef: this.state.cursorRef + prependedMessageCount,
237 | })
238 |
239 | this._prependOffset = prependedMessageCount
240 |
241 |
242 |
243 | }
244 |
245 | else if (oldRowIndex[oldRowIndex.length - 1] !== newRowIndex[newRowIndex.length - 1]) {
246 | this.setState({listOffset: this.state.listOffset + 1})
247 | }
248 |
249 | //@TODO more elegant handling?
250 | //this._scrollAfterUpdate = true
251 |
252 | if (this.props.renderCount >= newRowIndex.length) {
253 | process.nextTick(this.props.onEndReached)
254 | }
255 |
256 | },
257 |
258 | shouldComponentUpdate: function(nextProps, nextState){
259 | //@TODO: implement to prevent uncessary renders on prop changes
260 | return true
261 | },
262 |
263 | componentDidMount: function() {
264 | this._handleScroll = _.throttle(this._handleScroll, this.props.throttle)
265 | this._initScrollTop()
266 | },
267 |
268 | componentWillUpdate: function(nextProps, nextState){
269 | if (nextState.cursorRef){
270 | var containerEl = this.getDOMNode()
271 | var refOffset = this._prependOffset || 0
272 | this._prependOffset = 0;
273 | this._targetCursorOffset = this.refs[nextState.cursorRef - refOffset].getDOMNode().offsetTop - containerEl.scrollTop
274 | }
275 | },
276 |
277 | componentDidUpdate: function(prevProps, prevState){
278 |
279 | if (this.state.cursorRef){
280 | var containerEl = this.getDOMNode();
281 | var currentCursorOffset = this.refs[this.state.cursorRef].getDOMNode().offsetTop - containerEl.scrollTop
282 | var adjustment = currentCursorOffset - this._targetCursorOffset
283 | containerEl.scrollTop += adjustment
284 | }
285 |
286 | if (this._initScroll) {
287 | this._initScrollTop();
288 | }
289 |
290 | //@TODO possible only call when requested instead of every update
291 | if(this._scrollAfterUpdate){
292 | this._handleScroll()
293 | this._scrollAfterUpdate = false
294 | }
295 |
296 | if(this._seekFront){
297 | containerEl.scrollTop = this.props.reverse ? containerEl.scrollHeight - containerEl.offsetHeight : 0
298 | this._seekFront = false
299 | }
300 |
301 | this._setCursor()
302 | },
303 |
304 | render: function() {
305 | var {reverse, frontCap, backCap} = this.props
306 | var {backHeight, frontHeight} = this.state
307 |
308 | var refRange = this._getRefRange()
309 |
310 | var rows = this.props.rowIndex.slice(refRange.first(), refRange.last() + 1)
311 | var refs = this._getRefRenderRange().toArray()
312 | var keys = this._getKeyList().map(val => "everscroll-" + val).toArray()
313 |
314 | var [topSpacerHeight, bottomSpacerHeight] = reverse
315 | ? [backHeight, frontHeight]
316 | : [frontHeight, backHeight]
317 |
318 | if (reverse) {
319 | rows.reverse();
320 | }
321 |
322 | var renderRows = rows.map((ID, index) =>{
323 | return (
324 |
325 | {this._renderRow(ID, index)}
326 |
327 | )
328 | })
329 |
330 | return (
331 |
332 |
333 | {reverse ? backCap : frontCap}
334 |
335 |
336 |
337 | {renderRows}
338 |
339 |
340 |
341 | {!reverse ? backCap : frontCap}
342 |
343 |
344 | )
345 | }
346 |
347 | })
348 |
349 | module.exports = Everscroll
350 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-everscroll",
3 | "version": "0.0.4",
4 | "description": "Performant large lists and infinite scrolling in React.",
5 | "main": "index.js",
6 | "dependencies": {
7 | "immutable": "^3.6.2",
8 | "lodash": "^3.1.0"
9 | },
10 | "peerDependencies": {
11 | "react": "0.12.x || >=0.13.0-beta.1"
12 | },
13 | "devDependencies": {},
14 | "scripts": {
15 | "test": "echo \"Error: no test specified\" && exit 1"
16 | },
17 | "author": "gnoff ",
18 | "maintainers": [
19 | {
20 | "name": "gnoff",
21 | "email": "josh@root-two.com"
22 | },
23 | {
24 | "name": "rt2zz",
25 | "email": "zack@root-two.com"
26 | }
27 | ],
28 | "license": "MIT",
29 | "repository": {
30 | "type": "git",
31 | "url": "https://github.com/gnoff/react-everscroll.git"
32 | },
33 | "keywords": [
34 | "react",
35 | "reactjs",
36 | "infinite",
37 | "everscroll"
38 | ],
39 | "bugs": {
40 | "url": "https://github.com/gnoff/react-everscroll/issues"
41 | },
42 | "homepage": "https://github.com/gnoff/react-everscroll"
43 | }
44 |
--------------------------------------------------------------------------------