Skip to content

Commit a68ffac

Browse files
authored
Feature: Debounced/delayed Loading (#11)
* Add utils and delayed loading test in stories * Intermediate solution Not sure where the reducer/update function is headed, but we'll find out, I suppose. * Finish refactoring * Make buffering work It works, but now the intersection logic is not the same. Scrolling until inView is true, and then up, triggers the promise cancelation. This is undesirable. We want the image to keep loading as long as it stays on the screen. Oddly, scrolling past the image, and it being in view, still lets it load without canceling (as intendeed). Another difference is that loading only commences once the image is fully in view, rather than on the margin. * Document things a bit better * There was no bug; the placeholder had a 0 height! * A few more docs * Name things, expose as props Added: - debugActions: logs actions in dev, warns and logs in prod - debounceDurationMs: whether, and how much to debounce loading, 0 by default, so no change to v1. Added codepath to make it so. - Provisional changes.md document with API compatibility - Various TODOs and cleanup tasks * Fix passing of debounceDurationMs to cb * Use debugActions in the story, destructure it Also add a note to hopefully remember to destructure next time :) * Pin microbundle and ts versions * Clean dir before build * TS BUG: need declaration: false to build correctly If we set declaration: true, then typescript (specifically rpt2, Rollup's TS plugin) fails with a cryptic path error. This is filed under microsoft/TypeScript#25047. Curiously, this does not happen on master, and I have vaguely traced it back to unionize and the fact that it does fancy type exports, which we partially re-export here. We would have to manually emit a declaration file, which is not hard tbh, but a bit annoying. Even funnier, webpack has no issue building in storybook, but that might be because it does not emit declarations anyway. Steps: - Get declaration file from master, add to repository, and copy to dist/ upon build; - See if massaging our exports fixes it; - Wait for TS :/ * 1.1.0-rc.1 * Version bump * Make update static, use Commands The Elm/JS/Redux/React-Component fusion is complete. The reducer is now a pure function, because it only describes side-effects, and does not execute them. The logic is really nice to read there. * Naming things * Version bump * Use TS3.0 * Copy definitions separately Because of the "path undefined" bug, we have to run `tsc` separately, to get the definitions and copy them over. * Do not expose Buffering as a separate state Expose it under 'Loading' for backwards compatibility * Improve debounceDurationMs docs * Add first point to README * Cancel Promises, clear cache on unmount * Add docs for debounceDurationMs
1 parent e98a143 commit a68ffac

12 files changed

+558
-111
lines changed

