Skip to content

Instantly share code, notes, and snippets.

@ianmcnally
Last active May 15, 2021 22:14
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ianmcnally/4b68c56900a20840b6ca840e2403771c to your computer and use it in GitHub Desktop.
Save ianmcnally/4b68c56900a20840b6ca840e2403771c to your computer and use it in GitHub Desktop.
Lazy image loading
//@flow
import React, { Component } from 'react'
import type { ElementRef } from 'react'
type Props = { src: string, alt: string }
type State = { source?: string }
export default class LazyImage extends Component<Props, State> {
state = {}
observer: IntersectionObserver
node: HTMLImageElement
setRef: ElementRef<'img'> = (node: HTMLImageElement): void => {
this.node = node
}
componentDidMount() {
const { updateSourceIfElementIsInView } = this
if (typeof IntersectionObserver === 'undefined') {
this.setImageSourceFromProps()
return
}
//$FlowFixMe: signature is wrong in flow
this.observer = new IntersectionObserver(updateSourceIfElementIsInView)
this.observer.observe(this.node)
}
updateSourceIfElementIsInView = (
entries: Array<IntersectionObserverEntry>,
) => {
const [elementEntry] = entries
if (elementEntry.isIntersecting) {
this.setImageSourceFromProps()
this.observer.disconnect()
}
}
setImageSourceFromProps() {
const { src: source } = this.props
this.setState({ source })
}
render() {
const { setRef } = this
// eslint-disable-next-line no-unused-vars
const { src, alt, ...restProps } = this.props
const { source } = this.state
return <img {...restProps} alt={alt} src={source} ref={setRef} />
}
}
//@flow
import * as React from 'react'
import { shallow, ShallowWrapper } from 'enzyme'
import LazyImage from './lazy-image'
describe('when rendered', () => {
const props = { src: 'fillmurray.com/200/200', alt: 'Bill Murray' }
let wrapper: ShallowWrapper
beforeAll(() => {
wrapper = shallow(<LazyImage {...props} />, {
disableLifecycleMethods: true,
})
})
it('renders an img without a src attribute', () => {
expect(wrapper.getElement()).toMatchSnapshot()
})
})
describe('when it mounts', () => {
const props = { src: 'fillmurray.com/200/200', alt: 'Bill Murray' }
const mockRef = { mock: true }
const windowIntersectionObserver = window.IntersectionObserver
const observe = jest.fn()
let wrapper: ShallowWrapper
beforeAll(() => {
window.IntersectionObserver = jest.fn(function() {
this.observe = observe
})
wrapper = shallow(<LazyImage {...props} />, {
disableLifecycleMethods: true,
})
wrapper.getElement().ref(mockRef)
wrapper.instance().componentDidMount()
})
afterAll(() => {
window.IntersectionObserver = windowIntersectionObserver
})
it('creates an observer on the element ref', () => {
expect(observe).toBeCalledWith(mockRef)
})
})
describe('when the element is in view', () => {
const props = { src: 'fillmurray.com/200/200', alt: 'Bill Murray' }
const mockRef = { mock: true }
const windowIntersectionObserver = window.IntersectionObserver
const disconnect = jest.fn()
let wrapper: ShallowWrapper
beforeAll(() => {
const mockEntry = { isIntersecting: true }
window.IntersectionObserver = jest.fn(function() {
this.observe = () => {}
this.disconnect = disconnect
})
wrapper = shallow(<LazyImage {...props} />, {
disableLifecycleMethods: true,
})
wrapper.getElement().ref(mockRef)
wrapper.instance().componentDidMount()
const observerCallback = window.IntersectionObserver.mock.calls[0][0]
observerCallback([mockEntry])
wrapper.update()
})
afterAll(() => {
window.IntersectionObserver = windowIntersectionObserver
})
it('disconnects the observer', () => {
expect(disconnect).toBeCalled()
})
it('updates the image source to be props.src', () => {
expect(wrapper.find('img').prop('src')).toEqual(props.src)
})
})
describe('when the element is out of view', () => {
const props = { src: 'fillmurray.com/200/200', alt: 'Bill Murray' }
const mockRef = { mock: true }
const windowIntersectionObserver = window.IntersectionObserver
const disconnect = jest.fn()
let wrapper: ShallowWrapper
beforeAll(() => {
const mockEntry = { isIntersecting: false }
window.IntersectionObserver = jest.fn(function() {
this.observe = () => {}
this.disconnect = disconnect
})
wrapper = shallow(<LazyImage {...props} />, {
disableLifecycleMethods: true,
})
wrapper.getElement().ref(mockRef)
wrapper.instance().componentDidMount()
const observerCallback = window.IntersectionObserver.mock.calls[0][0]
observerCallback([mockEntry])
wrapper.update()
})
afterAll(() => {
window.IntersectionObserver = windowIntersectionObserver
})
it('does not disconnect the observer', () => {
expect(disconnect).not.toBeCalled()
})
it('does not update the image source', () => {
expect(wrapper.find('img').prop('src')).toBe(undefined)
})
})
describe('when IntersectionObserver does not exist', () => {
const props = { src: 'fillmurray.com/200/200', alt: 'Bill Murray' }
const windowIntersectionObserver = window.IntersectionObserver
const renderComponent = () => shallow(<LazyImage {...props} />)
beforeAll(() => {
delete window.IntersectionObserver
})
afterAll(() => {
window.IntersectionObserver = windowIntersectionObserver
})
it('does not throw an error', () => {
expect(renderComponent).not.toThrow()
})
it('updates the image source from props.src', () => {
expect(
renderComponent()
.find('img')
.prop('src'),
).toEqual(props.src)
})
})
describe('with additional properties', () => {
const props = {
src: 'fillmurray.com/200/200',
alt: 'Bill Murray',
title: 'This is Bill Murray',
}
let wrapper: ShallowWrapper
beforeAll(() => {
wrapper = shallow(<LazyImage {...props} />, {
disableLifecycleMethods: true,
})
})
it('passes them to the img element', () => {
expect(wrapper.getElement()).toMatchSnapshot()
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment