-
-
Save lon-io/fab900a0fb9ed37bc9b1408735877600 to your computer and use it in GitHub Desktop.
Lazy image loading
This file contains hidden or 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 hidden or 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