README.md

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- Full presentational control for the caller (render props).
3131
- Modern, performant implementation, using [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) and providing [polyfill information](#polyfill-intersection-observer).
3232
- [Eager loading / Server-side rendering support](#eager-loading--server-side-rendering-ssr).
33+
- [Debounce / Delay](#debounce--delay); can wait for an image to be in the viewport for a set time, before loading.
3334
- Works with horizontal scrolling, supports background images.
3435
- [Fallbacks for SEO / when Javascript is disabled](#fallback-without-javascript).
3536
- Easy to understand source code. You should be able to fork and do your thing if desired.
@@ -292,6 +293,25 @@ Think about the cases where it is beneficial to do this, and apply it with inten
292293
Examples might be eager-loading hero images, preloading the first few elements in a list and so on.
293294
[Some of these use cases are provided as examples](#examples).
294295
296+
### Debounce / Delay
297+
298+
In cases where you have a long list of images that the user might scroll through, then loading intermediate images can waste bandwidth and processing time.
299+
This is undesired.
300+
The way to handle it is with a **minimum duration** that the image has to stay within the viewport, before making the request.
301+
This is specified using the `debounceDurationMs` prop:
302+
303+
```jsx
304+
<LazyImage
305+
src="/img/porto_buildings_large.jpg"
306+
alt="Buildings with tiled exteriors, lit by the sunset."
307+
debounceDurationMs={1000}
308+
placeholder={({ imageProps, ref }) => (
309+
<img ref={ref} src="/img/porto_buildings_lowres.jpg" alt={imageProps.alt} />
310+
)}
311+
actual={({ imageProps }) => <img {...imageProps} />}
312+
/>
313+
```
314+
295315
### Fallback without Javascript
296316
297317
If Javascript is disabled altogether by the user, then they will be stuck with the placeholder (and any images loaded eagerly).
@@ -410,19 +430,20 @@ The presentation can be derived from those plus, crucially, any specific needs y
410430
411431
**`<LazyImage />`** accepts the following props:
412432
413-
| Name | Type | Default | Required | Description |
414-
| ---------------------- | ------------------------------------------------------------------------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------- |
415-
| **src** | String | | true | The source of the image to load |
416-
| **alt** | String | | false | The alt text description of the image you are loading |
417-
| **srcSet** | String | | false | If your images use srcset, you can pass the `srcSet` prop to provide that information for preloading. |
418-
| **sizes** | String | | false | If your images use srcset, the sizes attribute helps the browser decide which source to load. |
419-
| **actual** | Function (render callback) of type ({imageProps}) => React.ReactNode | | true | Component to display once image has loaded |
420-
| **placeholder** | Function (render callback) of type ({imageProps, ref}) => React.ReactNode | undefined | true | Component to display while no request for the actual image has been made |
421-
| **loading** | Function (render callback) of type () => React.ReactNode | placeholder | false | Component to display while the image is loading |
422-
| **error** | Function (render callback) of type () => React.ReactNode | actual (broken image) | false | Component to display if the image loading has failed (render prop) |
423-
| **loadEagerly** | Boolean | false | false | Whether to skip checking for viewport and always show the 'actual' component |
424-
| **observerProps** | {threshold: number, rootMargin: string} | {threshold: 0.01, rootMargin: "50px 0px"} | false | Subset of props for the IntersectionObserver |
425-
| **experimentalDecode** | Boolean | false | false | Decode the image off-main-thread using the Image Decode API. Test before using! |
433+
| Name | Type | Default | Required | Description |
434+
| ---------------------- | ------------------------------------------------------------------------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
435+
| **src** | String | | true | The source of the image to load |
436+
| **alt** | String | | false | The alt text description of the image you are loading |
437+
| **srcSet** | String | | false | If your images use srcset, you can pass the `srcSet` prop to provide that information for preloading. |
438+
| **sizes** | String | | false | If your images use srcset, the sizes attribute helps the browser decide which source to load. |
439+
| **actual** | Function (render callback) of type ({imageProps}) => React.ReactNode | | true | Component to display once image has loaded |
440+
| **placeholder** | Function (render callback) of type ({imageProps, ref}) => React.ReactNode | undefined | true | Component to display while no request for the actual image has been made |
441+
| **loading** | Function (render callback) of type () => React.ReactNode | placeholder | false | Component to display while the image is loading |
442+
| **error** | Function (render callback) of type () => React.ReactNode | actual (broken image) | false | Component to display if the image loading has failed (render prop) |
443+
| **debounceDurationMs** | Number | N/A | false | The minimum duration that the image has to be in the viewport before starting to load, in ms. This can help avoid loading images while the user scrolls quickly past them. |
444+
| **loadEagerly** | Boolean | false | false | Whether to skip checking for viewport and always show the 'actual' component |
445+
| **observerProps** | {threshold: number, rootMargin: string} | {threshold: 0.01, rootMargin: "50px 0px"} | false | Subset of props for the IntersectionObserver |
446+
| **experimentalDecode** | Boolean | false | false | Decode the image off-main-thread using the Image Decode API. Test before using! |
426447
427448
**`<LazyImageFull />`** accepts the following props:
428449
@@ -432,6 +453,7 @@ The presentation can be derived from those plus, crucially, any specific needs y
432453
| **alt** | String | | false | The alt text description of the image you are loading |
433454
| **srcSet** | String | | false | If your images use srcset, you can pass the `srcSet` prop to provide that information for preloading. |
434455
| **sizes** | String | | false | If your images use srcset, the sizes attribute helps the browser decide which source to load. |
456+
| **debounceDurationMs** | Number | N/A | false | The minimum duration that the image has to be in the viewport before starting to load, in ms. |
435457
| **loadEagerly** | Boolean | false | false | Whether to skip checking for viewport and always show the 'actual' component |
436458
| **observerProps** | {threshold: number, rootMargin: string} | {threshold: 0.01, rootMargin: "50px 0px"} | false | Subset of props for the IntersectionObserver |
437459
| **children** | Function of type ({imageProps, imageState, ref}) => React.ReactNode | | true | Function to call that renders based on the props and state provided to it by LazyImageFull |

changes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- "Buffering" state is exposed in LazyImageFull, but not in LazyImage. Does it make sense to even expose?
2+
- Not exposing it would be non-breaking!
3+
- Add "debounceDuration" or prop; decide on naming!
4+
- Make optional, and 0 by default.

package-lock.json

Lines changed: 12 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "react-lazy-images",
33
"description": "React utilities for lazy image loading",
4-
"version": "1.0.1",
4+
"version": "1.1.0-rc.3",
55
"source": "src/index.tsx",
66
"module": "dist/react-lazy-images.es.js",
77
"main": "dist/react-lazy-images.js",
@@ -10,9 +10,11 @@
1010
"scripts": {
1111
"dev": "npm-run-all bundle:prod storybook",
1212
"dev:ts": "tsc --watch --pretty",
13-
"build": "npm-run-all bundle:prod size",
13+
"clean": "rimraf dist/",
14+
"build": "npm-run-all clean bundle:prod copy-defs size",
1415
"bundle:prod": "microbundle build",
1516
"bundle:watch": "microbundle watch",
17+
"copy-defs": "tsc && cp -r ts-build/*.d.ts dist/",
1618
"storybook": "start-storybook -p 8080 -s ./stories/demo",
1719
"storybook:build": "npm run bundle:prod && build-storybook -c .storybook -s ./stories/demo -o .out",
1820
"storybook:deploy": "storybook-to-ghpages --existing-output-dir=.out",
@@ -63,7 +65,8 @@
6365
},
6466
"homepage": "https://github.com/fpapado/react-lazy-images#readme",
6567
"dependencies": {
66-
"react-intersection-observer": "^6.1.0"
68+
"react-intersection-observer": "^6.1.0",
69+
"unionize": "^2.1.2"
6770
},
6871
"peerDependencies": {
6972
"react": "^15 || ^16",
@@ -88,7 +91,7 @@
8891
"eslint-plugin-jsx-a11y": "^6.1.1",
8992
"husky": "^1.0.0-rc.13",
9093
"lint-staged": "^7.2.0",
91-
"microbundle": "^0.5.1",
94+
"microbundle": "0.5.1",
9295
"npm-run-all": "^4.1.3",
9396
"prettier": "^1.13.7",
9497
"react": "^16.4.1",
@@ -97,7 +100,7 @@
97100
"react-docgen-typescript-webpack-plugin": "^1.1.0",
98101
"react-dom": "^16.4.1",
99102
"tachyons": "^4.10.0",
100-
"typescript": "^2.9.2",
101-
"typescript-eslint-parser": "^16.0.1"
103+
"typescript": "^3.0.1",
104+
"typescript-eslint-parser": "^17.0.1"
102105
}
103106
}

0 commit comments

Comments
 (0)