├── example
├── .npmignore
├── index.html
├── tsconfig.json
├── package.json
└── index.tsx
├── media
├── demo.gif
├── header.png
└── logo.png
├── src
├── loaders
│ ├── index.ts
│ ├── breathing
│ │ ├── styles.css
│ │ ├── Breathing.tsx
│ │ └── README.md
│ └── shimmer
│ │ ├── styles.css
│ │ ├── README.md
│ │ └── Shimmer.tsx
├── index.ts
├── anims
│ └── anims.css
├── IntendedError.ts
└── Image.tsx
├── tsdx.config.js
├── .gitignore
├── LICENSE
├── .github
└── workflows
│ └── main.yml
├── tsconfig.json
├── package.json
└── README.md
/example/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/media/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokcan/react-shimmer/HEAD/media/demo.gif
--------------------------------------------------------------------------------
/media/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokcan/react-shimmer/HEAD/media/header.png
--------------------------------------------------------------------------------
/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokcan/react-shimmer/HEAD/media/logo.png
--------------------------------------------------------------------------------
/src/loaders/index.ts:
--------------------------------------------------------------------------------
1 | export { Shimmer, ShimmerProps } from './shimmer/Shimmer';
2 | export { Breathing, BreathingProps } from './breathing/Breathing';
3 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { SuspenseImage as Image } from './Image';
2 | export { ImageProps } from './Image';
3 |
4 | // Loaders
5 | export * from './loaders';
6 |
--------------------------------------------------------------------------------
/tsdx.config.js:
--------------------------------------------------------------------------------
1 | const postcss = require('rollup-plugin-postcss');
2 |
3 | module.exports = {
4 | rollup(config) {
5 | config.plugins.push(postcss());
6 | return config;
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/anims/anims.css:
--------------------------------------------------------------------------------
1 | .fadein {
2 | animation-name: fadein;
3 | animation-duration: 0.3s;
4 | animation-fill-mode: forwards;
5 | }
6 |
7 | @keyframes fadein {
8 | from { opacity: 0; }
9 | to { opacity: 1; }
10 | }
11 |
--------------------------------------------------------------------------------
/src/IntendedError.ts:
--------------------------------------------------------------------------------
1 | export default class IntendedError extends Error {
2 | createdAt: Date;
3 | intention: string;
4 |
5 | constructor(intention = 'forcePromiseReject', ...params: undefined[]) {
6 | super(...params);
7 | this.createdAt = new Date();
8 | this.intention = intention;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/loaders/breathing/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --default-bg-color: #e1e2e4;
3 | }
4 |
5 | .breathing {
6 | width: 100%;
7 | height: 100%;
8 | background: var(--default-bg-color);
9 | animation: breathing ease-in-out infinite alternate;
10 | }
11 |
12 | @keyframes breathing {
13 | from {
14 | opacity: 0.25;
15 | }
16 | to {
17 | opacity: 1;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-shimmer
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # IDE Stuff
4 | /.idea
5 | /.vscode
6 |
7 | # dependencies
8 | node_modules
9 |
10 | # testing
11 | /coverage
12 |
13 | # builds
14 | build
15 | dist
16 | .rpt2_cache
17 | .parcel-cache
18 |
19 | # misc
20 | .DS_Store
21 | .env
22 | .env.local
23 | .env.development.local
24 | .env.test.local
25 | .env.production.local
26 |
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "types": ["node"]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-shimmer-example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "react-app-polyfill": "^1.0.0"
12 | },
13 | "alias": {
14 | "react": "../node_modules/react",
15 | "react-dom": "../node_modules/react-dom/profiling",
16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^16.9.11",
20 | "@types/react-dom": "^16.8.4",
21 | "parcel": "^2.0.0-beta.2",
22 | "typescript": "^3.4.5"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/loaders/shimmer/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --default-bg-color: #f6f7f8;
3 | --default-bg-moving-gradient: linear-gradient(to right, rgb(238, 238, 238) 8%, rgb(222, 222, 222) 18%, rgb(238, 238, 238) 33%);
4 | }
5 |
6 | .shimmer {
7 | background: var(--default-bg-color);
8 | background-image: var(--default-bg-moving-gradient);
9 | background-repeat: no-repeat;
10 | animation: shimmering forwards infinite ease-in-out, fadein 0.02s forwards;
11 | }
12 |
13 | @keyframes fadein {
14 | from {
15 | opacity: 0;
16 | }
17 |
18 | to {
19 | opacity: 1;
20 | }
21 | }
22 |
23 | @keyframes shimmering {
24 | from {
25 | background-position: top right;
26 | }
27 |
28 | to {
29 | background-position: top left;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/loaders/breathing/Breathing.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import clsx from 'clsx';
4 |
5 | import './styles.css';
6 |
7 | export interface BreathingProps {
8 | className?: string;
9 | duration?: number;
10 | height?: number;
11 | width?: number;
12 | }
13 |
14 | const DEFAULT_DURATION_MS = 1000;
15 |
16 | export const Breathing = ({
17 | className,
18 | duration = DEFAULT_DURATION_MS,
19 | height,
20 | width,
21 | }: BreathingProps) => {
22 | const style = {
23 | height,
24 | width,
25 | animationDuration: `${(duration / 1000).toFixed(1)}s`,
26 | };
27 | return ;
28 | };
29 |
30 | Breathing.propTypes = {
31 | className: PropTypes.string,
32 | duration: PropTypes.number,
33 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
34 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
35 | };
36 |
--------------------------------------------------------------------------------
/src/loaders/breathing/README.md:
--------------------------------------------------------------------------------
1 | ### Breathing Loader
2 |
3 | ## Usage
4 |
5 | ```jsx
6 | import React from 'react'
7 | import Image, { Breathing } from 'react-shimmer'
8 |
9 | function App(props) {
10 | return (
11 |
12 | }
15 | />
16 |
17 | )
18 | }
19 | ```
20 |
21 | or as a standalone placeholder:
22 |
23 | ```jsx
24 | import React from 'react'
25 | import { Breathing } from 'react-shimmer'
26 |
27 | function App(props) {
28 | return (
29 |
30 |
31 |
32 | )
33 | }
34 | ```
35 |
36 | ### Properties
37 |
38 | Property | Type | Required | Default value | Description
39 | :--- | :--- | :--- | :--- | :---
40 | `className`|string|no|| Override default styles with className
41 | `width`|number|no||
42 | `height`|number|no||
43 | `duration`|number|no|`1000`| Animation duration (ms)
44 | -----
45 |
46 | ## License
47 |
48 | MIT © [gokcan](https://github.com/gokcan)
49 |
--------------------------------------------------------------------------------
/src/loaders/shimmer/README.md:
--------------------------------------------------------------------------------
1 | ### Shimmer Loader
2 |
3 | ## Usage
4 |
5 | ```jsx
6 | import React from 'react'
7 | import Image, { Shimmer } from 'react-shimmer'
8 |
9 | function App(props) {
10 | return (
11 |
12 | }
15 | />
16 |
17 | )
18 | }
19 | ```
20 |
21 | or as a standalone placeholder:
22 |
23 | ```jsx
24 | import React from 'react'
25 | import { Shimmer } from 'react-shimmer'
26 |
27 | function App(props) {
28 | return (
29 |
30 |
31 |
32 | )
33 | }
34 | ```
35 |
36 | ### Properties
37 |
38 | Property | Type | Required | Default value | Description
39 | :--- | :--- | :--- | :--- | :---
40 | `width`|number|yes||
41 | `height`|number|yes||
42 | `className`|string|no|| Override default styles with className
43 | `duration`|number|no|`1600`| Animation duration (ms)
44 | -----
45 |
46 | ## License
47 |
48 | MIT © [gokcan](https://github.com/gokcan)
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Gökcan Değirmenci
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 |
--------------------------------------------------------------------------------
/src/loaders/shimmer/Shimmer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import clsx from 'clsx';
4 |
5 | import './styles.css';
6 |
7 | const DEFAULT_DURATION_MS = 1600;
8 | const DEFAULT_HEIGHT = 400;
9 | const DEFAULT_WIDTH = 400;
10 |
11 | export interface ShimmerProps {
12 | height: number;
13 | width: number;
14 | className?: string;
15 | duration?: number;
16 | }
17 |
18 | const calcShimmerStyle = (
19 | width: number,
20 | height: number,
21 | duration = DEFAULT_DURATION_MS
22 | ) => ({
23 | backgroundSize: `${width * 10}px ${height}px`,
24 | animationDuration: `${(duration / 1000).toFixed(1)}s`,
25 | });
26 |
27 | export const Shimmer = ({
28 | className,
29 | duration,
30 | height = DEFAULT_HEIGHT,
31 | width = DEFAULT_WIDTH,
32 | }: ShimmerProps) => {
33 | const shimmerStyle = calcShimmerStyle(width, height, duration);
34 | const style = { ...shimmerStyle, ...{ height, width } };
35 |
36 | return ;
37 | };
38 |
39 | Shimmer.propTypes = {
40 | height: PropTypes.number.isRequired,
41 | width: PropTypes.number.isRequired,
42 | className: PropTypes.string,
43 | duration: PropTypes.number,
44 | };
45 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11';
2 | import * as ReactDOM from 'react-dom';
3 | import * as React from 'react';
4 | import { randomBytes } from 'crypto';
5 |
6 | import { Image, Shimmer } from '../.';
7 |
8 | const getRandomSource = () =>
9 | `https://picsum.photos/2000/1440?q=${randomBytes(8).toString('hex')}`;
10 |
11 | const App = () => {
12 | const [source, setSource] = React.useState('');
13 |
14 | React.useEffect(() => {
15 | setRandom();
16 | }, []);
17 |
18 | const setRandom = () => {
19 | setSource(getRandomSource());
20 | };
21 |
22 | return (
23 |
24 |
33 | } />
34 |
35 |
42 |
43 | );
44 | };
45 |
46 | ReactDOM.render(, document.getElementById('root'));
47 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: main
2 | on: [push]
3 | jobs:
4 | build:
5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
6 |
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | node: ['12.x', '14.x']
11 | os: [ubuntu-latest, windows-latest, macOS-latest]
12 |
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v2
16 |
17 | - name: Use Node ${{ matrix.node }}
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: ${{ matrix.node }}
21 |
22 | - name: Install deps and build (with cache)
23 | uses: bahmutov/npm-install@v1
24 |
25 | - name: Lint
26 | run: yarn lint
27 |
28 | - name: Test
29 | run: yarn test --ci --coverage --maxWorkers=2
30 |
31 | - name: Build
32 | run: yarn build
33 |
34 | publish:
35 | name: Publish to npm if merged to master
36 |
37 | runs-on: ubuntu-latest
38 |
39 | if: github.ref == 'refs/heads/master'
40 |
41 | needs: build
42 |
43 | steps:
44 | - uses: actions/checkout@v2
45 |
46 | - uses: actions/setup-node@v1
47 | with:
48 | node-version: 14
49 |
50 | - name: Install
51 | run: yarn install
52 |
53 | - name: Publish
54 | run: |
55 | npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
56 | npm publish
57 | env:
58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
59 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types"],
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "lib": ["dom", "esnext"],
6 | "importHelpers": true,
7 | // output .d.ts declaration files for consumers
8 | "declaration": true,
9 | // output .js.map sourcemap files for consumers
10 | "sourceMap": true,
11 | // match output dir to input dir. e.g. dist/index instead of dist/src/index
12 | "rootDir": "./src",
13 | // stricter type-checking for stronger correctness. Recommended by TS
14 | "strict": true,
15 | // linter checks for common issues
16 | "noImplicitReturns": true,
17 | "noFallthroughCasesInSwitch": true,
18 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | // use Node's module resolution algorithm, instead of the legacy TS one
22 | "moduleResolution": "node",
23 | // transpile JSX to React.createElement
24 | "jsx": "react",
25 | // interop between ESM and CJS modules. Recommended by TS
26 | "esModuleInterop": true,
27 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
28 | "skipLibCheck": true,
29 | // error out if import and file system have a casing mismatch. Recommended by TS
30 | "forceConsistentCasingInFileNames": true,
31 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
32 | "noEmit": true,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-shimmer",
3 | "version": "3.2.0",
4 | "description": "React Image (Suspense-like) Loader component that simulates a shimmer effect",
5 | "author": "gokcan",
6 | "keywords": [
7 | "react",
8 | "reactjs",
9 | "suspense",
10 | "react-suspense",
11 | "react-image",
12 | "loader",
13 | "loading-indicator",
14 | "shimmer",
15 | "activity-indicator",
16 | "placeholder",
17 | "spinner"
18 | ],
19 | "repository": "gokcan/react-shimmer",
20 | "license": "MIT",
21 | "main": "dist/index.js",
22 | "typings": "dist/index.d.ts",
23 | "files": [
24 | "dist",
25 | "src"
26 | ],
27 | "engines": {
28 | "node": ">=12"
29 | },
30 | "scripts": {
31 | "start": "tsdx watch",
32 | "build": "tsdx build",
33 | "test": "tsdx test --passWithNoTests",
34 | "lint": "tsdx lint",
35 | "prepare": "tsdx build"
36 | },
37 | "peerDependencies": {
38 | "react": ">=16",
39 | "react-dom": ">=16"
40 | },
41 | "husky": {
42 | "hooks": {
43 | "pre-commit": "tsdx lint"
44 | }
45 | },
46 | "prettier": {
47 | "printWidth": 80,
48 | "semi": true,
49 | "singleQuote": true,
50 | "trailingComma": "es5"
51 | },
52 | "module": "dist/react-shimmer.esm.js",
53 | "dependencies": {
54 | "clsx": "^1.1.0",
55 | "prop-types": "^15.7.2"
56 | },
57 | "devDependencies": {
58 | "@types/prop-types": "^15.7.4",
59 | "@types/react": "^17.0.15",
60 | "@types/react-dom": "^17.0.9",
61 | "husky": "^7.0.1",
62 | "postcss": "^8.3.6",
63 | "react": "^17.0.2",
64 | "react-dom": "^17.0.2",
65 | "rollup-plugin-postcss": "^4.0.0",
66 | "tsdx": "^0.14.1",
67 | "tslib": "^2.3.0",
68 | "typescript": "^4.3.5"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | > A powerful, customisable, Suspense-like `
` component that (optionally) simulates a [**shimmer**](https://github.com/facebook/Shimmer) effect while __loading__. (with zero dependencies!).
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ### [__Live Demo__](https://codesandbox.io/s/nh9x1)
27 |
28 | 
29 |
30 | ## Install
31 |
32 | ```bash
33 | npm i react-shimmer
34 | ```
35 |
36 | or
37 |
38 | ```bash
39 | yarn add react-shimmer
40 | ```
41 |
42 | ## Usage
43 |
44 | ```jsx
45 | import React from 'react'
46 | import { Image, Shimmer } from 'react-shimmer'
47 |
48 | function App() {
49 | return (
50 |
51 | }
54 | />
55 |
56 | )
57 | }
58 | ```
59 |
60 | ```jsx
61 | import React from 'react'
62 | import { Image, Breathing } from 'react-shimmer'
63 |
64 | function App() {
65 | return (
66 |
67 | }
70 | />
71 |
72 | )
73 | }
74 | ```
75 |
76 | or you can use your custom React component as a fallback:
77 |
78 | ```jsx
79 | import React from 'react'
80 | import { Image } from 'react-shimmer'
81 |
82 | import Spinner from './Spinner'
83 |
84 | function App(props) {
85 | return (
86 |
87 | }
90 | />
91 |
92 | )
93 | }
94 | ```
95 |
96 | ### Properties
97 |
98 | Property | Type | Required | Default value | Description
99 | :--- | :--- | :--- | :--- | :---
100 | `src`|string|yes||
101 | `fallback`|ReactNode|yes||
102 | `errorFallback`|func|no||
103 | `onLoad`|func|no||
104 | `delay`|number|no|| Delay in milliseconds before showing the `fallback`
105 | `fadeIn`|bool|no|false|Use built-in fade animation on img
106 | `NativeImgProps`|React.ImgHTMLAttributes|no||
107 | -----
108 |
109 | ## Contributing
110 | ---
111 |
112 | Feel free to send PRs.
113 |
114 | ## License
115 |
116 | MIT © [gokcan](https://github.com/gokcan)
117 |
--------------------------------------------------------------------------------
/src/Image.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @class SuspenseImage
3 | * @version 3.2.0
4 | * @author github.com/gokcan
5 | */
6 |
7 | import React, { ReactNode, ImgHTMLAttributes, Component } from 'react';
8 | import PropTypes from 'prop-types';
9 | import clsx from 'clsx';
10 |
11 | import IntendedError from './IntendedError';
12 |
13 | import './anims/anims.css';
14 |
15 | export interface ImageProps {
16 | src: string;
17 | fallback: ReactNode;
18 | errorFallback?: (err: string) => ReactNode;
19 | onLoad?: (image: HTMLImageElement) => any;
20 | delay?: number;
21 | fadeIn?: boolean;
22 | NativeImgProps?: ImgHTMLAttributes;
23 | }
24 |
25 | interface State {
26 | isLoading: boolean;
27 | error?: string;
28 | }
29 |
30 | const initialState: State = {
31 | isLoading: false,
32 | error: '',
33 | };
34 |
35 | export class SuspenseImage extends Component {
36 | static propTypes = {
37 | src: PropTypes.string.isRequired,
38 | fallback: PropTypes.element.isRequired,
39 | errorFallback: PropTypes.func,
40 | onLoad: PropTypes.func,
41 | delay: PropTypes.number,
42 | fadeIn: PropTypes.bool,
43 | NativeImgProps: PropTypes.object,
44 | };
45 |
46 | state: State = { ...initialState };
47 |
48 | timeoutId?: NodeJS.Timeout;
49 | forceReject?: (reason: Error) => void;
50 | _isMounted = false;
51 |
52 | imgRef = React.createRef();
53 |
54 | componentDidMount() {
55 | this._isMounted = true;
56 | this.start();
57 | }
58 |
59 | componentDidUpdate(prevProps: ImageProps) {
60 | const { src } = this.props;
61 | if (src && src !== prevProps.src) {
62 | this.safeClearTimeout();
63 | this.forceReject && this.forceReject(new IntendedError());
64 | this.setState({ ...initialState }, () => this.start());
65 | }
66 | }
67 |
68 | componentWillUnmount() {
69 | this._isMounted = false;
70 | this.forceReject = undefined;
71 | this.safeClearTimeout();
72 | }
73 |
74 | private start = async () => {
75 | const { src, fallback, delay } = this.props;
76 | if (!src || !fallback) {
77 | const errorMessage = 'src and fallback props must be provided.';
78 | if (process.env.NODE_ENV !== 'production') {
79 | console.error(errorMessage);
80 | }
81 | this.setState({ error: errorMessage });
82 | return;
83 | }
84 | /*
85 | * To avoid instant loading 'flash' while downloading images with high-speed internet connection
86 | * (or downloading smaller images that do not need much loading-time),
87 | * user may want to give delay before starting to show the loading indicator.
88 | */
89 | if (delay && delay > 0) {
90 | this.timeoutId = setTimeout(() => {
91 | this.timeoutId = undefined;
92 | if (!this.state.error && this._isMounted) {
93 | this.setState({ isLoading: true });
94 | }
95 | }, delay);
96 | } else {
97 | this.setState({ isLoading: true });
98 | }
99 |
100 | this.tryLoadImage();
101 | };
102 |
103 | private loadImage = async (): Promise => {
104 | const img = this.imgRef.current;
105 |
106 | if (!img) {
107 | return;
108 | }
109 |
110 | const { onLoad } = this.props;
111 | return new Promise((resolve, reject) => {
112 | this.forceReject = reject;
113 |
114 | const onResolve = async () => {
115 | if (img.decode !== undefined) {
116 | try {
117 | await img.decode();
118 | } catch (error) {
119 | if (process.env.NODE_ENV !== 'production') {
120 | console.error(
121 | 'An Error occurred while trying to decode an image',
122 | error
123 | );
124 | }
125 | }
126 | }
127 | resolve();
128 | if (onLoad) {
129 | onLoad(img);
130 | }
131 | };
132 |
133 | const onReject = () => {
134 | reject(
135 | new Error('An Error occurred while trying to download an image')
136 | );
137 | };
138 |
139 | if (img.complete) {
140 | onResolve();
141 | } else {
142 | img.onload = onResolve;
143 | }
144 | img.onerror = onReject;
145 | });
146 | };
147 |
148 | private tryLoadImage = async (): Promise => {
149 | try {
150 | await this.loadImage();
151 | if (this._isMounted) {
152 | this.setState({ isLoading: false });
153 | }
154 | } catch (error) {
155 | // If this is an intended(forced) rejection, don't make it visible to user.
156 | if (!(error instanceof IntendedError) && this._isMounted) {
157 | this.setState({ error, isLoading: false });
158 | }
159 | }
160 | };
161 |
162 | private safeClearTimeout() {
163 | if (this.timeoutId) {
164 | clearTimeout(this.timeoutId);
165 | this.timeoutId = undefined;
166 | }
167 | }
168 |
169 | render() {
170 | const { error, isLoading } = this.state;
171 | const { src, fallback, errorFallback, fadeIn, NativeImgProps } = this.props;
172 | const { className, ...stripClassname } = NativeImgProps || {};
173 |
174 | if (isLoading) {
175 | return fallback;
176 | } else if (error) {
177 | return errorFallback ? (
178 | errorFallback(error)
179 | ) : (
180 |
181 | ❌
182 |
183 | );
184 | } else if (src) {
185 | return (
186 |
198 | );
199 | }
200 |
201 | return null;
202 | }
203 | }
204 |
205 | export { SuspenseImage as Image };
206 |
--------------------------------------------------------------------------------