├── .all-contributorsrc
├── .babelrc
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .npmrc
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── index.d.ts
├── introduction.md
├── jest.config.js
├── other
├── CODE_OF_CONDUCT.md
├── MAINTAINING.md
├── USERS.md
├── idealimage-vs-img.md
├── idealimage-vs-img
│ ├── ii-androind-filmstrip.png
│ ├── ii-androind-screen.jpg
│ ├── ii-androind-waterfall.png
│ ├── ii-ios-filmstrip.png
│ ├── ii-ios-screen.jpg
│ ├── ii-ios-waterfall.png
│ ├── img-android-filmstrip.png
│ ├── img-android-screen.jpg
│ ├── img-android-waterfall.png
│ ├── img-ios-filmstrip.png
│ ├── img-ios-screen.jpg
│ └── img-ios-waterfall.png
├── images
│ ├── andre-spieker-238-unsplash.jpg
│ ├── andre-spieker-238-unsplash.webp
│ ├── jairo-alzate-45522-unsplash.jpg
│ ├── jairo-alzate-45522-unsplash.webp
│ ├── marvin-meyer-188676-unsplash.jpg
│ ├── marvin-meyer-188676-unsplash.webp
│ ├── nidhin-mundackal-281287-unsplash.jpg
│ ├── nidhin-mundackal-281287-unsplash.webp
│ ├── vincent-van-zalinge-408523-unsplash.jpg
│ └── vincent-van-zalinge-408523-unsplash.webp
├── introduction
│ ├── error-404.png
│ ├── error.png
│ ├── filmstrip-img-android-0.1.png
│ ├── filmstrip-img-android.png
│ ├── filmstrip-img-ios-0.1.png
│ ├── filmstrip-img.png
│ ├── filmstrip-lqip-react.png
│ ├── filmstrip-lqip.png
│ ├── load-size.png
│ ├── load.png
│ ├── loaded.png
│ ├── loading.png
│ ├── noicon.png
│ ├── offline.png
│ ├── screen-slow3g-chrome.jpg
│ ├── screen-slow3g-safari-1sec.jpg
│ ├── screen.jpg
│ ├── waterfall-img.png
│ ├── waterfall-lazy-load.png
│ ├── waterfall-slow3g-chrome.png
│ └── waterfall-slow3g-safari-1sec.png
└── manual-releases.md
├── package.json
├── prettier.config.js
├── renovate.json
├── src
├── __tests__
│ ├── .eslintrc
│ ├── IdealImage.js
│ ├── IdealImageWithDefaults.js
│ ├── __snapshots__
│ │ ├── IdealImage.js.snap
│ │ ├── icon.js.snap
│ │ └── idealImageWithDefaults.js.snap
│ ├── basic.test.tsx
│ ├── composeStyle.js
│ ├── helpers.js
│ ├── helpers.node.js
│ └── icon.js
├── components
│ ├── Icon
│ │ ├── Download.js
│ │ ├── Loading.js
│ │ ├── Offline.js
│ │ ├── Warning.js
│ │ └── index.js
│ ├── IdealImage
│ │ └── index.js
│ ├── IdealImageWithDefaults
│ │ └── index.js
│ ├── Media
│ │ └── index.js
│ ├── MediaWithDefaults
│ │ ├── README.md
│ │ └── index.js
│ ├── composeStyle.js
│ ├── constants.js
│ ├── helpers.js
│ ├── icons.js
│ ├── loaders.js
│ ├── theme.js
│ ├── theme.module.css
│ └── unfetch.js
└── index.js
├── styleguide.config.js
└── tsconfig.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "react-ideal-image",
3 | "projectOwner": "stereobooster",
4 | "files": [
5 | "README.md"
6 | ],
7 | "imageSize": 100,
8 | "commit": false,
9 | "contributors": [
10 | {
11 | "login": "stereobooster",
12 | "name": "stereobooster",
13 | "avatar_url": "https://avatars3.githubusercontent.com/u/179534?s=460&v=4",
14 | "profile": "https://github.com/stereobooster",
15 | "contributions": [
16 | "code",
17 | "doc",
18 | "infra",
19 | "test"
20 | ]
21 | },
22 | {
23 | "login": "sompylasar",
24 | "name": "Ivan Babak",
25 | "avatar_url": "https://avatars1.githubusercontent.com/u/498274?s=460&v=4",
26 | "profile": "https://github.com/sompylasar",
27 | "contributions": [
28 | "doc"
29 | ]
30 | },
31 | {
32 | "login": "palerdot",
33 | "name": "Arun Kumar",
34 | "avatar_url": "https://avatars1.githubusercontent.com/u/4299398?s=460&v=4",
35 | "profile": "https://github.com/palerdot",
36 | "contributions": [
37 | "doc"
38 | ]
39 | },
40 | {
41 | "login": "hipstersmoothie",
42 | "name": "Andrew Lisowski",
43 | "avatar_url": "https://avatars3.githubusercontent.com/u/1192452?v=4",
44 | "profile": "http://hipstersmoothie.com",
45 | "contributions": [
46 | "code"
47 | ]
48 | },
49 | {
50 | "login": "tvthatsme",
51 | "name": "Timothy Vernon",
52 | "avatar_url": "https://avatars1.githubusercontent.com/u/3386714?v=4",
53 | "profile": "https://github.com/tvthatsme",
54 | "contributions": [
55 | "test"
56 | ]
57 | },
58 | {
59 | "login": "vs1682",
60 | "name": "vishalShinde",
61 | "avatar_url": "https://avatars0.githubusercontent.com/u/5151881?v=4",
62 | "profile": "http://vs1682.github.io",
63 | "contributions": [
64 | "doc"
65 | ]
66 | },
67 | {
68 | "login": "EvgeniyKumachev",
69 | "name": "Evgeniy Kumachev",
70 | "avatar_url": "https://avatars3.githubusercontent.com/u/5207796?v=4",
71 | "profile": "https://github.com/EvgeniyKumachev",
72 | "contributions": [
73 | "doc"
74 | ]
75 | },
76 | {
77 | "login": "Tawe",
78 | "name": "John Munn",
79 | "avatar_url": "https://avatars0.githubusercontent.com/u/2087056?v=4",
80 | "profile": "https://github.com/Tawe",
81 | "contributions": [
82 | "code"
83 | ]
84 | }
85 | ],
86 | "repoType": "github"
87 | }
88 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/react"],
3 | "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.js text eol=lf
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
13 |
14 | * `react-ideal-image` version:
15 | * `node` version:
16 | * `npm` (or `yarn`) version:
17 |
18 | Relevant code or config
19 |
20 | ```javascript
21 | ```
22 |
23 | What you did:
24 |
25 | What happened:
26 |
27 |
28 |
29 | Reproduction repository:
30 |
31 |
35 |
36 | Problem description:
37 |
38 | Suggested solution:
39 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 | **What**:
20 |
21 |
22 |
23 | **Why**:
24 |
25 |
26 |
27 | **How**:
28 |
29 |
30 |
31 | **Checklist**:
32 |
33 |
34 |
35 |
36 |
37 | * [ ] Documentation
38 | * [ ] Tests
39 | * [ ] Ready to be merged
40 | * [ ] Added myself to contributors table
41 |
42 |
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | styleguide
5 | .opt-in
6 | .opt-out
7 | .DS_Store
8 | .eslintcache
9 |
10 | # these cause more harm than good
11 | # when working with contributors
12 | package-lock.json
13 | yarn.lock
14 | yarn-error.log
15 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=http://registry.npmjs.org/
2 | package-lock=false
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | cache:
4 | directories:
5 | - "~/.npm"
6 | - "node_modules"
7 | notifications:
8 | email: false
9 | node_js:
10 | - '10'
11 | install:
12 | - npm install -g npm
13 | - npm install
14 | script: npm run validate
15 | after_success: kcd-scripts travis-after-success
16 | branches:
17 | only: master
18 | env:
19 | global:
20 | secure: hG6olL1YgWWkvDW+oVQ4BNIape5GsI9uOqPG6WAP2t1LjG5McqFCP6N0HJLfGI/iiTjbhDhBi69lFXyBzWGEM8Smvmnh1iekRQ+Vrt1L9Q+aXEoRm97LtAbIlKNat4QP9ZW1gaNAPgWrzE0mlPxtOYUNjpR8+T6Ivt2Q04iT2cP7kqgC6CmGVVEk3hOAQqWXkkzUUNifcRs9IdeJrBQzWBE2s1Pw+e2GSvb75fW0hXT9AzfFbl5Q1Nw4R2Mai8uDcqaTEROYxdipz1NJTxvEzix71a2vNEXDGMocnLJldd6SCIJ4SzTwq2qcTnlH0PizvK7ym9yRqDbUudxq09/hqf/q8UzjbWMxImB6Aa7E6KH0zfguJUb0jlOd5mkGnJBE67G+yYrgfCutmGIho4UzWgwpu5PJsENzy1xUN/syKtgCc7Nu1l4tag+3U1vo45GcN+CcY05gKWj7rK9RkXTwBvloXJXhocXfVP3hoQq+WDqFyfEdg74U97Rn6bJ7kluC14m7IAoKRwLjJpLx8BRGCVhMwvPlCdW2ZkBkLgkQVHuNXXZ41sSVxq4PEI+04CncEvmrrWbyBJSApq998hKVkVLwtl1RtVxBN5jJJk59JuDjpq4WTN5GIDNs7x6/oyuGFAQzPJ4/tKxXPA6Jls+TzruY837oG5hfhk2vUY1NzHg=
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | The changelog is automatically updated using [semantic-release](https://github.com/semantic-release/semantic-release).
4 | You can see it on the [releases page](../../releases).
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for being willing to contribute!
4 |
5 | **Working on your first Pull Request?** You can learn how from this _free_ series
6 | [How to Contribute to an Open Source Project on GitHub][egghead]
7 |
8 | ## Project setup
9 |
10 | 1. Fork and clone the repo
11 | 2. Run `npm run setup -s` to install dependencies and run validation
12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name`
13 |
14 | > Tip: Keep your `master` branch pointing at the original repository and make
15 | > pull requests from branches on your fork. To do this, run:
16 | >
17 | > ```
18 | > git remote add upstream https://github.com/stereobooster/react-ideal-image.git
19 | > git fetch upstream
20 | > git branch --set-upstream-to=upstream/master master
21 | > ```
22 | >
23 | > This will add the original repository as a "remote" called "upstream,"
24 | > Then fetch the git information from that remote, then set your local `master`
25 | > branch to use the upstream master branch whenever you run `git pull`.
26 | > Then you can make all of your pull request branches based on this `master`
27 | > branch. Whenever you want to update your version of `master`, do a regular
28 | > `git pull`.
29 |
30 | ## Add yourself as a contributor
31 |
32 | This project follows the [all contributors][all-contributors] specification.
33 | To add yourself to the table of contributors on the `README.md`, please use the
34 | automated script as part of your PR:
35 |
36 | ```console
37 | npm run add-contributor
38 | ```
39 |
40 | Follow the prompt and commit `.all-contributorsrc` and `README.md` in the PR.
41 | If you've already added yourself to the list and are making
42 | a new type of contribution, you can run it again and select the added
43 | contribution type.
44 |
45 | ## Committing and Pushing changes
46 |
47 | Please make sure to run the tests before you commit your changes. You can run
48 | `npm run test:update` which will update any snapshots that need updating.
49 | Make sure to include those changes (if they exist) in your commit.
50 |
51 | ### opt into git hooks
52 |
53 | There are git hooks set up with this project that are automatically installed
54 | when you install dependencies. They're really handy, but are turned off by
55 | default (so as to not hinder new contributors). You can opt into these by
56 | creating a file called `.opt-in` at the root of the project and putting this
57 | inside:
58 |
59 | ```
60 | pre-commit
61 | ```
62 |
63 | ## Help needed
64 |
65 | Please checkout the [the open issues][issues]
66 |
67 | Also, please watch the repo and respond to questions/bug reports/feature
68 | requests! Thanks!
69 |
70 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
71 | [all-contributors]: https://github.com/stereobooster/all-contributors
72 | [issues]: https://github.com/stereobooster/react-ideal-image/issues
73 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2017 stereobooster
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://vshymanskyy.github.io/StandWithUkraine)
2 |
3 |
4 |
react-ideal-image
5 |
6 |
Adaptive image component
7 |
8 |
9 |
10 |
11 | [![Build Status][build-badge]][build]
12 | [![Code Coverage][coverage-badge]][coverage]
13 | [![version][version-badge]][package]
14 | [![downloads][downloads-badge]][npmtrends]
15 | [![MIT License][license-badge]][license]
16 |
17 | [](#contributors)
18 | [![PRs Welcome][prs-badge]][prs]
19 | [![Code of Conduct][coc-badge]][coc]
20 |
21 | [![Watch on GitHub][github-watch-badge]][github-watch]
22 | [![Star on GitHub][github-star-badge]][github-star]
23 | [![Tweet][twitter-badge]][twitter]
24 |
25 | ## The problem
26 |
27 | I need React component to asynchronously load images, which will adapt based on network, which will allow a user to control, which image to load.
28 |
29 | ## This solution
30 |
31 | Read the [introduction](introduction.md).
32 |
33 | ## Table of Contents
34 |
35 |
36 |
37 |
38 | - [Installation](#installation)
39 | - [Usage](#usage)
40 | - [Props](#props)
41 | - [getIcon](#geticon)
42 | - [getMessage](#getmessage)
43 | - [getUrl](#geturl)
44 | - [height](#height)
45 | - [icons](#icons)
46 | - [loader](#loader)
47 | - [placeholder](#placeholder)
48 | - [shouldAutoDownload](#shouldautodownload)
49 | - [srcSet](#srcset)
50 | - [theme](#theme)
51 | - [threshold](#threshold)
52 | - [width](#width)
53 | - [Other Solutions](#other-solutions)
54 | - [Contributors](#contributors)
55 | - [LICENSE](#license)
56 |
57 |
58 |
59 | ## Installation
60 |
61 | This module is distributed via [npm][npm] which is bundled with [node][node] and
62 | should be installed as one of your project's `dependencies`:
63 |
64 | ```
65 | npm install react-ideal-image --save
66 | ```
67 |
68 | > This package also depends on `react`, `prop-types`, and `react-waypoint`.
69 | > Please make sure you have those installed as well.
70 |
71 | ## Usage
72 |
73 | Example for create-react-app (you need v2 for macros) based project
74 |
75 | ```js
76 | import React from 'react'
77 | import lqip from 'lqip.macro'
78 | import IdealImage from 'react-ideal-image'
79 |
80 | import image from './images/doggo.jpg'
81 | const lqip = lqip('./images/doggo.jpg')
82 |
83 | const App = () => (
84 |
91 | )
92 | ```
93 |
94 | ## Props
95 |
96 | This is the list of props that you need to pass to the component.
97 |
98 | ### getIcon
99 |
100 | > `function(state: object)` | optional, default icon is provided based on state object
101 |
102 | This function decides what icon to show based on the current state of the component.
103 |
104 | ### getMessage
105 |
106 | > `function(icon: string, state: object)` | optional, default message is provided based on the icon and state object.
107 |
108 | This function decides what message to show based on the icon (returned from getIcon prop) and the current state of the component.
109 |
110 | ### getUrl
111 |
112 | > `function({})` | optional, no useful default
113 |
114 | This function is called as soon as the component enters the viewport and is used to generate urls based on width and format if `props.srcSet` doesn't provide src field.
115 |
116 | ### height
117 |
118 | > `number` | required
119 |
120 | The Height of the image in px.
121 |
122 | ### icons
123 |
124 | > `object` | required
125 |
126 | This provides a map of the icons. By default, the component uses icons from material design, implemented as React components with the SVG element. You can customize icons
127 |
128 | ```js
129 | const icons = {
130 | load: DownloadIcon,
131 | //...
132 | }
133 | ```
134 |
135 | ### loader
136 |
137 | > `string` | optional, defaults to 'xhr'
138 |
139 | This prop takes one of the 2 options, `xhr` and `image`. Read more about it [here](https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#cancel-download).
140 |
141 | ### placeholder
142 |
143 | > `object` | required
144 |
145 | This takes one of the 2 objects
146 |
147 | ```js
148 | // To add a solid color placeholder
149 | {
150 | color: ''
151 | }
152 | ```
153 |
154 | or
155 |
156 | ```js
157 | /**
158 | * To add a low quality image
159 | * [Low Quality Image Placeholder](https://github.com/zouhir/lqip)
160 | * [SVG-Based Image Placeholder](https://github.com/technopagan/sqip)
161 | * base64 encoded image of low quality
162 | */
163 | {
164 | lqip: ''
165 | }
166 | ```
167 |
168 | Read more about it [here](https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#lqip).
169 |
170 | ### shouldAutoDownload
171 |
172 | > `function({})` | optional, default function is provided which decides based on the device network.
173 |
174 | This function decides if image should be downloaded automatically. The default function returns `false` for a `2g` network,
175 | for a `3g` network it decides based on `props.threshold` and for a `4g` network it returns `true` by default.
176 |
177 | ### srcSet
178 |
179 | > `array[srcType: object]` | required
180 |
181 | This provides an array of sources of different format and size of the image. Read more about it [here](https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#srcset).
182 | The `srcType` has below structure
183 |
184 | ```js
185 | srcType = {
186 | width: number, // required
187 | src: string,
188 | size: number,
189 | format: string, // one of the 'jpeg' or 'webp'
190 | }
191 | ```
192 |
193 | ### theme
194 |
195 | > `object` | required
196 |
197 | This provides a theme to the component. By default, the component uses inline styles, but it is also possible to use CSS modules and override all styles.
198 |
199 | ```js
200 | const theme = {
201 | placeholder: {
202 | backgroundSize: 'cover',
203 | backgroundRepeat: 'no-repeat',
204 | position: 'relative',
205 | },
206 | // ...
207 | }
208 | ```
209 |
210 | ### threshold
211 |
212 | > `number` | optional
213 |
214 | Tells how much to wait in milliseconds until consider the download to be slow.
215 |
216 | ### width
217 |
218 | > `number` | required
219 |
220 | Width of the image in px.
221 |
222 | ## Other Solutions
223 |
224 | - [react-progressive-image](https://github.com/FormidableLabs/react-progressive-image)
225 | - [react-lazyload](https://github.com/jasonslyvia/react-lazyload)
226 | - [react-lazy-image](https://github.com/sergiodxa/react-lazy-image)
227 | - [react-image](https://github.com/mbrevda/react-image)
228 | - [react-lazy-load](https://github.com/loktar00/react-lazy-load)
229 | - [react-graceful-image](https://github.com/linasmnew/react-graceful-image)
230 | - [react-worker-image](https://github.com/nitish24p/react-worker-image)
231 | - [lazy-image](https://github.com/notwaldorf/lazy-image)
232 | - [react-simple-image](https://github.com/bitjourney/react-simple-image)
233 | - [react-power-picture](https://github.com/tvthatsme/react-power-picture)
234 | - [react-shimmer](https://github.com/gokcan/react-shimmer)
235 | - [gatsby-image](https://www.gatsbyjs.org/packages/gatsby-image/)
236 | - [react-async-elements ``](https://github.com/palmerhq/react-async-elements#img)
237 |
238 | ## Contributors
239 |
240 | Thanks goes to these people ([emoji key][emojis]):
241 |
242 |
243 |
244 | | [ stereobooster](https://github.com/stereobooster) [💻](https://github.com/stereobooster/react-ideal-image/commits?author=stereobooster "Code") [📖](https://github.com/stereobooster/react-ideal-image/commits?author=stereobooster "Documentation") [🚇](#infra-stereobooster "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/stereobooster/react-ideal-image/commits?author=stereobooster "Tests") | [ Ivan Babak](https://github.com/sompylasar) [📖](https://github.com/stereobooster/react-ideal-image/commits?author=sompylasar "Documentation") | [ Arun Kumar](https://github.com/palerdot) [📖](https://github.com/stereobooster/react-ideal-image/commits?author=palerdot "Documentation") | [ Andrew Lisowski](http://hipstersmoothie.com) [💻](https://github.com/stereobooster/react-ideal-image/commits?author=hipstersmoothie "Code") | [ Timothy Vernon](https://github.com/tvthatsme) [⚠️](https://github.com/stereobooster/react-ideal-image/commits?author=tvthatsme "Tests") | [ vishalShinde](http://vs1682.github.io) [📖](https://github.com/stereobooster/react-ideal-image/commits?author=vs1682 "Documentation") | [ Evgeniy Kumachev](https://github.com/EvgeniyKumachev) [📖](https://github.com/stereobooster/react-ideal-image/commits?author=EvgeniyKumachev "Documentation") |
245 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
246 | | [ John Munn](https://github.com/Tawe) [💻](https://github.com/stereobooster/react-ideal-image/commits?author=Tawe "Code") |
247 |
248 |
249 |
250 | This project follows the [all-contributors][all-contributors] specification.
251 | Contributions of any kind welcome!
252 |
253 | ## LICENSE
254 |
255 | Code - MIT
256 |
257 | Icons - [Apache License 2.0](https://github.com/google/material-design-icons/blob/master/LICENSE)
258 |
259 | [npm]: https://www.npmjs.com/
260 | [node]: https://nodejs.org
261 | [build-badge]: https://img.shields.io/travis/stereobooster/react-ideal-image.svg?style=flat-square
262 | [build]: https://travis-ci.org/stereobooster/react-ideal-image
263 | [coverage-badge]: https://img.shields.io/codecov/c/github/stereobooster/react-ideal-image.svg?style=flat-square
264 | [coverage]: https://codecov.io/github/stereobooster/react-ideal-image
265 | [version-badge]: https://img.shields.io/npm/v/react-ideal-image.svg?style=flat-square
266 | [package]: https://www.npmjs.com/package/react-ideal-image
267 | [downloads-badge]: https://img.shields.io/npm/dm/react-ideal-image.svg?style=flat-square
268 | [npmtrends]: http://www.npmtrends.com/react-ideal-image
269 | [license-badge]: https://img.shields.io/npm/l/react-ideal-image.svg?style=flat-square
270 | [license]: https://github.com/stereobooster/react-ideal-image/blob/master/LICENSE
271 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
272 | [prs]: http://makeapullrequest.com
273 | [donate-badge]: https://img.shields.io/badge/$-support-green.svg?style=flat-square
274 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
275 | [coc]: https://github.com/stereobooster/react-ideal-image/blob/master/other/CODE_OF_CONDUCT.md
276 | [github-watch-badge]: https://img.shields.io/github/watchers/stereobooster/react-ideal-image.svg?style=social
277 | [github-watch]: https://github.com/stereobooster/react-ideal-image/watchers
278 | [github-star-badge]: https://img.shields.io/github/stars/stereobooster/react-ideal-image.svg?style=social
279 | [github-star]: https://github.com/stereobooster/react-ideal-image/stargazers
280 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20react-ideal-image%20by%20%40stereobooster%20https%3A%2F%2Fgithub.com%2Fstereobooster%2Freact-ideal-image%20%F0%9F%91%8D
281 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/stereobooster/react-ideal-image.svg?style=social
282 | [emojis]: https://github.com/kentcdodds/all-contributors#emoji-key
283 | [all-contributors]: https://github.com/kentcdodds/all-contributors
284 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import {Component, ComponentType, ComponentClass, CSSProperties} from 'react'
2 |
3 | export type LoadingState = 'initial' | 'loading' | 'loaded' | 'error'
4 |
5 | export type IconKey =
6 | | 'load'
7 | | 'loading'
8 | | 'loaded'
9 | | 'error'
10 | | 'noicon'
11 | | 'offline'
12 |
13 | export interface SrcType {
14 | width: number
15 | src?: string
16 | size?: number
17 | format?: 'webp' | 'jpeg'
18 | }
19 |
20 | type ThemeKey = 'placeholder' | 'img' | 'icon' | 'noscript'
21 |
22 | export interface ImageProps {
23 | /**
24 | * This function decides what icon to show based on the current state of the component.
25 | */
26 | getIcon?: (state: LoadingState) => IconKey
27 | /**
28 | * This function decides what message to show based on the icon (returned from getIcon prop) and
29 | * the current state of the component.
30 | */
31 | getMessage?: (icon: IconKey, state: LoadingState) => string
32 | /**
33 | * This function is called as soon as the component enters the viewport and is used to generate urls
34 | * based on width and format if props.srcSet doesn't provide src field.
35 | */
36 | getUrl?: (srcType: SrcType) => string
37 | /**
38 | * The Height of the image in px.
39 | */
40 | height: number
41 | /**
42 | * This provides a map of the icons. By default, the component uses icons from material design,
43 | * implemented as React components with the SVG element. You can customize icons
44 | */
45 | icons: Partial>
46 | /**
47 | * This prop takes one of the 2 options, xhr and image.
48 | * Read more about it:
49 | * https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#cancel-download
50 | */
51 | loader?: 'xhr' | 'image'
52 | /**
53 | * https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#lqip
54 | */
55 | placeholder: {color: string} | {lqip: string}
56 | /**
57 | * This function decides if image should be downloaded automatically. The default function
58 | * returns false for a 2g network, for a 3g network it decides based on props.threshold
59 | * and for a 4g network it returns true by default.
60 | */
61 | shouldAutoDownload?: (
62 | options: {
63 | connection?: 'slow-2g' | '2g' | '3g' | '4g'
64 | size?: number
65 | threshold?: number
66 | possiblySlowNetwork?: boolean
67 | },
68 | ) => boolean
69 | /**
70 | * This provides an array of sources of different format and size of the image.
71 | * Read more about it:
72 | * https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#srcset
73 | */
74 | srcSet: SrcType[]
75 | /**
76 | * This provides a theme to the component. By default, the component uses inline styles,
77 | * but it is also possible to use CSS modules and override all styles.
78 | */
79 | theme?: Partial>
80 | /**
81 | * Tells how much to wait in milliseconds until consider the download to be slow.
82 | */
83 | threshold?: number
84 | /**
85 | * Width of the image in px.
86 | */
87 | width: number
88 | }
89 |
90 | type IdealImageComponent = ComponentClass
91 |
92 | declare const IdealImage: IdealImageComponent
93 | export default IdealImage
94 |
--------------------------------------------------------------------------------
/introduction.md:
--------------------------------------------------------------------------------
1 | # An Almost Ideal React Image Component
2 |
3 | TL;DR. This started as an exercise - how to build ideal React image component. The focus was more on UX and browser capabilities, rather than React code. I created react component and published it to npm, but it has no tests and not battle tested in the wild, use it at your own risk.
4 |
5 | [Online example](https://stereobooster.github.io/react-ideal-image-experiments/) | [HN discussion](https://news.ycombinator.com/item?id=17210378) | [Guide To Async Components](https://github.com/stereobooster/guide-to-async-components) | [IdealImage vs img](https://github.com/stereobooster/react-ideal-image/blob/master/other/idealimage-vs-img.md)
6 |
7 | ## Lazy loading
8 |
9 | This is a straightforward feature - do not load images which are outside of the screen. Do not need to reinvent a wheel, there is [react-waypoint](https://github.com/brigade/react-waypoint), to trigger actions in the component as soon as it appears on the screen (pseudo code):
10 |
11 | ```js
12 | this.setState({src})}>
13 |
14 |
15 | ```
16 |
17 |
18 |
19 |
20 | Pic 1. Browser's `img` loads all 5 images on the page, but only 3 are visible
21 |
22 |
35 |
36 | ## Placeholder
37 |
38 | As soon as you start to do lazy loading you will notice unpleasant content jumps as soon as images get loaded. This is bad for two reasons: UX - content jumps make user loose visual track, performance - content jumps are [browser redraws](https://developers.google.com/speed/docs/insights/browser-reflow). This is why we need a placeholder - a thing which will fill place until the image gets loaded. To do this we need to know image size upfront. AMP has same requirements for all blocks. Simplest placeholder(pseudo code):
39 |
40 | ```js
41 | load () {
42 | const img = new Image()
43 | img.onload = () => this.setState({loaded:true})
44 | img.src = this.props.src
45 | }
46 | render() {
47 | if (!this.state.loaded) {
48 | return ()
49 | } else {
50 | return ()
51 | }
52 | }
53 | ```
54 |
55 | **Pic 4.** Load progress of images without dimension
56 |
57 | 
58 |
59 | ### LQIP
60 |
61 | Better, but not ideal. A user will see blank space until image load, this can be perceived as broken functionality - what if the image fails to load, what if it takes too long. Low-Quality Image Placeholder to the rescue. This technique is known since times of progressive JPEGs, later forgotten and reinvented by Facebook, Medium, and others. Also, we can use solid color placeholder or SQIP. Read more about placeholders [here](https://medium.freecodecamp.org/using-svg-as-placeholders-more-image-loading-techniques-bed1b810ab2c). To get LQIP you can use [sharp](https://github.com/lovell/sharp)
62 |
63 | ```js
64 | const getLqip = file =>
65 | new Promise((resolve, reject) => {
66 | sharp(file)
67 | .resize(20)
68 | .toBuffer((err, data, info) => {
69 | if (err) return reject(err)
70 | const {format} = info
71 | return resolve(`data:image/${format};base64,${data.toString('base64')}`)
72 | })
73 | })
74 |
75 | const lqip = await getLqip('cute-dog.jpg')
76 | ```
77 |
78 | Also check: [lqip](https://github.com/zouhir/lqip) or [lqip.macro](https://github.com/stereobooster/lqip.macro); [sqip](https://github.com/technopagan/sqip) or [sqip.macro](https://github.com/stereobooster/sqip.macro);
79 |
80 | Use LQIP like this (pseudo code):
81 |
82 | ```js
83 |
84 |
85 |
86 | ```
87 |
88 | Or in the component:
89 |
90 | ```js
91 |
92 | ```
93 |
94 | **Pic 5.** Load progress of images with LQIP, but without JS
95 |
96 | 
97 |
98 | **Pic 6.** Load progress of images with LQIP and with JS
99 |
100 | 
101 |
102 | ## Responsive
103 |
104 | ### Styles
105 |
106 | We are specifying exact width and height of the image and the placeholder. To make it responsive we need to add some CSS (pseudo code):
107 |
108 | ```js
109 | const img = {
110 | width: '100%',
111 | height: 'auto',
112 | maxWidth: '100%',
113 | }
114 |
115 | render() {
116 | if (this.state.loaded) {
117 | return ()
118 | } else {
119 | return ()
120 | }
121 | }
122 | ```
123 |
124 | **Pic 7.1** Load progress of `img` with "responsive style" in Android (0.1s interval)
125 |
126 | 
127 |
128 | **Pic 7.2.** Load progress of `img` with "responsive style" in iOS (0.5s interval)
129 |
130 | 
131 |
132 | ### `srcSet`
133 |
134 | This feature is about reimplementing `srcSet` property of [responsive image](https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-srcSet/). It would be nice to use image based on the size of the screen, to minimize traffic for the images on small devices.
135 |
136 | To do this we will need:
137 |
138 | - Set of images resized for different devices. You can use [sharp](https://github.com/lovell/sharp) to resize images.
139 | - Data about how much space image takes on the screen. This is easy because we mount placeholder before the image, so the reference to the placeholder can be used to get dimensions
140 | - Some heuristic based on `screen.width`, `screen.height`, `window.devicePixelRatio`, `body.clientHeight` to guess maximum image size for given device
141 | - Would be nice to take into account `orientationchange` events, but will not do this for now.
142 |
143 | See exact implementation in the code (`guessMaxImageWidth`). Our component will look like this:
144 |
145 | ```js
146 |
156 | ```
157 |
158 | Also possible to reimplement `sizes` param with [css-mediaquery](https://github.com/ericf/css-mediaquery), but this potentially can give more bugs than the actual value.
159 |
160 | ## Adaptive
161 |
162 | Most likely you haven't heard this term applied to the images, because I made it up. Adaptive image - an image which adapts to the environment, for example, if the browser supports WebP use it if the network is too slow stop auto download images if the browser is offline communicate to the user that download of the image is not possible at the moment.
163 |
164 | ### WebP
165 |
166 | To detect WebP support we can use this snippet copy-pasted from StackOverflow:
167 |
168 | ```js
169 | const detectWebpSupport = () => {
170 | if (ssr) return false
171 | const elem = document.createElement('canvas')
172 | if (elem.getContext && elem.getContext('2d')) {
173 | // was able or not to get WebP representation
174 | return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0
175 | } else {
176 | // very old browser like IE 8, canvas not supported
177 | return false
178 | }
179 | }
180 | ```
181 |
182 | Use component like this:
183 |
184 | ```js
185 |
192 | ```
193 |
194 | ### Slow network
195 |
196 | If the network is slow it makes no sense to auto-download image (as soon as it appears on the screen), because it will take a long time to load even more time if the browser tries to download more than one image simultaneously.
197 |
198 | Instead, we can let the user decide if they want to download image or not. There should be an icon over placeholder, so the user can click it to start the download, and click again to cancel the download. As soon as the download starts there should be no icon, but if it takes too long some indicator of loading state should appear to inform the user that it is still working.
199 |
200 | | load | no icon | loading |
201 | | -------------------------------- | ---------------------------------- | ----------------------------------- |
202 | |  |  |  |
203 |
204 | In Chrome it is pretty easy to detect the slow network with `navigator.connection.effectiveType`. If it is 'slow-2g', '2g', '3g' then the component will not auto-download images.
205 |
206 | | Component detected slow network and didn't try to load images  | Component switched to manual mode  |
207 | | :---------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- |
208 |
209 |
210 | For other browsers, we can try to guess if the download of the image takes too much time. How much time should be considered as too much is up-to developer, via `threshold` property (optional):
211 |
212 | ```js
213 |
214 | ```
215 |
216 | If image takes too long to download and the load was initiated by "Lazy loading" feature then:
217 |
218 | - load process will be canceled
219 | - the component will show control, so the user can initiate the download of the image manually
220 | - the component will broadcast event `possibly slow network`, so other components would not even try load images and will be switched to "Manual mode"
221 |
222 | | Component tried to download images, but canceled load after 1 second  | Component switched to manual mode  |
223 | | :---------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- |
224 |
225 |
226 | ### Cancel download
227 |
228 | In Chrome (and probably other browsers) you can asign empty string to `src` to cancel download, but this doesn't work in Mobile Safari:
229 |
230 | ```js
231 | const img = new Image()
232 | //...
233 | img.src = ''
234 | ```
235 |
236 | Other way to do it is to use good old `XMLHttpRequest` which supports cancel:
237 |
238 | ```js
239 | const request = new XMLHttpRequest()
240 | //...
241 | request.abort()
242 | ```
243 |
244 | Buuut:
245 |
246 | - if images are uncacheable this will not work - the browser will trigger another request for the image as soon as we insert an image in the DOM
247 | - if images are hosted on the different domain we will need to configure CORS properly
248 |
249 | This is why I chose to let developer decide which approach to use (default is `xhr`):
250 |
251 | ```js
252 |
253 | ```
254 |
255 | It is also possible to use `fetch` with [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController), but it is supported only in Chrome 66+ at the moment.
256 |
257 | ## More UX
258 |
259 | ### Network error
260 |
261 | If image network request errored we need to show user message that browser failed to download the image. The user should be able to recover from the error (in case of temporal issue), by clicking on the image user can trigger repetitive load.
262 |
263 | ### 404 error
264 |
265 | 404 error is the special one. We use LQIP placeholder, which creates "impression" of content, but our component can outlive real image. We need clearly explain to the user that image doesn't exist anymore.
266 |
267 | ### Offline
268 |
269 | Because we are lazy loading images, it can happen that we have some unloaded images at the moment when the browser goes offline. We should not confuse users in this case with an error message, instead we should clearly identify that browser is offline and this is why browser cannot load images.
270 |
271 | | Network error | 404 error | Offline |
272 | | --------------------------------- | ------------------------------------- | ----------------------------------- |
273 | |  |  |  |
274 |
275 | ## SSR or prerendering
276 |
277 | On the server, the component will be rendered with a placeholder (lqip) and without an icon. As soon as React application will boot, the component will decide if it needs to start download image or show download icon.
278 |
279 | ### Users with disabled JavaScript
280 |
281 | For users with disabled JavaScript or for search bots component will render the good old image in `