Skip to content

Commit fc7d1f4

Browse files
author
fpapado
committed
Feat: support srcSet preload, no src patterns
1 parent 25b27b0 commit fc7d1f4

File tree

3 files changed

+99
-29
lines changed

3 files changed

+99
-29
lines changed

README.md

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@
3434
* Easy to understand source code. You should be able to fork and do your thing if desired.
3535
* Ample documentation to help you understand the problem, in addition to the solutions.
3636

37-
What it does not do by itself:
37+
What it does not do out:
3838

3939
* Polyfill `IntersectionObserver`. Adding polyfills is something you should do consciously at the application level. See [Polyfilling IntersectionObserver](#polyfill-intersectionobserver) for how to do this.
4040
* Dictate the kind of placeholders displayed. There are many ways to do it; you can use a simple box with a background color (I hear gray is popular), a blurred image, some gradient, or anything you'd like. You are in control of the element that gets rendered.
41-
* Animate transitions between placeholder and source. Again, you are in control of the containers, so it is possible to implement those at the consumer.
41+
* Animate transitions between placeholder and source. Again, you are in control of the containers, so it should be possible to implement those at the consumer.
4242

4343
In other words, this library focuses on loading the images once in view and supporting loading patterns around that.
4444
The actual components are yours to decide!
@@ -58,10 +58,10 @@ Then import according to your modules model and bundler, such as [Rollup](https:
5858
```js
5959
// ES Modules
6060
// For all possible functions to import look at the documentation
61-
import { LazyImage } from "react-lazy-images";
61+
import {LazyImage} from 'react-lazy-images';
6262

6363
/// CommonJS modules
64-
const { LazyImage } = require("react-lazy-images");
64+
const {LazyImage} = require('react-lazy-images');
6565
```
6666

6767
A [UMD](https://github.com/umdjs/umd) version is also available on [unpkg](https://unpkg.com/):
@@ -102,14 +102,14 @@ Using this API is not specific to React; it just seems like a good fit for this
102102
If you want to just dive in, do this:
103103

104104
```jsx
105-
import { LazyImage } from "react-lazy-images";
105+
import {LazyImage} from 'react-lazy-images';
106106

107107
<LazyImage
108108
src="https://www.fillmurray.com/g/600/400"
109-
placeholder={({ cls }) => (
109+
placeholder={({cls}) => (
110110
<img src="https://www.fillmurray.com/g/60/40" className={cls} />
111111
)}
112-
actual={({ cls }) => (
112+
actual={({cls}) => (
113113
<img src="https://www.fillmurray.com/g/600/400" className={cls} />
114114
)}
115115
/>;
@@ -170,7 +170,7 @@ A common optimisation to the loading strategy is to preload the image before swa
170170
In other words, once the image is in view, you can kick off a request to load the image, and only show it once fully loaded.
171171
This avoids presenting a half-loaded image (i.e. one that is still scanning top-to-bottom), and makes the transition smoother.
172172
173-
This behaviour is provided by default:
173+
This behaviour is provided with the `src` prop:
174174
175175
```jsx
176176
// Note that the actual src is also provided separately,
@@ -192,6 +192,8 @@ This behaviour is provided by default:
192192
/>
193193
```
194194
195+
There is another case if you are using `srcset` for your images; `LazyImage` needs that information to preload the correct image. You can provide it with the `srcSet` prop.
196+
195197
### Loading and Error states
196198
197199
You can choose what to display on Loading and Error using the render props `loading` and `error`:
@@ -200,16 +202,16 @@ You can choose what to display on Loading and Error using the render props `load
200202
<div className="bg-light-silver h5 w-100">
201203
<LazyImage
202204
src="https://www.fillmurray.com/notanimage"
203-
placeholder={({ cls }) => <div className={cls} />}
204-
actual={({ cls }) => (
205+
placeholder={({cls}) => <div className={cls} />}
206+
actual={({cls}) => (
205207
<img src="https://www.fillmurray.com/notanimage" className={cls} />
206208
)}
207-
loading={({ cls }) => (
209+
loading={({cls}) => (
208210
<div className={cls}>
209211
<p className="pa3 f5 lh-copy near-white">Loading...</p>
210212
</div>
211213
)}
212-
error={({ cls }) => (
214+
error={({cls}) => (
213215
<div className={`bg-light-red h-100 w-100 ${cls}`}>
214216
<p>There was an error fetching this image :(</p>
215217
</div>
@@ -323,7 +325,7 @@ npm install --save intersection-observer
323325
And import it at your app's entry point:
324326
325327
```js
326-
import "intersection-observer";
328+
import 'intersection-observer';
327329
```
328330
329331
[Polyfill.io is an alternative method of distributing the polyfill](polyfill.io) if you wish.
@@ -338,21 +340,25 @@ It will not be as performant as the native IntersectionObserver, but likely no w
338340
## Examples
339341
340342
A variety of usage examples and recipes is provided in the form of storybook.
343+
341344
[You can browse the documentation online](https://fpapado.github.io/react-lazy-images) or look at `stories/`.
342345
346+
Read the notes section either on Storybook or the story source if you are wondering about the specifics of each pattern demonstrated.
347+
343348
## API Reference
344349
345350
**`<LazyImage />`** accepts the following props:
346351
347-
| Name | Type | Default | Required | Description |
348-
| ----------------- | --------------------------------------- | ----------------------------------------- | -------- | ---------------------------------------------------------------------------- |
349-
| **src** | String | | true | The source of the image to load |
350-
| **placeholder** | Function | | true | Placeholder component to display while image has not loaded |
351-
| **actual** | Function | | true | The component to display once image has loaded |
352-
| **loading** | Function | placeholder | false | The component to display while the image is loading |
353-
| **error** | Function | placeholder | false | The component to display if the image loading has failed |
354-
| **loadEagerly** | Boolean | false | false | Whether to skip checking for viewport and always show the 'actual' component |
355-
| **observerProps** | {threshold: number, rootMargin: string} | {threshold: 0.01, rootMargin: "50px 0px"} | false | Subset of props for the IntersectionObserver |
352+
| Name | Type | Default | Required | Description |
353+
| ----------------- | --------------------------------------- | ----------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
354+
| **src** | String | | true | The source of the image to load |
355+
| **srcSet** | String | | false | If your images use srcset, you can pass the `srcSet` prop to provide that information for preloading. |
356+
| **placeholder** | Function (render prop) | | true | Component to display while image has not loaded |
357+
| **actual** | Function (render prop) | | true | Component to display once image has loaded |
358+
| **loading** | Function (render prop) | placeholder | false | Component to display while the image is loading |
359+
| **error** | Function | placeholder | false | Component to display if the image loading has failed (render prop) |
360+
| **loadEagerly** | Boolean | false | false | Whether to skip checking for viewport and always show the 'actual' component |
361+
| **observerProps** | {threshold: number, rootMargin: string} | {threshold: 0.01, rootMargin: "50px 0px"} | false | Subset of props for the IntersectionObserver |
356362
357363
[You can consult Typescript types in the code](./src/LazyImage.tsx) as a more exact definition.
358364

src/LazyImage.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface RenderPropArgs {
1414
export interface LazyImageProps {
1515
/** The source of the image to load */
1616
src: string;
17+
srcSet: string;
1718
/** The component to display while image has not loaded */
1819
placeholder: (RenderPropArgs) => React.ReactElement<{}>;
1920

@@ -87,12 +88,21 @@ export class LazyImage extends React.Component<LazyImageProps, LazyImageState> {
8788
// Update functions
8889
onInView(inView) {
8990
if (inView) {
90-
// Kick off request for Image and attach listeners for response
91-
this.setState((state, props) => ({...state, imageState: 'Loading'}));
92-
93-
loadImage(this.props.src)
94-
.then(this.onLoadSuccess)
95-
.catch(this.onLoadError);
91+
// If src is not specified, then there is nothing to preload; skip to Loaded state
92+
// TODO: alternatively, we could have a sort of timeout here?
93+
if (!this.props.src) {
94+
this.setState((state, props) => ({
95+
...state,
96+
imageState: 'LoadSuccess'
97+
}));
98+
} else {
99+
// Kick off request for Image and attach listeners for response
100+
this.setState((state, props) => ({...state, imageState: 'Loading'}));
101+
102+
loadImage({src: this.props.src, srcSet: this.props.srcSet})
103+
.then(this.onLoadSuccess)
104+
.catch(this.onLoadError);
105+
}
96106
}
97107
}
98108

@@ -167,9 +177,12 @@ export class LazyImage extends React.Component<LazyImageProps, LazyImageState> {
167177
using the img element per se. Should we use an explicit fetch(), perhaps?
168178
*/
169179
/** Promise constructor for loading an image */
170-
const loadImage = src =>
180+
const loadImage = ({src, srcSet}) =>
171181
new Promise((resolve, reject) => {
172182
const image = new Image();
183+
if (srcSet) {
184+
image.srcset = srcSet;
185+
}
173186
image.src = src;
174187
image.onload = resolve;
175188
image.onerror = reject;

stories/LazyImage.story.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,57 @@ storiesOf('LazyImage', module)
6464
</Container>
6565
))
6666
)
67+
// With srcSet
68+
.add(
69+
'With src and srcSet',
70+
withInfo(
71+
'With srcset, the browser decides which image to load. In that case, src is not informative enough for preloading. You can pass the `srcSet` prop to provide that additional information to LazyImage.'
72+
)(() => (
73+
<Container>
74+
<LazyImage
75+
src="https://www.fillmurray.com/g/300/200"
76+
srcSet="https://www.fillmurray.com/g/900/600 900w, https://www.fillmurray.com/g/600/400 600w, https://www.fillmurray.com/g/300/200 300w"
77+
placeholder={({cls}) => (
78+
<img
79+
src="https://www.fillmurray.com/g/60/40"
80+
className={`${cls} w-100`}
81+
/>
82+
)}
83+
actual={({cls}) => (
84+
<img
85+
src="https://www.fillmurray.com/g/300/200"
86+
srcSet="https://www.fillmurray.com/g/900/600 900w, https://www.fillmurray.com/g/600/400 600w, https://www.fillmurray.com/g/300/200 300w"
87+
className={`${cls} w-100`}
88+
/>
89+
)}
90+
/>
91+
</Container>
92+
))
93+
)
94+
// With srcSet
95+
.add(
96+
'Without src or srcSet',
97+
withInfo(
98+
'Sometimes, it might be impractical to specify the src with your current setup. For example, it is possible that you are generating the sources for an Image CDN and have a dedicated component for it. In those cases, changing the component might be impractical in the short-term. If you provide no src or srcSet, then the preload-before-swap behaviour is not used. We believe that showing a possibly still-downloading image is better than having lazy-loading at all.'
99+
)(() => (
100+
<Container>
101+
<LazyImage
102+
placeholder={({cls}) => (
103+
<img
104+
src="https://www.fillmurray.com/g/60/40"
105+
className={`${cls} w-100`}
106+
/>
107+
)}
108+
actual={({cls}) => (
109+
<img
110+
src="https://www.fillmurray.com/g/600/400"
111+
className={`${cls} w-100`}
112+
/>
113+
)}
114+
/>
115+
</Container>
116+
))
117+
)
67118
// Always load an image (aka "eagerly"; how the browser does it already.
68119
// Useful if you want to load the actual content without waiting for Javascript.
69120
.add(

0 commit comments

Comments
 (0)