Last active
February 25, 2025 06:10
-
-
Save ianmcnally/4b68c56900a20840b6ca840e2403771c to your computer and use it in GitHub Desktop.
Lazy image loading
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//@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} /> | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//@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