├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .storybook └── config.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── UPGRADING.md ├── package.json ├── src ├── components │ ├── DefaultWrapper.js │ ├── Lazy.js │ ├── LazyChild.js │ ├── LazyGroup.js │ └── __tests__ │ │ ├── DefaultWrapper.spec.js │ │ ├── Lazy.spec.js │ │ ├── LazyGroup.spec.js │ │ └── __snapshots__ │ │ ├── DefaultWrapper.spec.js.snap │ │ ├── Lazy.spec.js.snap │ │ └── LazyGroup.spec.js.snap ├── index.js └── lib │ └── wrap.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@researchgate/babel-preset-rg"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | # top-most EditorConfig file 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | insert_final_newline = false 9 | indent_style = space 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{js,jsx,scss}] 15 | charset = utf-8 16 | insert_final_newline = true 17 | indent_size = 4 18 | 19 | [{*.json,.travis.yml,.*rc}] 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "@researchgate/rg-react", 3 | "parser": "babel-eslint", 4 | "settings": { 5 | "import/extensions": [ 6 | ".js", 7 | ".jsx" 8 | ], 9 | "react": { 10 | "version": "detect" 11 | } 12 | }, 13 | "rules": { 14 | "semi": [ 15 | "error", 16 | "never" 17 | ], 18 | "import/extensions": ["error", "always", { "js": "never", "jsx": "never" }], 19 | "react/jsx-boolean-value": "never" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | yarn-error.log 4 | /lib 5 | coverage 6 | docs/static 7 | docs/favicon* 8 | docs/*.html 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "jsxBracketSameLine": false, 4 | "overrides": [{ 5 | "files": "*.json", 6 | "options": { 7 | "tabWidth": 2 8 | } 9 | }], 10 | "printWidth": 120, 11 | "semi": false, 12 | "singleQuote": true, 13 | "tabWidth": 4, 14 | "trailingComma": "all", 15 | "useTabs": false 16 | } 17 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merri/react-lazy/f66de17aa0ef6c550106d7caa4ad02a320449c99/.storybook/config.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "6" 5 | cache: yarn 6 | deploy: 7 | local_dir: docs 8 | provider: pages 9 | skip_cleanup: true 10 | github_token: $GITHUB_TOKEN # Set in travis-ci.org dashboard 11 | on: 12 | branch: master 13 | condition: '"$TRAVIS_JOB_NUMBER" == *.1' 14 | before_install: yarn global add greenkeeper-lockfile@1 15 | before_script: 16 | - greenkeeper-lockfile-update 17 | # true needs to be removed after https://github.com/conventional-changelog/conventional-github-releaser/pull/47 is fixed 18 | - 'if [[ -n "$TRAVIS_TAG" ]] && [[ "$TRAVIS_JOB_NUMBER" == *.1 ]]; then yarn run create-github-release || true; fi' 19 | script: 20 | - yarn run lint 21 | - yarn test -- --ci --coverage 22 | - 'if [[ "$TRAVIS_JOB_NUMBER" == *.1 ]]; then yarn run build:storybook; fi' 23 | after_script: greenkeeper-lockfile-upload 24 | after_success: 25 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [1.1.0] - 2019-04-26 9 | 10 | ### Fixed 11 | - Allow blocking lazy load in `onViewport` events by using `preventDefault()` 12 | - No longer re-render as `noscript` if setting `visible` to false after element has come to viewport 13 | - `component` propTypes have been fixed to `any` 14 | - `clientOnly` works now correctly with `LazyGroup` 15 | 16 | ### Changed 17 | - `ref` is now passed through 18 | - Build and test system has been replaced with a less clunky modern one 19 | - `Lazy`'s render method has been compacted to shorter code 20 | 21 | ### Removed 22 | - Support for old versions of React, now minimum of 16.4 required 23 | 24 | 25 | ## [1.0.3] - 2018-06-13 26 | 27 | ### Changed 28 | - Now uses `@researchgate/react-intersection-observer` instead of customized implementation 29 | 30 | ### Fixed 31 | - Case where `viewport` and `threshold` were not passed to `Observer` in `Lazy` 32 | 33 | 34 | ## [1.0.2] - 2018-05-31 35 | 36 | ### Changed 37 | - Size of final generated JavaScript has been reduced 38 | - Minor code speed optimizations 39 | 40 | ### Removed 41 | - `DefaultWrapper` and `LazyChild` are no longer exported 42 | 43 | 44 | ## [1.0.1] - 2018-05-30 45 | 46 | ### Changed 47 | - React version requirement to 16 48 | 49 | ### Removed 50 | - Symbol polyfill requirement 51 | 52 | 53 | ## [1.0.0] - 2018-05-30 54 | 55 | ### Added 56 | - `Observer` component 57 | - `threshold` prop to `Lazy` and `LazyGroup` 58 | - `viewport` prop to `Lazy` and `LazyGroup` which works like `root` of IntersectionObserver 59 | 60 | ### Changed 61 | - `cushion` prop works now like `marginRoot` of IntersectionObserver 62 | - `jsOnly` prop is now `clientOnly` 63 | - Viewport detection to use IntersectionObserver API with code heavily based on 64 | [@researchgate/react-intersection-observer](https://github.com/researchgate/react-intersection-observer) 65 | - `LazyGroup` no longer extends from `Lazy` as both use `Observer` now 66 | - `onViewport` receives intersection observer event as first parameter 67 | 68 | ### Removed 69 | - `checkElementsInViewport` 70 | 71 | ### Fixed 72 | - Production build no longer requires `PropTypes` 73 | 74 | 75 | ## [0.6.1] - 2018-05-26 76 | 77 | - Fix dependency issue with `uglifyjs-webpack-plugin` 78 | 79 | 80 | ## [0.6.0] - 2018-05-26 81 | 82 | - Update to React 16.4 83 | - Use throttle instead of debounce 84 | - Update to Webpack 4 and other latest packages (except for the tests because I am lazy) 85 | 86 | 87 | ## [0.5.1] - 2017-11-14 88 | 89 | - Update to React 16.1.1 90 | - Update to Node 8 LTS; run Travis in it and include a `package-lock.json` 91 | - Fix render mismatch in certain cases 92 | 93 | 94 | ## [0.5.0] - 2017-11-07 95 | 96 | - Update to React 16 97 | - Add `jsOnly` option as a solution for less [React 16.0.0 errors](https://github.com/facebook/react/issues/10993) 98 | 99 | 100 | ## [0.4.1] - 2017-09-14 101 | 102 | - Update dependencies 103 | - Fix linting issues 104 | - Add event listener helpers 105 | - Use passive listeners 106 | - Add wheel event to listened events 107 | 108 | 109 | ## [0.4.0] - 2017-07-23 110 | 111 | - Remove `imgWrapperComponent` as it's development went only half-way to completion 112 | - Add `LazyGroup`, `LazyChild` and `DefaultWrapper` components 113 | 114 | 115 | ## [0.3.1] - 2017-07-18 116 | 117 | - Fix non-`ltIE9` not loading images 118 | 119 | 120 | ## [0.3.0] - 2017-07-18 121 | 122 | - Disable IE conditional comment rendering by default 123 | - Add `ltIE9` to enable IE conditional comment rendering 124 | - Update dependencies 125 | - Fix `React.DOM.div` warnings 126 | 127 | 128 | ## [0.2.1] - 2017-05-12 129 | 130 | - Fix issue where images loaded when `display: none` was set to hide an element 131 | - Introduces issue where having lazy loading on `position: fixed` elements does not work 132 | 133 | 134 | ## [0.2.0] - 2017-05-12 135 | 136 | - Upgrade to React 15.5 137 | - Babel 6 138 | - Webpack 2 139 | - Convert from createClass to ES6 class 140 | - Modularize 141 | - Test Travis CI in Node 6.10 142 | - Update linting 143 | - Change `nodeName` to `component` and allow for more than string tags 144 | - Fix bug in cushion handling 145 | - Add support for `imgWrapperComponent` which renders given component around all noscripted img childs 146 | - No longer expose `verge` 147 | 148 | 149 | ## [0.1.0] - 2016-04-11 150 | 151 | - Upgrade to React 0.14 152 | 153 | 154 | ## [0.0.3] - 2015-09-11 155 | 156 | - Fix issue #1: require loaded client side only intended minified version due to wrong reference in package.json 157 | 158 | 159 | ## [0.0.2] - 2015-07-29 160 | 161 | - Use debounce instead of throttle. 162 | - Expose checkElementsInViewport to allow manual triggers. 163 | 164 | 165 | ## [0.0.1] - 2015-07-28 166 | 167 | - Initial release. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 - 2019 Vesa Piittinen 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-lazy 2 | [![Version](https://img.shields.io/npm/v/react-lazy.svg)](https://www.npmjs.com/package/react-lazy) 3 | [![Build Status](https://travis-ci.org/Merri/react-lazy.svg)](https://travis-ci.org/Merri/react-lazy) 4 | 5 | > Lazy load your content without breaking the internet! 6 | 7 | Supports universal rendering including disabled JavaScript by using `noscript` elements that are also friendly to all 8 | search engines. Uses modern IntersectionObserver API using the excellent 9 | [@researchgate/react-intersection-observer](https://github.com/researchgate/react-intersection-observer). 10 | 11 | Also optionally supports displaying the content on IE8 and earlier by adding conditional comments to skip `noscript` 12 | elements. 13 | 14 | [View lazy loading demo](https://merri.github.io/react-lazy/) 15 | 16 | ```js 17 | npm install react-lazy 18 | 19 | import { Lazy } from 'react-lazy' 20 | // or 21 | import { LazyGroup } from 'react-lazy' 22 | ``` 23 | 24 | You also need to polyfill `intersection-observer`! Use polyfill.io or `require('intersection-observer')`. Check 25 | [Can I use](https://caniuse.com/#feat=intersectionobserver) for browser support status. `Map` and `Set` are also 26 | required, but these are required by React as well. 27 | 28 | 29 | ---- 30 | 31 | ## Why `react-lazy`? 32 | 33 | 1. Minimalistic and performant implementation with less dependencies than other solutions 34 | 2. You can choose between ease-of-use (LazyGroup) and do-it-yourself (Lazy) 35 | 3. The hard part of handling `noscript` is done for you 36 | 37 | 38 | ---- 39 | 40 | ### Why lazy load content such as images? 41 | 42 | You want to save your bandwidth and/or server load. As a side effect you may also gain some performance benefits on 43 | client side, especially on mobile devices. However the main benefit (and main purpose) for you should always be the 44 | reduction of bandwidth/server load. 45 | 46 | Likely side effect of lazy loading is that user may see content flashing as it comes into view; sometimes with a lot of 47 | delay as it depends on connectivity. You can make the experience less flickery by adding a transition when image is 48 | loaded (a bit harder to develop) or by giving `Lazy` a large cushion (500 pixels or more) to load image before it is 49 | actually in the viewport. Using both strategies together is recommended. You can test the experience on your own site by 50 | dropping mobile connection to slow 3G. 51 | 52 | Chrome developer tools also has network throttling so you don't need to get yourself into a train to nowhere to test how 53 | well or poorly your site works in high latency conditions. However it is also recommended you do get yourself into a 54 | train to nowhere as it does good for your mind and soul to abandon the hectic although convenient city lifestyle every 55 | once in a while. 56 | 57 | 58 | ---- 59 | 60 | ## Usage: `` 61 | 62 | ```jsx 63 | // curly brackets are required 64 | import { Lazy } from 'react-lazy' 65 | 66 | ... 67 | 68 | 69 | My Lazy Loaded Image 70 | 71 | ``` 72 | 73 | ```html 74 | 75 | 76 | 79 | 80 | 81 | 82 | 83 | My Lazy Loaded Image 84 | 85 | ``` 86 | 87 | 88 | ---- 89 | 90 | ## Component introduction 91 | 92 | There are two components: `` and ``. 93 | 94 | **Lazy** provides basic functionality for lazy loading: it keeps render in `noscript` element until it has come into 95 | viewport and then simply swaps render. **Everything** inside the component is wrapped into `noscript`. As the component 96 | is quite simple and generic it doesn't support many other things that provide convenience; for example, with images you 97 | have to write your own logic for handling `onError` and `onLoad` so that you can do things like trigger transitions as 98 | images are loaded, or change what to render instead of the image if loading the image fails. 99 | 100 | **LazyGroup** extends `Lazy` functionality by wrapping only specified component types inside `noscript`. So only the 101 | specified components like `img` or `iframe` elements are wrapped to `noscript`. Other elements are simply rendered 102 | as-is. 103 | 104 | The wrappable components (`img`s and `iframe`s by default) are also always wrapped inside another component. This custom 105 | component will receive information on whether loading the `img` or `iframe` has succeeded or failed, thus allowing a 106 | single place to control lifecycles as images or other content is loaded. 107 | 108 | 109 | ## Shared features 110 | 111 | These features are supported by both `` and ``. 112 | 113 | 114 | ### IntersectionObserver props 115 | 116 | - `viewport` (= `root` option) 117 | - `cushion` (= `rootMargin` option) 118 | - `threshold` 119 | 120 | These props work like you would expect them to work with IntersectionObserver. 121 | 122 | 123 | ### `clientOnly` prop 124 | 125 | Disables `noscript` element rendering, instead rendering no HTML for the contents in server side. This gives behavior 126 | similar to most other lazy loaders, which is why it is not enabled by default in `react-lazy`. 127 | 128 | 129 | ### `ltIE9` prop 130 | 131 | Renders Internet Explorer 8 friendly syntax by adding conditional comments around `noscript`, effectively hiding 132 | existance of the tag from IE8 and earlier. This allows for minimal legacy browser support, since it is highly unlikely 133 | anyone writes their JS to execute on IE8 anymore. 134 | 135 | Essentially this feature allows to render a visually non-broken page to users of legacy browsers, making it possible to 136 | give minimally acceptable user experience to users of browsers that should be dead. 137 | 138 | This means there is no lazy rendering on legacy browsers, images load immediately. 139 | 140 | This prop has no effect if `clientOnly` is enabled. 141 | 142 | 143 | ### `onLoad` 144 | 145 | - On `Lazy` triggers after removing `noscript` element. 146 | - On `LazyGroup` triggers after **all** wrapped child components `onLoad` or `onError` events have triggered. 147 | 148 | ```jsx 149 | ... 150 | ``` 151 | 152 | 153 | ### `onViewport` 154 | 155 | Triggers before removing `noscript` elements. Given function receives IntersectionObserver event object. 156 | 157 | 158 | ### `visible` 159 | 160 | Allows you to manually tell if the element is actually visible to the user or not. 161 | 162 | 163 | ---- 164 | 165 | ## `` 166 | 167 | `Lazy` works fine with single images, but sometimes you may want to have slightly more control or better performance 168 | when you know multiple images load at the same time (for example, a row of thumbnails). In this case it makes no sense 169 | to check each individual image's position in viewport when checking for just the container component will be good enough 170 | — and also less for a browser to execute. 171 | 172 | You can also use `Lazy` for multiple images, but there are some practical limitations such as the fact that everything 173 | inside `Lazy` is within `noscript` element, thus there is nothing rendered inside. `LazyGroup` solves this issue by 174 | rendering `noscript` only around specific wrapped elements (`img` and `iframe` by default). Also, further control is 175 | given with `childWrapper` component that will receive a set of props to make life easier. 176 | 177 | Use cases: 178 | 179 | 1. You want all contained images/iframes to be transitioned at the exact same time after everything is loaded. 180 | 2. You want to use the abstraction provided by `childWrapper` instead of writing custom logic. 181 | 3. You want to have slightly better performance by only checking the container element's location relative to the view. 182 | 183 | 184 | ### Usage 185 | 186 | ```jsx 187 | // curly brackets are required 188 | import { LazyGroup } from 'react-lazy' 189 | 190 | function ImageContainer({ childProps, children, isFailed, isLoaded, ...props }) { 191 | return ( 192 | // usually the other props include `dangerouslySetInnerHtml` when rendering `noscript` element 193 |
194 | {isFailed ? 'The image did not load :( ' + childProps.src : children} 195 |
196 | ) 197 | } 198 | 199 | ... 200 | 201 | 202 | {this.props.images.map((image, index) => 203 |
  • 204 | 205 |
  • 206 | )} 207 |
    208 | ``` 209 | 210 | ### `childWrapper` lifecycle 211 | 212 | 1. On server side render and before the `LazyGroup` container is in viewport in client `childWrapper` will receive 213 | `dangerouslySetInnerHtml` prop (thus rendering `noscript` element that contains the lazily loaded content). 214 | 2. After coming into viewport `isFailed` and `isLoaded` are false. `childProps` also become available. 215 | 3. `isFailed` is set to true when `img`'s or `iframe`'s `onError` event triggers. You can use `childProps` to decide 216 | what to render. 217 | 4. `isLoaded` is set to true when `img`'s or `iframe`'s `onLoad` event triggers. 218 | 219 | 220 | ### `childrenToWrap` 221 | 222 | Use this array to decide which components are wrapped by `childWrapper`. Default value: `['iframe', 'img']` 223 | 224 | **Note!** The components **must** support `onError` and `onLoad` events as these are used to detect loading. 225 | 226 | 227 | ---- 228 | 229 | ## Other components 230 | 231 | You can see these components via React developer tools, but as of 1.0.2 they have not been exposed. 232 | 233 | 234 | ### `DefaultWrapper` 235 | 236 | This is the `childWrapper` used to render `LazyGroup`'s wrapped childs if no custom wrapper is given. The wrapper is a 237 | simple div with a `className` of `react-lazy-wrapper`. BEM convention is used to tell about the lifecycle: 238 | 239 | 1. `react-lazy-wrapper--placeholder` is set on server render and client render before `LazyGroup` is in viewport. 240 | 2. `react-lazy-wrapper--loading` is set once `LazyGroup` is in viewport. 241 | 3. `react-lazy-wrapper--failed` is set if lazy loaded component's `onError` event has triggered. 242 | 4. `react-lazy-wrapper--loaded` is set if lazy loaded component's `onLoad` event has triggered. 243 | 244 | 245 | ### `LazyChild` 246 | 247 | This is the component used by `LazyGroup` to handle rendering of the wrapped child components. It manages the `onLoad` / 248 | `onError` handling. It takes two props: `callback` and `wrapper`. `callback` is called by `LazyChild` once loading 249 | result has been resolved. `wrapper` is the component rendered around wrapped child element. 250 | -------------------------------------------------------------------------------- /docs/UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading from v0.6.1 to v1.0.0 2 | 3 | `react-lazy` has a lot of changes in the v1 release as it now uses the new IntersectionObserver API with code that is 4 | heavily based on work at 5 | [@researchgate/react-intersection-observer](https://github.com/researchgate/react-intersection-observer). For the most 6 | part `react-lazy`'s implementation is identical, but there are minor optimizations, removal of some dependancies and 7 | `viewport` and `cushion` props are preferred over `root` and `rootMargin` props (however latter are also supported). 8 | 9 | 10 | ## Changes in props passed to `Lazy` and `LazyGroup` 11 | 12 | - `cushion` is no longer integer value indicating pixels around element but instead `rootMargin` around the viewport 13 | `root` element. 14 | - `jsOnly` has been renamed as `clientOnly`. 15 | - `threshold` prop has been added; it works just like the one in `Observer`. 16 | - `viewport` prop has been added; it is the `root` option passed to IntersectionObserver. 17 | 18 | 19 | ## Other notable changes 20 | 21 | - `Observer` component has been added and exposed 22 | - `checkElementsInViewport` has been removed: it is unnecessary with IntersectionObserver. 23 | - `LazyGroup` no longer extends nor depends on `Lazy`. 24 | - `PropTypes` is no longer required in production build. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lazy", 3 | "description": "Universally rendering, lightweight and performant components for lazy loading with the noscript technique", 4 | "version": "1.1.0", 5 | "author": "Vesa Piittinen ", 6 | "bugs": { 7 | "url": "https://github.com/merri/react-lazy/issues" 8 | }, 9 | "devDependencies": { 10 | "@researchgate/babel-preset-rg": "^1.0.1", 11 | "@researchgate/eslint-config-rg-react": "^2.0.0", 12 | "@storybook/addon-actions": "^3.2.16", 13 | "@storybook/addon-knobs": "^3.4.4", 14 | "@storybook/addon-options": "^3.2.16", 15 | "@storybook/react": "^3.2.16", 16 | "babel-cli": "^6.24.1", 17 | "babel-eslint": "^8.0.2", 18 | "babel-jest": "^21.0.0", 19 | "babel-polyfill": "^6.26.0", 20 | "conventional-github-releaser": "^2.0.0", 21 | "cross-env": "^5.1.1", 22 | "css-loader": "^0.28.4", 23 | "eslint": "^4.11.0", 24 | "eslint-config-prettier": "^2.8.0", 25 | "eslint-plugin-import": "^2.8.0", 26 | "eslint-plugin-prettier": "^2.3.0", 27 | "eslint-plugin-react": "^7.5.1", 28 | "husky": "^0.14.3", 29 | "jest": "^21.0.0", 30 | "lint-staged": "^6.0.0", 31 | "prettier": "^1.8.2", 32 | "raf": "^3.4.0", 33 | "raw-loader": "^1.0.0-beta.0", 34 | "react": "^16.4.0", 35 | "react-dom": "^16.4.0", 36 | "react-test-renderer": "^16.4.0", 37 | "rimraf": "^2.6.1", 38 | "standard-version": "^4.2.0", 39 | "storybook-readme": "^3.1.1", 40 | "style-loader": "^0.19.0", 41 | "validate-commit-msg": "^2.14.0" 42 | }, 43 | "files": [ 44 | "lib" 45 | ], 46 | "homepage": "https://github.com/merri/react-lazy#readme", 47 | "keywords": [ 48 | "react", 49 | "react-component", 50 | "react-lazy-load", 51 | "lazy", 52 | "lazy-load", 53 | "lazyload", 54 | "lazy loading", 55 | "load", 56 | "isomorphic", 57 | "render", 58 | "universal", 59 | "performance", 60 | "invisible", 61 | "viewport", 62 | "visible" 63 | ], 64 | "license": "MIT", 65 | "lint-staged": { 66 | "{src,docs/docs}/**/*.js": [ 67 | "eslint --fix", 68 | "git add" 69 | ] 70 | }, 71 | "main": "lib/js/index.js", 72 | "module": "lib/es/index.js", 73 | "peerDependencies": { 74 | "prop-types": "^15.6.0", 75 | "react": ">=16.4", 76 | "react-dom": ">=16.4" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "https://github.com/merri/react-lazy.git" 81 | }, 82 | "jest": { 83 | "rootDir": "src", 84 | "testMatch": [ 85 | "**/__tests__/**/*.spec.js" 86 | ], 87 | "setupFiles": [ 88 | "raf/polyfill" 89 | ] 90 | }, 91 | "scripts": { 92 | "build": "yarn build:js && yarn build:es", 93 | "build:js": "cross-env BABEL_ENV=production BABEL_OUTPUT=cjs babel src --out-dir lib/js --ignore __tests__ --copy-files", 94 | "build:es": "cross-env BABEL_ENV=production BABEL_OUTPUT=esm babel src --out-dir lib/es --ignore __tests__ --copy-files", 95 | "build:storybook": "build-storybook --output-dir docs", 96 | "clear": "rimraf ./lib", 97 | "commitmsg": "validate-commit-msg", 98 | "coverage": "yarn test -- --coverage", 99 | "create-github-release": "conventional-github-releaser -p angular", 100 | "format": "eslint --fix {src,docs/docs}/**/*.js", 101 | "lint": "eslint {src,docs/docs}/.", 102 | "precommit": "yarn lint-staged && yarn test", 103 | "prepare": "yarn clear && yarn build", 104 | "prepublishOnly": "yarn test", 105 | "release": "standard-version", 106 | "storybook": "start-storybook -p 9001 -c .storybook", 107 | "test": "jest" 108 | }, 109 | "dependencies": { 110 | "@researchgate/react-intersection-observer": "^0.7.3" 111 | } 112 | } -------------------------------------------------------------------------------- /src/components/DefaultWrapper.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-template */ 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | function DefaultWrapper({ childProps, children, isFailed, isLoaded, ...props }) { 6 | const className = 7 | 'react-lazy-wrapper' + 8 | (!isLoaded && !isFailed ? ` react-lazy-wrapper--${childProps ? 'loading' : 'placeholder'}` : '') + 9 | (isFailed ? ' react-lazy-wrapper--failed' : '') + 10 | (isLoaded ? ' react-lazy-wrapper--loaded' : '') + 11 | (props.className ? ' ' + props.className : '') 12 | 13 | return ( 14 |
    15 | {children} 16 |
    17 | ) 18 | } 19 | 20 | DefaultWrapper.propTypes = { 21 | children: PropTypes.node, 22 | childProps: PropTypes.object, 23 | className: PropTypes.string, 24 | isFailed: PropTypes.bool, 25 | isLoaded: PropTypes.bool, 26 | } 27 | 28 | export default DefaultWrapper 29 | -------------------------------------------------------------------------------- /src/components/Lazy.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Observer from '@researchgate/react-intersection-observer' 5 | 6 | import { propsWithNoScriptRender } from '../lib/wrap' 7 | 8 | class Lazy extends React.PureComponent { 9 | constructor(props) { 10 | super(props) 11 | 12 | this.state = { show: false } 13 | 14 | this.handleViewport = this.handleViewport.bind(this) 15 | } 16 | 17 | handleViewport(event, unobserve) { 18 | if (!event.isIntersecting || !this.props.visible) { 19 | return 20 | } 21 | 22 | if (this.props.onViewport) { 23 | this.props.onViewport(event) 24 | } 25 | 26 | if (event.defaultPrevented) { 27 | return 28 | } 29 | 30 | unobserve() 31 | 32 | this.setState({ show: true }, this.props.onLoad) 33 | } 34 | 35 | render() { 36 | const { 37 | children, 38 | clientOnly, 39 | component, 40 | cushion, 41 | forwardedRef: ref, 42 | ltIE9, 43 | visible, 44 | onLoad, 45 | onViewport, 46 | threshold, 47 | viewport, 48 | ...rest 49 | } = this.props 50 | 51 | const props = Object.assign({ ref }, rest) 52 | const isClientRender = clientOnly || this.state.show 53 | 54 | return ( 55 | 56 | {React.createElement( 57 | component, 58 | isClientRender ? props : propsWithNoScriptRender(children, ltIE9, props), 59 | isClientRender && this.state.show && visible ? children : null, 60 | )} 61 | 62 | ) 63 | } 64 | } 65 | 66 | Lazy.defaultProps = { 67 | clientOnly: false, 68 | component: 'div', 69 | ltIE9: false, 70 | visible: true, 71 | } 72 | 73 | Lazy.propTypes = { 74 | clientOnly: PropTypes.bool, 75 | children: PropTypes.node, 76 | component: PropTypes.any, 77 | cushion: PropTypes.string, 78 | forwardedRef: PropTypes.any, 79 | ltIE9: PropTypes.bool, 80 | onLoad: PropTypes.func, 81 | onViewport: PropTypes.func, 82 | threshold: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]), 83 | viewport: PropTypes.oneOfType( 84 | [PropTypes.string].concat(typeof HTMLElement === 'undefined' ? [] : PropTypes.instanceOf(HTMLElement)), 85 | ), 86 | visible: PropTypes.bool, 87 | } 88 | 89 | // eslint-disable-next-line react/display-name,react/no-multi-comp 90 | export default React.forwardRef((props, ref) => ) 91 | -------------------------------------------------------------------------------- /src/components/LazyChild.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | function getProps(children, prop) { 5 | if (!children || !children.props) return null 6 | const { onError, onLoad, ...childProps } = children.props 7 | switch (prop) { 8 | case 'onError': 9 | return onError 10 | case 'onLoad': 11 | return onLoad 12 | default: 13 | return childProps 14 | } 15 | } 16 | 17 | class LazyChild extends React.PureComponent { 18 | constructor(props) { 19 | super(props) 20 | 21 | this.state = { isFailed: false, isLoaded: false } 22 | 23 | this.handleError = this.handleError.bind(this) 24 | this.handleLoad = this.handleLoad.bind(this) 25 | } 26 | 27 | componentDidMount() { 28 | this.setState({ childProps: getProps(this.props.children) }) 29 | } 30 | 31 | componentDidUpdate(prevProps) { 32 | if (prevProps.children === this.props.children) { 33 | return 34 | } 35 | 36 | this.setState({ childProps: getProps(this.props.children) }) 37 | } 38 | 39 | handleError(event) { 40 | this.setState({ isFailed: true }, this.props.callback) 41 | 42 | const onError = getProps(this.props.children, 'onError') 43 | if (onError) onError(event) 44 | } 45 | 46 | handleLoad(event) { 47 | this.setState({ isLoaded: true }, this.props.callback) 48 | 49 | const onLoad = getProps(this.props.children, 'onLoad') 50 | if (onLoad) onLoad(event) 51 | } 52 | 53 | render() { 54 | const { callback, children, wrapper, ...props } = this.props 55 | 56 | const child = children ? React.Children.only(children) : null 57 | 58 | return React.createElement( 59 | wrapper, 60 | { ...props, ...this.state }, 61 | child && !this.state.isFailed && !this.state.isLoaded 62 | ? React.cloneElement(child, { onError: this.handleError, onLoad: this.handleLoad }) 63 | : child, 64 | ) 65 | } 66 | } 67 | 68 | LazyChild.propTypes = { 69 | callback: PropTypes.func, 70 | children: PropTypes.node, 71 | wrapper: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired, 72 | } 73 | 74 | export default LazyChild 75 | -------------------------------------------------------------------------------- /src/components/LazyGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { countTypesTags, wrapTypesToLazyChild, wrapTypesToNoScript } from '../lib/wrap' 5 | 6 | import DefaultWrapper from './DefaultWrapper' 7 | import Observer from '@researchgate/react-intersection-observer' 8 | 9 | class LazyGroup extends React.PureComponent { 10 | constructor(props) { 11 | super(props) 12 | 13 | this.loadedImgTags = 0 14 | this.state = { imgTagCount: null, loadedAt: null, viewportAt: null } 15 | 16 | this.handleViewport = this.handleViewport.bind(this) 17 | this.handleImgLoaded = this.handleImgLoaded.bind(this) 18 | } 19 | 20 | handleImgLoaded() { 21 | this.loadedImgTags++ 22 | 23 | if (this.loadedImgTags === this.state.imgTagCount) { 24 | this.loadedImgTags = 0 25 | this.setState({ imgTagCount: null, loadedAt: Date.now() }, this.props.onLoad) 26 | } 27 | } 28 | 29 | handleViewport(event, unobserve) { 30 | const { children, childrenToWrap, onLoad, onViewport, visible } = this.props 31 | 32 | if (!event.isIntersecting || !visible) { 33 | return 34 | } 35 | 36 | if (onViewport) { 37 | onViewport(event) 38 | } 39 | 40 | if (event.defaultPrevented) { 41 | return 42 | } 43 | 44 | unobserve() 45 | 46 | const imgTagCount = countTypesTags(childrenToWrap, children) || null 47 | this.loadedImgTags = 0 48 | const viewportAt = Date.now() 49 | this.setState( 50 | { imgTagCount, loadedAt: !imgTagCount ? viewportAt : null, viewportAt }, 51 | !imgTagCount ? onLoad : null, 52 | ) 53 | } 54 | 55 | render() { 56 | const { 57 | children, 58 | childrenToWrap, 59 | childWrapper, 60 | clientOnly, 61 | component, 62 | cushion, 63 | forwardedRef: ref, 64 | ltIE9, 65 | onLoad, 66 | onViewport, 67 | threshold, 68 | viewport, 69 | visible, 70 | ...rest 71 | } = this.props 72 | 73 | const props = Object.assign({ ref }, rest) 74 | 75 | return ( 76 | 77 | {React.createElement( 78 | component, 79 | props, 80 | // swap render once element is visible in viewport 81 | clientOnly || this.state.viewportAt 82 | ? wrapTypesToLazyChild(childrenToWrap, children, childWrapper, this.handleImgLoaded, this.state.viewportAt != null) 83 | : wrapTypesToNoScript(childrenToWrap, children, ltIE9, childWrapper), 84 | )} 85 | 86 | ) 87 | } 88 | } 89 | 90 | LazyGroup.defaultProps = { 91 | childrenToWrap: ['iframe', 'img'], 92 | childWrapper: DefaultWrapper, 93 | clientOnly: false, 94 | component: 'div', 95 | ltIE9: false, 96 | visible: true, 97 | } 98 | 99 | LazyGroup.propTypes = { 100 | clientOnly: PropTypes.bool, 101 | children: PropTypes.node, 102 | childrenToWrap: PropTypes.arrayOf(PropTypes.any), 103 | childWrapper: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), 104 | component: PropTypes.any, 105 | cushion: PropTypes.string, 106 | forwardedRef: PropTypes.any, 107 | ltIE9: PropTypes.bool, 108 | onLoad: PropTypes.func, 109 | onViewport: PropTypes.func, 110 | threshold: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]), 111 | viewport: PropTypes.oneOfType( 112 | [PropTypes.string].concat(typeof HTMLElement === 'undefined' ? [] : PropTypes.instanceOf(HTMLElement)), 113 | ), 114 | visible: PropTypes.bool, 115 | } 116 | 117 | // eslint-disable-next-line react/display-name,react/no-multi-comp 118 | export default React.forwardRef((props, ref) => ) 119 | -------------------------------------------------------------------------------- /src/components/__tests__/DefaultWrapper.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import React from 'react' 3 | import renderer from 'react-test-renderer' 4 | 5 | import DefaultWrapper from '../DefaultWrapper' 6 | 7 | test('DefaultWrapper className indicates placeholder state by default', () => { 8 | const tree = renderer.create() 9 | expect(tree.toJSON()).toMatchSnapshot() 10 | }) 11 | 12 | test('DefaultWrapper className indicates loading state when childProps exists', () => { 13 | const tree = renderer.create() 14 | expect(tree.toJSON()).toMatchSnapshot() 15 | }) 16 | 17 | test('DefaultWrapper className indicates failed state when isFailed', () => { 18 | const tree = renderer.create() 19 | expect(tree.toJSON()).toMatchSnapshot() 20 | }) 21 | 22 | test('DefaultWrapper className indicates loaded state when isLoaded', () => { 23 | const tree = renderer.create() 24 | expect(tree.toJSON()).toMatchSnapshot() 25 | }) 26 | 27 | test('DefaultWrapper may contain children', () => { 28 | const tree = renderer.create( 29 | 30 |
    31 | , 32 | ) 33 | expect(tree.toJSON()).toMatchSnapshot() 34 | }) 35 | 36 | test('DefaultWrapper passes other props directly to the rendered div element', () => { 37 | const tree = renderer.create() 38 | expect(tree.toJSON()).toMatchSnapshot() 39 | }) 40 | -------------------------------------------------------------------------------- /src/components/__tests__/Lazy.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable react/prop-types,react/no-multi-comp */ 3 | import React from 'react' 4 | import renderer from 'react-test-renderer' 5 | 6 | import Lazy from '../Lazy' 7 | 8 | const originalIntersectionObserver = window.IntersectionObserver 9 | 10 | function noop() {} 11 | 12 | function mockIntersectionObserver() { 13 | return { 14 | disconnect: noop, 15 | observe: noop, 16 | unobserve: noop, 17 | } 18 | } 19 | 20 | const target = { nodeType: 1 } 21 | 22 | beforeAll(() => { 23 | window.IntersectionObserver = mockIntersectionObserver 24 | }) 25 | 26 | afterAll(() => { 27 | window.IntersectionObserver = originalIntersectionObserver 28 | }) 29 | 30 | describe('Lazy', () => { 31 | test('should render as div and with empty noscript by default', () => { 32 | const tree = renderer.create() 33 | expect(tree.toJSON()).toMatchSnapshot() 34 | }) 35 | 36 | test('should let change component to be rendered', () => { 37 | const tree = renderer.create() 38 | expect(tree.toJSON()).toMatchSnapshot() 39 | }) 40 | 41 | test('ltIE9 should render with IE conditional comments around noscript', () => { 42 | const tree = renderer.create() 43 | expect(tree.toJSON()).toMatchSnapshot() 44 | }) 45 | 46 | test('children should be rendered inside noscript', () => { 47 | const tree = renderer.create( 48 | 49 | I am inside noscript! 50 | , 51 | ) 52 | expect(tree.toJSON()).toMatchSnapshot() 53 | }) 54 | 55 | test('with ltIE9 children should be rendered inside noscript with conditional comments', () => { 56 | const tree = renderer.create( 57 | 58 | I am inside IECC noscript! 59 | , 60 | ) 61 | expect(tree.toJSON()).toMatchSnapshot() 62 | }) 63 | 64 | test('should render only div in clientOnly mode', () => { 65 | const tree = renderer.create() 66 | expect(tree.toJSON()).toMatchSnapshot() 67 | }) 68 | 69 | test('should call ref callback', () => { 70 | const spy = jest.fn() 71 | renderer.create(, { createNodeMock: () => target }) 72 | expect(spy).toHaveBeenCalledWith(target) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/components/__tests__/LazyGroup.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable react/prop-types,react/no-multi-comp */ 3 | import React from 'react' 4 | import renderer from 'react-test-renderer' 5 | 6 | import LazyGroup from '../LazyGroup' 7 | 8 | const originalIntersectionObserver = window.IntersectionObserver 9 | 10 | function noop() {} 11 | 12 | function mockIntersectionObserver() { 13 | return { 14 | disconnect: noop, 15 | observe: noop, 16 | unobserve: noop, 17 | } 18 | } 19 | 20 | const target = { nodeType: 1 } 21 | 22 | beforeAll(() => { 23 | window.IntersectionObserver = mockIntersectionObserver 24 | }) 25 | 26 | afterAll(() => { 27 | window.IntersectionObserver = originalIntersectionObserver 28 | }) 29 | 30 | describe('LazyGroup', () => { 31 | test('should render just a div by default', () => { 32 | const tree = renderer.create() 33 | expect(tree.toJSON()).toMatchSnapshot() 34 | }) 35 | 36 | test('should let change component to be rendered', () => { 37 | const tree = renderer.create() 38 | expect(tree.toJSON()).toMatchSnapshot() 39 | }) 40 | 41 | test('ltIE9 should render just a div by default', () => { 42 | const tree = renderer.create() 43 | expect(tree.toJSON()).toMatchSnapshot() 44 | }) 45 | 46 | test('should render span inside div by default', () => { 47 | const tree = renderer.create( 48 | 49 | I am NOT inside noscript! 50 | , 51 | ) 52 | expect(tree.toJSON()).toMatchSnapshot() 53 | }) 54 | 55 | test('childrenToWrap should wrap given component types inside noscript', () => { 56 | const tree = renderer.create( 57 | 58 | I am inside noscript! 59 |
    60 | I am not! But I am! 61 |
    62 |
    , 63 | ) 64 | expect(tree.toJSON()).toMatchSnapshot() 65 | 66 | const tree2 = renderer.create( 67 | 68 | I am inside IECC noscript! 69 |
    70 | I am not! But I am! 71 |
    72 |
    , 73 | ) 74 | expect(tree2.toJSON()).toMatchSnapshot() 75 | }) 76 | 77 | test('ltIE9 should render span inside div by default', () => { 78 | const tree = renderer.create( 79 | 80 | I am NOT inside IECC noscript! 81 | , 82 | ) 83 | expect(tree.toJSON()).toMatchSnapshot() 84 | }) 85 | 86 | test('childrenToWrap should wrap matching children in clientOnly mode', () => { 87 | const tree = renderer.create( 88 | 89 | I should be replaced by a placeholder! 90 |
    91 | Not wrapped! You can read me in the snapshot! Replaced by placeholder! 92 |
    93 |
    , 94 | ) 95 | expect(tree.toJSON()).toMatchSnapshot() 96 | }) 97 | 98 | test('should call ref callback', () => { 99 | const spy = jest.fn() 100 | renderer.create(, { createNodeMock: () => target }) 101 | expect(spy).toHaveBeenCalledWith(target) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/DefaultWrapper.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DefaultWrapper className indicates failed state when isFailed 1`] = ` 4 |
    7 | `; 8 | 9 | exports[`DefaultWrapper className indicates loaded state when isLoaded 1`] = ` 10 |
    13 | `; 14 | 15 | exports[`DefaultWrapper className indicates loading state when childProps exists 1`] = ` 16 |
    19 | `; 20 | 21 | exports[`DefaultWrapper className indicates placeholder state by default 1`] = ` 22 |
    25 | `; 26 | 27 | exports[`DefaultWrapper may contain children 1`] = ` 28 |
    31 |
    32 |
    33 | `; 34 | 35 | exports[`DefaultWrapper passes other props directly to the rendered div element 1`] = ` 36 |
    40 | `; 41 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Lazy.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Lazy children should be rendered inside noscript 1`] = ` 4 |
    I am inside noscript!", 8 | } 9 | } 10 | /> 11 | `; 12 | 13 | exports[`Lazy ltIE9 should render with IE conditional comments around noscript 1`] = ` 14 |
    ", 18 | } 19 | } 20 | /> 21 | `; 22 | 23 | exports[`Lazy should let change component to be rendered 1`] = ` 24 |
    ", 28 | } 29 | } 30 | /> 31 | `; 32 | 33 | exports[`Lazy should render as div and with empty noscript by default 1`] = ` 34 |
    ", 38 | } 39 | } 40 | /> 41 | `; 42 | 43 | exports[`Lazy should render only div in clientOnly mode 1`] = `
    `; 44 | 45 | exports[`Lazy with ltIE9 children should be rendered inside noscript with conditional comments 1`] = ` 46 |
    ", 50 | } 51 | } 52 | /> 53 | `; 54 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/LazyGroup.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LazyGroup childrenToWrap should wrap given component types inside noscript 1`] = ` 4 |
    5 |
    I am inside noscript!", 10 | } 11 | } 12 | /> 13 |
    14 | I am not! 15 |
    But I am!", 20 | } 21 | } 22 | /> 23 |
    24 |
    25 | `; 26 | 27 | exports[`LazyGroup childrenToWrap should wrap given component types inside noscript 2`] = ` 28 |
    29 |
    ", 34 | } 35 | } 36 | /> 37 |
    38 | I am not! 39 |
    ", 44 | } 45 | } 46 | /> 47 |
    48 |
    49 | `; 50 | 51 | exports[`LazyGroup childrenToWrap should wrap matching children in clientOnly mode 1`] = ` 52 |
    53 |
    56 |
    57 | Not wrapped! You can read me in the snapshot! 58 |
    61 |
    62 |
    63 | `; 64 | 65 | exports[`LazyGroup ltIE9 should render just a div by default 1`] = `
    `; 66 | 67 | exports[`LazyGroup ltIE9 should render span inside div by default 1`] = ` 68 |
    69 | 70 | I am NOT inside IECC noscript! 71 | 72 |
    73 | `; 74 | 75 | exports[`LazyGroup should let change component to be rendered 1`] = `
    `; 76 | 77 | exports[`LazyGroup should render just a div by default 1`] = `
    `; 78 | 79 | exports[`LazyGroup should render span inside div by default 1`] = ` 80 |
    81 | 82 | I am NOT inside noscript! 83 | 84 |
    85 | `; 86 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as LazyGroup } from './components/LazyGroup' 2 | export { default as Lazy } from './components/Lazy' 3 | -------------------------------------------------------------------------------- /src/lib/wrap.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToStaticMarkup } from 'react-dom/server' 3 | 4 | import Lazy from '../components/Lazy' 5 | import LazyChild from '../components/LazyChild' 6 | import LazyGroup from '../components/LazyGroup' 7 | 8 | export function countTypesTags(types, children, count = 0) { 9 | if (!children) { 10 | return count 11 | } 12 | 13 | React.Children.forEach(children, child => { 14 | if (!child || child.type === Lazy || child.type === LazyGroup || child.type === LazyChild) { 15 | return 16 | } else if (types.includes(child.type)) { 17 | count++ 18 | } else { 19 | const props = child.props || {} 20 | count += countTypesTags(types, props.children) 21 | } 22 | }) 23 | 24 | return count 25 | } 26 | 27 | export function propsWithNoScriptRender(children, ltIE9, props = {}) { 28 | const noscript = renderToStaticMarkup(React.createElement('noscript', null, children)) 29 | 30 | props.dangerouslySetInnerHTML = { 31 | __html: !ltIE9 32 | ? noscript 33 | : noscript 34 | .replace(''), 36 | } 37 | 38 | return props 39 | } 40 | 41 | export function wrapTypesToLazyChild(types, children, wrapper, callback, inViewport) { 42 | if (!children) { 43 | return children 44 | } 45 | 46 | return React.Children.map(children, child => { 47 | if (!child || child.type === Lazy || child.type === LazyGroup || child.type === LazyChild) { 48 | return child 49 | } else if (types.includes(child.type)) { 50 | return ( 51 | 52 | {inViewport ? child : null} 53 | 54 | ) 55 | } 56 | const props = child.props || {} 57 | const laziedChildren = wrapTypesToLazyChild(types, props.children, wrapper, callback, inViewport) 58 | 59 | if (laziedChildren !== props.children) { 60 | return React.cloneElement(child, null, laziedChildren) 61 | } 62 | 63 | return child 64 | }) 65 | } 66 | 67 | export function wrapTypesToNoScript(types, children, ltIE9, wrapper) { 68 | if (!children) { 69 | return children 70 | } 71 | 72 | return React.Children.map(children, child => { 73 | if (!child || child.type === Lazy || child.type === LazyGroup || child.type === LazyChild) { 74 | return child 75 | } else if (types.includes(child.type)) { 76 | return React.createElement(wrapper, propsWithNoScriptRender(child, ltIE9)) 77 | } 78 | 79 | const props = child.props || {} 80 | const noscriptedChildren = wrapTypesToNoScript(types, props.children, ltIE9, wrapper) 81 | 82 | if (noscriptedChildren !== props.children) { 83 | return React.cloneElement(child, null, noscriptedChildren) 84 | } 85 | 86 | return child 87 | }) 88 | } 89 | --------------------------------------------------------------------------------