Skip to content

Commit b1c5bc8

Browse files
committed
Implement basic version of lazy loading
1 parent 0981818 commit b1c5bc8

File tree

9 files changed

+293
-0
lines changed

9 files changed

+293
-0
lines changed

.babelrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"presets": ["env", "react"],
3+
"plugins": [
4+
"transform-object-rest-spread"
5+
]
6+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
build
3+
package-lock.json

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
React Lazy Load Image
2+
=====================
3+
4+
React Component to lazy load images using a HOC to track window scroll position.

package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "react-lazy-load-image",
3+
"version": "0.0.1",
4+
"description": " React Component to lazy load images using a HOC to track window scroll position. ",
5+
"main": "build/index.js",
6+
"dependencies": {
7+
"react": "^16.2.0"
8+
},
9+
"devDependencies": {
10+
"babel-cli": "^6.24.1",
11+
"babel-core": "^6.26.0",
12+
"babel-jest": "^22.4.0",
13+
"babel-loader": "^7.1.2",
14+
"babel-plugin-transform-object-rest-spread": "^6.26.0",
15+
"babel-preset-env": "^1.6.1",
16+
"babel-preset-react": "^6.24.1",
17+
"jest": "^22.4.0",
18+
"path": "^0.12.7",
19+
"react-dom": "^16.2.0",
20+
"webpack": "^3.11.0"
21+
},
22+
"scripts": {
23+
"test": "jest",
24+
"start": "webpack --watch",
25+
"build": "webpack"
26+
},
27+
"repository": {
28+
"type": "git",
29+
"url": "git+https://github.com/Aljullu/react-lazy-load-image.git"
30+
},
31+
"keywords": [
32+
"react",
33+
"lazyloading"
34+
],
35+
"author": {
36+
"name": "Albert Juhé Lluveras",
37+
"email": "contact@albertjuhe.com"
38+
},
39+
"license": "MIT",
40+
"bugs": {
41+
"url": "https://github.com/Aljullu/react-lazy-load-image/issues"
42+
},
43+
"homepage": "https://github.com/Aljullu/react-lazy-load-image#readme"
44+
}

src/components/LazyLoadImage.jsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React from 'react';
2+
import { PropTypes } from 'prop-types';
3+
4+
class LazyLoadImage extends React.Component {
5+
constructor(props) {
6+
super(props);
7+
8+
this.state = {
9+
visible: false
10+
}
11+
12+
this.previousBoundingBox = {
13+
bottom: -1,
14+
top: -1
15+
};
16+
}
17+
18+
componentDidMount() {
19+
this.updateVisibility();
20+
}
21+
22+
componentWillReceiveProps(nextProps) {
23+
this.updateVisibility(nextProps.scrollPosition);
24+
}
25+
26+
componentDidUpdate(prevProps, prevState) {
27+
if (this.refs.placeholder) {
28+
const boundingBox = {
29+
bottom: this.refs.placeholder.offsetTop +
30+
this.refs.placeholder.offsetHeight,
31+
top: this.refs.placeholder.offsetTop
32+
};
33+
34+
if (this.previousBoundingBox.bottom !== boundingBox.bottom ||
35+
this.previousBoundingBox.top !== boundingBox.top) {
36+
this.updateVisibility();
37+
}
38+
}
39+
}
40+
41+
updateVisibility(scrollPosition = this.props.scrollPosition) {
42+
if (this.state.visible) {
43+
return;
44+
}
45+
46+
this.setState({
47+
visible: this.refs.image ? true : this.isImageInViewport(scrollPosition)
48+
});
49+
}
50+
51+
isImageInViewport(scrollPosition) {
52+
if (!this.refs.placeholder) {
53+
return false;
54+
}
55+
56+
const { threshold } = this.props;
57+
const boundingBox = {
58+
bottom: this.refs.placeholder.offsetTop +
59+
this.refs.placeholder.offsetHeight,
60+
top: this.refs.placeholder.offsetTop
61+
};
62+
const viewport = {
63+
bottom: scrollPosition + window.innerHeight,
64+
top: scrollPosition
65+
};
66+
67+
this.previousBoundingBox = boundingBox;
68+
69+
return Boolean(viewport.top - threshold <= boundingBox.bottom &&
70+
viewport.bottom + threshold >= boundingBox.top);
71+
}
72+
73+
render() {
74+
const { scrollPosition, threshold, ...props } = this.props;
75+
76+
if (!this.state.visible) {
77+
const { className, height, width } = this.props;
78+
return (
79+
<span className={'lazy-load-image-placeholder ' + className}
80+
ref="placeholder"
81+
style={{height, width}}>
82+
</span>
83+
);
84+
}
85+
86+
return (
87+
<img
88+
{...props}
89+
ref="image" />
90+
);
91+
}
92+
}
93+
94+
LazyLoadImage.propTypes = {
95+
scrollPosition: PropTypes.number.isRequired,
96+
className: PropTypes.string,
97+
height: PropTypes.number,
98+
threshold: PropTypes.number,
99+
width: PropTypes.number
100+
};
101+
102+
LazyLoadImage.defaultProps = {
103+
className: '',
104+
height: 0,
105+
threshold: 100,
106+
width: 0
107+
};
108+
109+
export default LazyLoadImage;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import ReactTestUtils from 'react-dom/test-utils';
3+
import assert from 'assert';
4+
5+
import LazyLoadImage from './LazyLoadImage.jsx';
6+
7+
const {
8+
scryRenderedDOMComponentsWithClass,
9+
scryRenderedDOMComponentsWithTag
10+
} = ReactTestUtils;
11+
12+
describe('LazyLoadImage', function() {
13+
function renderLazyLoadImage(scrollPosition) {
14+
return ReactTestUtils.renderIntoDocument(
15+
<LazyLoadImage
16+
scrollPosition={scrollPosition}
17+
src="" />
18+
);
19+
}
20+
21+
function expectImages(wrapper, numberOfImages) {
22+
const img = scryRenderedDOMComponentsWithTag(wrapper, 'img');
23+
24+
expect(img.length).toEqual(numberOfImages);
25+
}
26+
27+
function expectPlaceholders(wrapper, numberOfPlaceholders) {
28+
const placeholder = scryRenderedDOMComponentsWithClass(wrapper,
29+
'lazy-load-image-placeholder');
30+
31+
expect(placeholder.length).toEqual(numberOfPlaceholders);
32+
}
33+
34+
it('renders the placeholder when it\'s not in the viewport', function() {
35+
const lazyLoadImage = renderLazyLoadImage(-1000);
36+
37+
expectImages(lazyLoadImage, 0);
38+
expectPlaceholders(lazyLoadImage, 1);
39+
});
40+
41+
it('renders the image when it\'s in the viewport', function() {
42+
const lazyLoadImage = renderLazyLoadImage(0);
43+
44+
expectImages(lazyLoadImage, 1);
45+
expectPlaceholders(lazyLoadImage, 0);
46+
});
47+
48+
it('renders the image when it appears in the viewport', function() {
49+
const lazyLoadImage = renderLazyLoadImage(-1000);
50+
51+
lazyLoadImage.componentWillReceiveProps({scrollPosition: 0});
52+
53+
expectImages(lazyLoadImage, 1);
54+
expectPlaceholders(lazyLoadImage, 0);
55+
});
56+
});

