UNPKG

react-lazy-images

Version:
1 lines 20.3 kB
{"version":3,"file":"react-lazy-images.es.js","sources":["../src/LazyImageFull.tsx","../src/LazyImage.tsx"],"sourcesContent":["import React from \"react\";\nimport Observer from \"react-intersection-observer\";\nimport { unionize, ofType, UnionOf } from \"unionize\";\n\n/**\n * Valid props for LazyImage components\n */\nexport type CommonLazyImageProps = ImageProps & {\n // NOTE: if you add props here, remember to destructure them out of being\n // passed to the children, in the render() callback.\n\n /** Whether to skip checking for viewport and always show the 'actual' component\n * @see https://github.com/fpapado/react-lazy-images/#eager-loading--server-side-rendering-ssr\n */\n loadEagerly?: boolean;\n\n /** Subset of props for the IntersectionObserver\n * @see https://github.com/thebuilder/react-intersection-observer#props\n */\n observerProps?: ObserverProps;\n\n /** Use the Image Decode API;\n * The call to a new HTML <img> element’s decode() function returns a promise, which,\n * when fulfilled, ensures that the image can be appended to the DOM without causing\n * a decoding delay on the next frame.\n * @see: https://www.chromestatus.com/feature/5637156160667648\n */\n experimentalDecode?: boolean;\n\n /** Whether to log out internal state transitions for the component */\n debugActions?: boolean;\n\n /** Delay a certain duration before starting to load, in ms.\n * This can help avoid loading images while the user scrolls quickly past them.\n * TODO: naming things.\n */\n debounceDurationMs?: number;\n};\n\n/** Valid props for LazyImageFull */\nexport interface LazyImageFullProps extends CommonLazyImageProps {\n /** Children should be either a function or a node */\n children: (args: RenderCallbackArgs) => React.ReactNode;\n}\n\n/** Values that the render props take */\nexport interface RenderCallbackArgs {\n imageState: ImageState;\n imageProps: ImageProps;\n /** When not loading eagerly, a ref to bind to the DOM element. This is needed for the intersection calculation to work. */\n ref?: React.RefObject<any>;\n}\n\nexport interface ImageProps {\n /** The source of the image to load */\n src: string;\n\n /** The source set of the image to load */\n srcSet?: string;\n\n /** The alt text description of the image you are loading */\n alt?: string;\n\n /** Sizes descriptor */\n sizes?: string;\n}\n\n/** Subset of react-intersection-observer's props */\nexport interface ObserverProps {\n /**\n * Margin around the root that expands the area for intersection.\n * @see https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin\n * @default \"50px 0px\"\n * @example Declaration same as CSS margin:\n * `\"10px 20px 30px 40px\"` (top, right, bottom, left).\n */\n rootMargin?: string;\n\n /** Number between 0 and 1 indicating the the percentage that should be\n * visible before triggering.\n * @default `0.01`\n */\n threshold?: number;\n}\n\n/** States that the image loading can be in.\n * Used together with LazyImageFull render props.\n * External representation of the internal state.\n * */\nexport enum ImageState {\n NotAsked = \"NotAsked\",\n Loading = \"Loading\",\n LoadSuccess = \"LoadSuccess\",\n LoadError = \"LoadError\"\n}\n\n/** The component's state */\nconst LazyImageFullState = unionize({\n NotAsked: {},\n Buffering: {},\n // Could try to make it Promise<HTMLImageElement>,\n // but we don't use the element anyway, and we cache promises\n Loading: {},\n LoadSuccess: {},\n LoadError: ofType<{ msg: string }>()\n});\n\ntype LazyImageFullState = UnionOf<typeof LazyImageFullState>;\n\n/** Actions that change the component's state.\n * These are not unlike Actions in Redux or, the ones I'm inspired by,\n * Msg in Elm.\n */\nconst Action = unionize({\n ViewChanged: ofType<{ inView: boolean }>(),\n BufferingEnded: {},\n // MAYBE: Load: {},\n LoadSuccess: {},\n LoadError: ofType<{ msg: string }>()\n});\n\ntype Action = UnionOf<typeof Action>;\n\n/** Commands (Cmd) describe side-effects as functions that take the instance */\n// FUTURE: These should be tied to giving back a Msg / asynchronoulsy giving a Msg with conditions\ntype Cmd = (instance: LazyImageFull) => void;\n\n/** The output from a reducer is the next state and maybe a command */\ntype ReducerResult = {\n nextState: LazyImageFullState;\n cmd?: Cmd;\n};\n\n///// Commands, things that perform side-effects /////\n/** Get a command that sets a buffering Promise */\nconst getBufferingCmd = (durationMs: number): Cmd => instance => {\n // Make cancelable buffering Promise\n const bufferingPromise = makeCancelable(delayedPromise(durationMs));\n\n // Kick off promise chain\n bufferingPromise.promise\n .then(() => instance.update(Action.BufferingEnded()))\n .catch(\n _reason => {}\n //console.log({ isCanceled: _reason.isCanceled })\n );\n\n // Side-effect; set the promise in the cache\n instance.promiseCache.buffering = bufferingPromise;\n};\n\n/** Get a command that sets an image loading Promise */\nconst getLoadingCmd = (\n imageProps: ImageProps,\n experimentalDecode?: boolean\n): Cmd => instance => {\n // Make cancelable loading Promise\n const loadingPromise = makeCancelable(\n loadImage(imageProps, experimentalDecode)\n );\n\n // Kick off request for Image and attach listeners for response\n loadingPromise.promise\n .then(_res => instance.update(Action.LoadSuccess({})))\n .catch(e => {\n // If the Loading Promise was canceled, it means we have stopped\n // loading due to unmount, rather than an error.\n if (!e.isCanceled) {\n // TODO: think more about the error here\n instance.update(Action.LoadError({ msg: \"Failed to load\" }));\n }\n });\n\n // Side-effect; set the promise in the cache\n instance.promiseCache.loading = loadingPromise;\n};\n\n/** Command that cancels the buffering Promise */\nconst cancelBufferingCmd: Cmd = instance => {\n // Side-effect; cancel the promise in the cache\n // We know this exists if we are in a Buffering state\n instance.promiseCache.buffering.cancel();\n};\n\n/**\n * Component that preloads the image once it is in the viewport,\n * and then swaps it in. Takes a render prop that allows to specify\n * what is rendered based on the loading state.\n */\nexport class LazyImageFull extends React.Component<\n LazyImageFullProps,\n LazyImageFullState\n> {\n static displayName = \"LazyImageFull\";\n\n /** A central place to store promises.\n * A bit silly, but passing promsises directly in the state\n * was giving me weird timing issues. This way we can keep\n * the promises in check, and pick them up from the respective methods.\n * FUTURE: Could pass the relevant key in Buffering and Loading, so\n * that at least we know where they are from a single source.\n */\n promiseCache: {\n [key: string]: CancelablePromise;\n } = {};\n\n initialState = LazyImageFullState.NotAsked();\n\n /** Emit the next state based on actions.\n * This is the core of the component!\n */\n static reducer(\n action: Action,\n prevState: LazyImageFullState,\n props: LazyImageFullProps\n ): ReducerResult {\n return Action.match(action, {\n ViewChanged: ({ inView }) => {\n if (inView === true) {\n // If src is not specified, then there is nothing to preload; skip to Loaded state\n if (!props.src) {\n return { nextState: LazyImageFullState.LoadSuccess() }; // Error wtf\n } else {\n // If in view, only load something if NotAsked, otherwise leave untouched\n return LazyImageFullState.match(prevState, {\n NotAsked: () => {\n // If debounce is specified, then start buffering\n if (!!props.debounceDurationMs) {\n return {\n nextState: LazyImageFullState.Buffering(),\n cmd: getBufferingCmd(props.debounceDurationMs)\n };\n } else {\n // If no debounce is specified, then start loading immediately\n return {\n nextState: LazyImageFullState.Loading(),\n cmd: getLoadingCmd(props, props.experimentalDecode)\n };\n }\n },\n // Do nothing in other states\n default: () => ({ nextState: prevState })\n });\n }\n } else {\n // If out of view, cancel if Buffering, otherwise leave untouched\n return LazyImageFullState.match(prevState, {\n Buffering: () => ({\n nextState: LazyImageFullState.NotAsked(),\n cmd: cancelBufferingCmd\n }),\n // Do nothing in other states\n default: () => ({ nextState: prevState })\n });\n }\n },\n // Buffering has ended/succeeded, kick off request for image\n BufferingEnded: () => ({\n nextState: LazyImageFullState.Loading(),\n cmd: getLoadingCmd(props, props.experimentalDecode)\n }),\n // Loading the image succeeded, simple\n LoadSuccess: () => ({ nextState: LazyImageFullState.LoadSuccess() }),\n // Loading the image failed, simple\n LoadError: e => ({ nextState: LazyImageFullState.LoadError(e) })\n });\n }\n\n constructor(props: LazyImageFullProps) {\n super(props);\n this.state = this.initialState;\n\n // Bind methods\n this.update = this.update.bind(this);\n }\n\n update(action: Action) {\n // Get the next state and any effects\n const { nextState, cmd } = LazyImageFull.reducer(\n action,\n this.state,\n this.props\n );\n\n // Debugging\n if (this.props.debugActions) {\n if (process.env.NODE_ENV === \"production\") {\n console.warn(\n 'You are running LazyImage with debugActions=\"true\" in production. This might have performance implications.'\n );\n }\n console.log({ action, prevState: this.state, nextState });\n }\n\n // Actually set the state, and kick off any effects after that\n this.setState(nextState, () => cmd && cmd(this));\n }\n\n componentWillUnmount() {\n // Clear the Promise Cache\n if (this.promiseCache.loading) {\n // NOTE: This does not cancel the request, only the callback.\n // We weould need fetch() and an AbortHandler for that.\n this.promiseCache.loading.cancel();\n }\n if (this.promiseCache.buffering) {\n this.promiseCache.buffering.cancel();\n }\n this.promiseCache = {};\n }\n\n // Render function\n render() {\n // This destructuring is silly\n const {\n children,\n loadEagerly,\n observerProps,\n experimentalDecode,\n debounceDurationMs,\n debugActions,\n ...imageProps\n } = this.props;\n\n if (loadEagerly) {\n // If eager, skip the observer and view changing stuff; resolve the imageState as loaded.\n return children({\n // We know that the state tags and the enum match up\n imageState: LazyImageFullState.LoadSuccess().tag as ImageState,\n imageProps\n });\n } else {\n return (\n <Observer\n rootMargin=\"50px 0px\"\n // TODO: reconsider threshold\n threshold={0.01}\n {...observerProps}\n onChange={inView => this.update(Action.ViewChanged({ inView }))}\n >\n {({ ref }) =>\n children({\n // We know that the state tags and the enum match up, apart\n // from Buffering not being exposed\n imageState:\n this.state.tag === \"Buffering\"\n ? ImageState.Loading\n : (this.state.tag as ImageState),\n imageProps,\n ref\n })\n }\n </Observer>\n );\n }\n }\n}\n\n///// Utilities /////\n\n/** Promise constructor for loading an image */\nconst loadImage = (\n { src, srcSet, alt, sizes }: ImageProps,\n experimentalDecode = false\n) =>\n new Promise((resolve, reject) => {\n const image = new Image();\n if (srcSet) {\n image.srcset = srcSet;\n }\n if (alt) {\n image.alt = alt;\n }\n if (sizes) {\n image.sizes = sizes;\n }\n image.src = src;\n\n /** @see: https://www.chromestatus.com/feature/5637156160667648 */\n if (experimentalDecode && \"decode\" in image) {\n return (\n image\n // NOTE: .decode() is not in the TS defs yet\n // TODO: consider writing the .decode() definition and sending a PR\n //@ts-ignore\n .decode()\n .then((image: HTMLImageElement) => resolve(image))\n .catch((err: any) => reject(err))\n );\n }\n\n image.onload = resolve;\n image.onerror = reject;\n });\n\n/** Promise that resolves after a specified number of ms */\nconst delayedPromise = (ms: number) =>\n new Promise(resolve => setTimeout(resolve, ms));\n\ninterface CancelablePromise {\n promise: Promise<{}>;\n cancel: () => void;\n}\n\n/** Make a Promise \"cancelable\".\n *\n * Rejects with {isCanceled: true} if canceled.\n *\n * The way this works is by wrapping it with internal hasCanceled_ state\n * and checking it before resolving.\n */\nconst makeCancelable = (promise: Promise<any>): CancelablePromise => {\n let hasCanceled_ = false;\n\n const wrappedPromise = new Promise((resolve, reject) => {\n promise.then(\n (val: any) => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val))\n );\n promise.catch(\n (error: any) =>\n hasCanceled_ ? reject({ isCanceled: true }) : reject(error)\n );\n });\n\n return {\n promise: wrappedPromise,\n cancel() {\n hasCanceled_ = true;\n }\n };\n};\n","import React from \"react\";\nimport {\n LazyImageFull,\n CommonLazyImageProps,\n ImageState,\n ImageProps\n} from \"./LazyImageFull\";\n\n/**\n * Valid props for LazyImage\n */\nexport interface LazyImageRenderPropArgs {\n imageProps: ImageProps;\n}\n\nexport interface RefArg {\n /** When not loading eagerly, a ref to bind to the DOM element. This is needed for the intersection calculation to work. */\n ref?: React.RefObject<any>;\n}\n\nexport interface LazyImageProps extends CommonLazyImageProps {\n /** Component to display once image has loaded */\n actual: (args: LazyImageRenderPropArgs) => React.ReactElement<{}>;\n\n /** Component to display while image has not been requested\n * @default: undefined\n */\n placeholder: (\n args: LazyImageRenderPropArgs & RefArg\n ) => React.ReactElement<{}>;\n\n /** Component to display while the image is loading\n * @default placeholder, if defined\n */\n loading?: () => React.ReactElement<{}>;\n\n /** Component to display if the image fails to load\n * @default actual (broken image)\n */\n error?: () => React.ReactElement<{}>;\n}\n\n/**\n * Component that preloads the image once it is in the viewport,\n * and then swaps it in. Has predefined rendering logic, but the\n * specifics are up to the caller.\n */\nexport const LazyImage: React.StatelessComponent<LazyImageProps> = ({\n actual,\n placeholder,\n loading,\n error,\n ...rest\n}) => (\n <LazyImageFull {...rest}>\n {({ imageState, imageProps, ref }) => {\n // Call the appropriate render callback based on the state\n // and the props specified, passing on relevant props.\n switch (imageState) {\n case ImageState.NotAsked:\n return !!placeholder && placeholder({ imageProps, ref });\n\n case ImageState.Loading:\n // Only render loading if specified, otherwise placeholder\n return !!loading\n ? loading()\n : !!placeholder && placeholder({ imageProps, ref });\n\n case ImageState.LoadSuccess:\n return actual({ imageProps });\n\n case ImageState.LoadError:\n // Only render error if specified, otherwise actual (broken image)\n return !!error ? error() : actual({ imageProps });\n }\n }}\n </LazyImageFull>\n);\n\nLazyImage.displayName = \"LazyImage\";\n"],"names":["ImageState","LazyImageFullState","unionize","NotAsked","Buffering","Loading","LoadSuccess","LoadError","ofType","Action","ViewChanged","BufferingEnded","getLoadingCmd","imageProps","experimentalDecode","instance","loadingPromise","makeCancelable","loadImage","promise","then","_res","update","catch","e","isCanceled","msg","promiseCache","loading","cancelBufferingCmd","buffering","cancel","props","_super","_this","state","initialState","bind","tslib_1.__extends","LazyImageFull","action","prevState","match","_a","src","debounceDurationMs","nextState","cmd","durationMs","bufferingPromise","delayedPromise","_reason","default","this","debugActions","process","env","NODE_ENV","console","warn","log","setState","children","loadEagerly","observerProps","imageState","tag","React","Observer","rootMargin","threshold","onChange","inView","ref","Component","srcSet","alt","sizes","Promise","resolve","reject","image","Image","srcset","decode","err","onload","onerror","ms","setTimeout","hasCanceled_","val","error","LazyImage","actual","placeholder","rest","displayName"],"mappings":"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"}