Skip to content

Commit

Permalink
feat(Responsive): re render only on visibility or props change (#3274)
Browse files Browse the repository at this point in the history
* feat(Responsive): re render only on visibility or props change

* add getWidth check in componentDidUpdate

* some changes
  • Loading branch information
danielr18 authored and layershifter committed Nov 25, 2018
1 parent fca4035 commit 94711d3
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 66 deletions.
46 changes: 15 additions & 31 deletions src/addons/Responsive/Responsive.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import _ from 'lodash'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import shallowEqual from 'shallowequal'

import {
customPropTypes,
Expand All @@ -10,6 +9,7 @@ import {
getUnhandledProps,
isBrowser,
} from '../../lib'
import isVisible from './lib/isVisible'

/**
* Responsive can control visibility of content.
Expand Down Expand Up @@ -56,10 +56,15 @@ export default class Responsive extends Component {
static onlyLargeScreen = { minWidth: 1200, maxWidth: 1919 }
static onlyWidescreen = { minWidth: 1920 }

constructor(...args) {
super(...args)
state = {
visible: true,
}

static getDerivedStateFromProps(props) {
const width = _.invoke(props, 'getWidth')
const visible = isVisible(width, props)

this.state = { width: _.invoke(this.props, 'getWidth') }
return { visible }
}

componentDidMount() {
Expand All @@ -74,31 +79,6 @@ export default class Responsive extends Component {
cancelAnimationFrame(this.frameId)
}

shouldComponentUpdate(nextProps, nextState) {
// Update when any prop changes or the width changes. If width does not change, no update is required.
return this.state.width !== nextState.width || !shallowEqual(this.props, nextProps)
}

// ----------------------------------------
// Helpers
// ----------------------------------------

fitsMaxWidth = () => {
const { maxWidth } = this.props
const { width } = this.state

return _.isNil(maxWidth) ? true : width <= maxWidth
}

fitsMinWidth = () => {
const { minWidth } = this.props
const { width } = this.state

return _.isNil(minWidth) ? true : width >= minWidth
}

isVisible = () => this.fitsMinWidth() && this.fitsMaxWidth()

// ----------------------------------------
// Event handlers
// ----------------------------------------
Expand All @@ -112,9 +92,12 @@ export default class Responsive extends Component {

handleUpdate = (e) => {
this.ticking = false

const { visible } = this.state
const width = _.invoke(this.props, 'getWidth')
const nextVisible = isVisible(width, this.props)

this.setState({ width })
if (visible !== nextVisible) this.setState({ visible: nextVisible })
_.invoke(this.props, 'onUpdate', e, { ...this.props, width })
}

Expand All @@ -124,11 +107,12 @@ export default class Responsive extends Component {

render() {
const { children } = this.props
const { visible } = this.state

const ElementType = getElementType(Responsive, this.props)
const rest = getUnhandledProps(Responsive, this.props)

if (this.isVisible()) return <ElementType {...rest}>{children}</ElementType>
if (visible) return <ElementType {...rest}>{children}</ElementType>
return null
}
}
10 changes: 10 additions & 0 deletions src/addons/Responsive/lib/isVisible.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import _ from 'lodash'

const fitsMaxWidth = (width, maxWidth) => (_.isNil(maxWidth) ? true : width <= maxWidth)

const fitsMinWidth = (width, minWidth) => (_.isNil(minWidth) ? true : width >= minWidth)

const isVisible = (width, { maxWidth, minWidth }) =>
fitsMinWidth(width, minWidth) && fitsMaxWidth(width, maxWidth)

export default isVisible
2 changes: 1 addition & 1 deletion test/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ beforeEach(() => {
warn = console.warn
error = console.error

console.log = throwOnConsole('log')
// console.log = throwOnConsole('log')
console.info = throwOnConsole('info')
console.warn = throwOnConsole('warn')
console.error = throwOnConsole('error')
Expand Down
100 changes: 66 additions & 34 deletions test/specs/addons/Responsive/Responsive-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('Responsive', () => {

describe('children', () => {
it('renders by default', () => {
shallow(<Responsive />).should.be.present()
mount(<Responsive />).should.be.present()
})
})

Expand All @@ -40,26 +40,24 @@ describe('Responsive', () => {
describe('getWidth', () => {
it('defaults to window.innerWidth when is browser', () => {
sandbox.stub(window, 'innerWidth').value(500)
shallow(<Responsive />)
.state('width')
.should.equal(500)
const { getWidth } = mount(<Responsive />).instance().props
getWidth().should.equal(500)
})

it('defaults to "0" when non-browser', () => {
isBrowser.override = false

shallow(<Responsive />)
.state('width')
.should.equal(0)
const { getWidth } = mount(<Responsive />).instance().props
getWidth().should.equal(0)

isBrowser.override = null
})

it('allows a custom function that returns a number', () => {
const getWidth = () => 500
const wrapper = shallow(<Responsive getWidth={getWidth} />)

wrapper.state('width').should.equal(500)
const getWidth = sandbox.spy(() => 500)
mount(<Responsive getWidth={getWidth} />)
getWidth.should.have.been.calledOnce()
getWidth.should.have.returned(500)
})

it('is called on resize', () => {
Expand All @@ -77,24 +75,62 @@ describe('Responsive', () => {
describe('maxWidth', () => {
it('renders when fits', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyMobile.maxWidth)
shallow(<Responsive {...Responsive.onlyMobile}>Show me!</Responsive>).should.not.be.blank()
mount(<Responsive {...Responsive.onlyMobile}>Show me!</Responsive>).should.not.be.blank()
})

it('renders when next maxWidth fits', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.maxWidth)
const wrapper = mount(<Responsive {...Responsive.onlyMobile} />)
wrapper.should.be.blank()
wrapper.setProps({ ...Responsive.onlyTablet })
wrapper.update()
wrapper.should.not.be.blank()
})

it('renders when next getWidth makes maxWidth fit', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.maxWidth)
const wrapper = mount(<Responsive {...Responsive.onlyMobile} />)
wrapper.should.be.blank()
const getWidth = () => Responsive.onlyMobile.maxWidth
wrapper.setProps({ getWidth })
wrapper.update()
wrapper.should.not.be.blank()
})

it('do not render when not fits', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.maxWidth)
shallow(<Responsive {...Responsive.onlyMobile}>Hide me!</Responsive>).should.be.blank()
mount(<Responsive {...Responsive.onlyMobile}>Hide me!</Responsive>).should.be.blank()
})
})

describe('minWidth', () => {
it('renders when fits', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyMobile.minWidth)
shallow(<Responsive {...Responsive.onlyMobile}>Show me!</Responsive>).should.not.be.blank()
mount(<Responsive {...Responsive.onlyMobile}>Show me!</Responsive>).should.not.be.blank()
})

it('renders when next minWidth fits', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyMobile.minWidth)
const wrapper = mount(<Responsive {...Responsive.onlyTablet} />)
wrapper.should.be.blank()
wrapper.setProps({ ...Responsive.onlyMobile })
wrapper.update()
wrapper.should.not.be.blank()
})

it('renders when next getWidth makes minWidth fit', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyMobile.maxWidth)
const wrapper = mount(<Responsive {...Responsive.onlyTablet} />)
wrapper.should.be.blank()
const getWidth = () => Responsive.onlyTablet.minWidth
wrapper.setProps({ getWidth })
wrapper.update()
wrapper.should.not.be.blank()
})

it('do not render when not fits', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.minWidth)
shallow(<Responsive {...Responsive.onlyMobile}>Hide me!</Responsive>).should.be.blank()
mount(<Responsive {...Responsive.onlyMobile}>Hide me!</Responsive>).should.be.blank()
})
})

Expand Down Expand Up @@ -126,35 +162,31 @@ describe('Responsive', () => {
})
})

describe('shouldComponentUpdate', () => {
it('returns true when width changes', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyMobile.minWidth)
const wrapper = mount(<Responsive />)
describe('render', () => {
it('does not re render if fit does not change', () => {
const wrapper = mount(<Responsive {...Responsive.onlyTablet} />)
const instance = wrapper.instance()
const spy = sandbox.spy(instance, 'shouldComponentUpdate')

sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.minWidth)
const spy = sandbox.spy(instance, 'render')
sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.minWidth + 1)
domEvent.fire(window, 'resize')
spy.should.have.returned(true)
spy.should.not.have.been.called()
})

it('returns false when width stays the same', () => {
sandbox.stub(window, 'innerWidth').value(Responsive.onlyMobile.minWidth)
const wrapper = mount(<Responsive />)
it('re renders if fit changes', () => {
const wrapper = mount(<Responsive {...Responsive.onlyTablet} />)
const instance = wrapper.instance()
const spy = sandbox.spy(instance, 'shouldComponentUpdate')

const spy = sandbox.spy(instance, 'render')
sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.minWidth - 1)
domEvent.fire(window, 'resize')
spy.should.have.returned(false)
spy.should.have.been.calledOnce()
})

it('returns true when props change', () => {
it('re renders when props change', () => {
const wrapper = mount(<Responsive {...Responsive.onlyMobile} />)
const instance = wrapper.instance()
const spy = sandbox.spy(instance, 'shouldComponentUpdate')

wrapper.setProps({ ...Responsive.onlyTablet })
spy.should.have.returned(true)
const spy = sandbox.spy(instance, 'render')
wrapper.setProps({ as: 'h1' })
spy.should.have.been.called()
})
})
})

0 comments on commit 94711d3

Please sign in to comment.