src/hocs/trackWindowScroll.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
3+
const trackWindowScroll = (BaseComponent) => {
4+
class ScrollAwareComponent extends React.Component {
5+
constructor(props) {
6+
super(props);
7+
8+
this.state = {
9+
scrollPosition: window.scrollY || window.pageYOffset
10+
};
11+
}
12+
13+
componentDidMount() {
14+
window.addEventListener('scroll', this.onChangeScroll.bind(this));
15+
window.addEventListener('resize', this.onChangeScroll.bind(this));
16+
}
17+
18+
componentWillUnmount() {
19+
window.removeEventListener('scroll', this.onChangeScroll.bind(this));
20+
window.removeEventListener('resize', this.onChangeScroll.bind(this));
21+
}
22+
23+
onChangeScroll() {
24+
this.setState({
25+
scrollPosition: window.scrollY || window.pageYOffset
26+
});
27+
}
28+
29+
render() {
30+
return (
31+
<BaseComponent
32+
scrollPosition={this.state.scrollPosition}
33+
{...this.props} />
34+
);
35+
}
36+
}
37+
38+
return ScrollAwareComponent;
39+
};
40+
41+
export default trackWindowScroll;
42+

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as LazyLoadImage } from './components/LazyLoadImage.jsx';
2+
export { default as trackWindowScroll } from './hocs/trackWindowScroll.js';

webpack.config.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
var path = require('path');
2+
module.exports = {
3+
entry: './src/index.js',
4+
output: {
5+
path: path.resolve(__dirname, 'build'),
6+
filename: 'index.js',
7+
libraryTarget: 'commonjs2'
8+
},
9+
module: {
10+
rules: [
11+
{
12+
test: /\.(js|jsx)$/,
13+
include: path.resolve(__dirname, 'src'),
14+
exclude: /(node_modules|bower_components|build)/,
15+
use: {
16+
loader: 'babel-loader',
17+
options: {
18+
presets: ['env']
19+
}
20+
}
21+
}
22+
]
23+
},
24+
externals: {
25+
'react': 'commonjs react'
26+
}
27+
};

0 commit comments

Comments
 (0)