Skip to content

Instantly share code, notes, and snippets.

@janicduplessis
Created August 19, 2019 01:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janicduplessis/172cf7a8b36426627e99b2243a08ee08 to your computer and use it in GitHub Desktop.
Save janicduplessis/172cf7a8b36426627e99b2243a08ee08 to your computer and use it in GitHub Desktop.
/* global IntersectionObserver */
import * as React from 'react';
// @ts-ignore
import { findNodeHandle } from 'react-native-web';
import { BaseImage, BaseImageProps } from './BaseImage';
import { Box } from '../Box';
// Cache if we've seen an image before so we don't both with
// lazy-loading & fading in on subsequent mounts.
const imageCache = new Set();
type IOListener = [HTMLElement, () => void];
let io: IntersectionObserver | null = null;
const listeners: IOListener[] = [];
function getIO() {
if (
io === null &&
typeof window !== `undefined` &&
typeof IntersectionObserver !== 'undefined'
) {
io = new IntersectionObserver(
entries => {
entries.forEach(entry => {
listeners.forEach(l => {
if (io !== null && l[0] === entry.target) {
// Edge doesn't currently support isIntersecting, so also test for an intersectionRatio > 0
if (entry.isIntersecting || entry.intersectionRatio > 0) {
io.unobserve(l[0]);
l[1]();
}
}
});
});
},
{ rootMargin: `200px` },
);
}
return io;
}
const listenToIntersections = (el: HTMLElement, cb: () => void) => {
const observer = getIO();
if (observer !== null) {
observer.observe(el);
listeners.push([el, cb]);
}
};
type State = {
isVisible: boolean;
imgLoaded: boolean;
IOSupported: boolean;
fadeIn?: boolean;
seenBefore: boolean;
};
const resolveSource = (source: BaseImageProps['source']) => {
if (source == null) {
return null;
}
if (typeof source === 'object' && source.uri != null) {
return source.uri;
} else if (typeof source === 'string') {
return source;
} else {
return null;
}
};
export class Image extends React.Component<BaseImageProps, State> {
static defaultProps = {
critical: false,
fadeIn: true,
objectPosition: 'center',
};
state: State;
constructor(props: BaseImageProps) {
super(props);
// If this browser doesn't support the IntersectionObserver API
// we default to start downloading the image right away.
let isVisible = true;
let imgLoaded = true;
let IOSupported = false;
const fadeIn = props.fadeIn;
// If this image has already been loaded before then we can assume it's
// already in the browser cache so it's cheap to just show directly.
const imageUrl = resolveSource(props.source);
const seenBefore = imageUrl != null && imageCache.has(imageUrl);
if (
!seenBefore &&
typeof window !== 'undefined' &&
typeof IntersectionObserver !== 'undefined'
) {
isVisible = false;
imgLoaded = false;
IOSupported = true;
}
// Always don't render image while server rendering
if (typeof window === 'undefined') {
isVisible = false;
imgLoaded = false;
}
this.state = {
isVisible,
imgLoaded,
IOSupported,
fadeIn,
seenBefore,
};
}
_imageRef = React.createRef<HTMLImageElement>();
componentDidMount() {
if (this.props.critical) {
const img = this._imageRef.current;
if (img && img.complete) {
this._handleImageLoaded();
}
}
}
_handleRef = (ref: any) => {
if (this.state.IOSupported && ref != null) {
listenToIntersections(findNodeHandle(ref), () => {
this.setState({ isVisible: true });
});
}
};
_handleImageLoaded = () => {
this.setState({ imgLoaded: true });
if (this.state.seenBefore) {
this.setState({ fadeIn: false });
} else {
const imageUrl = resolveSource(this.props.source);
if (imageUrl != null) {
imageCache.add(imageUrl);
}
}
this.props.onLoad && this.props.onLoad();
};
render() {
const {
source,
onLoad,
resizeMode,
tintColor,
resizeMethod,
...others
} = this.props;
return (
<Box
ref={this._handleRef}
opacity={
// This avoid a weird bug with server rendering in WKWebview were
// opacity stays set a 0.
typeof window === 'undefined'
? undefined
: this.state.imgLoaded || this.state.fadeIn === false
? 1
: 0
}
css={{
// @ts-ignore
transition: this.state.fadeIn === true ? 'opacity 0.5s' : 'none',
}}
overflow="hidden"
width={
source != null && typeof source === 'object'
? source.width
: undefined
}
height={
source != null && typeof source === 'object'
? source.height
: undefined
}
{...others}
>
{this.state.isVisible && (
<BaseImage
source={source}
onLoad={this._handleImageLoaded}
resizeMode={resizeMode}
resizeMethod={resizeMethod}
tintColor={tintColor}
flexGrow={1}
/>
)}
</Box>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment