Skip to content

Commit 18436bd

Browse files
FaberVitalehartzis
authored andcommitted
Memoize hook output (#149)
* chore(lint): add eslint-plugin-react-hooks * refactor(performance): memoize useSwipeable output
1 parent c56d950 commit 18436bd

File tree

5 files changed

+95
-33
lines changed

5 files changed

+95
-33
lines changed

.eslintrc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@
3333
"react/jsx-no-undef": 2,
3434
"react/jsx-wrap-multilines": 2,
3535
"react/no-string-refs": 0,
36-
"react/prop-types": [1, {"ignore": ["className", "children", "style"]}]
36+
"react/prop-types": [1, { "ignore": ["className", "children", "style"] }],
37+
"react-hooks/rules-of-hooks": 2,
38+
"react-hooks/exhaustive-deps": 1
3739
},
38-
"plugins": [
39-
"import",
40-
"react"
41-
]
40+
"plugins": ["import", "react", "react-hooks"]
4241
}

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"eslint-plugin-import": "^2.14.0",
8888
"eslint-plugin-prettier": "^3.0.0",
8989
"eslint-plugin-react": "^7.11.1",
90+
"eslint-plugin-react-hooks": "^1.6.0",
9091
"gh-pages": "^1.0.0",
9192
"jest": "^23.6.0",
9293
"prettier": "1.15.3",

src/__tests__/index.spec.js

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* global document, jest, expect, beforeAll, afterAll */
22
import React from 'react'
33
import Enzyme from 'enzyme'
4+
import PropTypes from 'prop-types'
45
import Adapter from 'enzyme-adapter-react-16'
56
import { Swipeable, useSwipeable, LEFT, RIGHT, UP, DOWN } from '../index'
67
import { createTouchEventObject as cte, createMouseEventObject as cme } from './helpers/events'
@@ -55,14 +56,22 @@ function mockListenersSetup(el) {
5556
*/
5657
function SwipeableUsingHook(props) {
5758
const eventHandlers = useSwipeable(props)
59+
const Elem = props.nodeName
5860
// Use innerRef prop to access the mounted div for testing.
5961
const ref = el => (props.innerRef && props.innerRef(el), eventHandlers.ref(el)) // eslint-disable-line
6062
return (
61-
<div {...eventHandlers} ref={ref}>
63+
<Elem {...eventHandlers} ref={ref}>
6264
{props.children}
63-
</div>
65+
</Elem>
6466
)
6567
}
68+
SwipeableUsingHook.propTypes = {
69+
nodeName: PropTypes.string
70+
}
71+
72+
SwipeableUsingHook.defaultProps = {
73+
nodeName: 'div'
74+
}
6675

6776
function setupGetMountedComponent(TYPE, mockListeners = mockListenersSetup) {
6877
return props => {
@@ -387,7 +396,7 @@ function setupGetMountedComponent(TYPE, mockListeners = mockListenersSetup) {
387396
})
388397
})
389398

390-
it('Cleans up and re-attaches touch event listeners', () => {
399+
it('Cleans up and re-attaches touch event listeners if trackTouch changes', () => {
391400
let spies
392401
const mockListeners = el => {
393402
// already spying
@@ -413,6 +422,32 @@ function setupGetMountedComponent(TYPE, mockListeners = mockListenersSetup) {
413422
expect(spies.addEventListener).toHaveBeenCalledTimes(6)
414423
expect(spies.removeEventListener).toHaveBeenCalledTimes(3)
415424
})
425+
426+
it('Cleans up and re-attaches touch event listeners if the DOM element changes', () => {
427+
let spies
428+
const mockListeners = el => {
429+
// already spying
430+
if (spies) return
431+
spies = {}
432+
spies.addEventListener = jest.spyOn(el, 'addEventListener')
433+
spies.removeEventListener = jest.spyOn(el, 'removeEventListener')
434+
}
435+
const { wrapper } = setupGetMountedComponent(TYPE, mockListeners)({})
436+
437+
expect(spies.addEventListener).toHaveBeenCalledTimes(3)
438+
expect(spies.removeEventListener).not.toHaveBeenCalled()
439+
440+
wrapper.setProps({ nodeName: 'p' })
441+
442+
expect(spies.addEventListener).toHaveBeenCalledTimes(3)
443+
expect(spies.removeEventListener).toHaveBeenCalledTimes(3)
444+
// VERIFY REMOVED HANDLERS ARE THE SAME ONES THAT WERE ADDED!
445+
expect(spies.addEventListener.mock.calls.length).toBe(3)
446+
spies.addEventListener.mock.calls.forEach((call, idx) => {
447+
expect(spies.removeEventListener.mock.calls[idx][0]).toBe(call[0])
448+
expect(spies.removeEventListener.mock.calls[idx][1]).toBe(call[1])
449+
})
450+
})
416451
})
417452

418453
it(`${TYPE} handles updated prop swipe callbacks`, () => {
@@ -423,15 +458,22 @@ function setupGetMountedComponent(TYPE, mockListeners = mockListenersSetup) {
423458
}
424459
const onSwipedLeft = jest.fn()
425460

461+
function TestHookComponent({ next }) {
462+
const handlers = useSwipeable({ onSwipedLeft: next })
463+
// Use innerRef to access the mounted div for testing.
464+
const ref = el => (innerRef(el), handlers.ref(el))
465+
return <div {...handlers} ref={ref} />
466+
}
467+
TestHookComponent.propTypes = {
468+
next: PropTypes.func.isRequired
469+
}
470+
426471
function TestComponent() {
427472
const [page, setPage] = React.useState(0)
428473
const next = () => (setPage(page + 1), onSwipedLeft(page + 1))
429474

430475
if (TYPE === USESWIPEABLE) {
431-
const handlers = useSwipeable({ onSwipedLeft: next })
432-
// Use innerRef to access the mounted div for testing.
433-
const ref = el => (innerRef(el), handlers.ref(el))
434-
return <div {...handlers} ref={ref} />
476+
return <TestHookComponent next={next} />
435477
}
436478
if (TYPE === SWIPEABLE) {
437479
// Use innerRef to access the mounted div for testing.

src/index.js

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -161,22 +161,6 @@ function getHandlers(set, handlerProps) {
161161
})
162162
}
163163

164-
// update state, and handlers
165-
set((state, props) => {
166-
let addState = {}
167-
// clean up touch handlers if no longer tracking touches
168-
if (!props.trackTouch && state.cleanUpTouch) {
169-
state.cleanUpTouch()
170-
addState.cleanUpTouch = null
171-
} else if (props.trackTouch && !state.cleanUpTouch) {
172-
// attach/re-attach touch handlers
173-
if (state.el) {
174-
addState.cleanUpTouch = attachTouch(state.el)
175-
}
176-
}
177-
return { ...state, ...addState }
178-
})
179-
180164
// set ref callback to attach touch event listeners
181165
const output = { ref: onRef }
182166

@@ -185,17 +169,46 @@ function getHandlers(set, handlerProps) {
185169
output.onMouseDown = onStart
186170
}
187171

188-
return output
172+
return [output, attachTouch]
173+
}
174+
175+
function updateTransientState(state, props, attachTouch) {
176+
let addState = {}
177+
// clean up touch handlers if no longer tracking touches
178+
if (!props.trackTouch && state.cleanUpTouch) {
179+
state.cleanUpTouch()
180+
addState.cleanUpTouch = null
181+
} else if (props.trackTouch && !state.cleanUpTouch) {
182+
// attach/re-attach touch handlers
183+
if (state.el) {
184+
addState.cleanUpTouch = attachTouch(state.el)
185+
}
186+
}
187+
return { ...state, ...addState }
189188
}
190189

191190
export function useSwipeable(props) {
191+
const { trackMouse } = props
192192
const transientState = React.useRef({ ...initialState, type: 'hook' })
193193
const transientProps = React.useRef()
194194
transientProps.current = { ...defaultProps, ...props }
195-
return getHandlers(
196-
cb => (transientState.current = cb(transientState.current, transientProps.current)),
197-
{ trackMouse: props.trackMouse }
195+
196+
const [handlers, attachTouch] = React.useMemo(
197+
() =>
198+
getHandlers(
199+
cb => (transientState.current = cb(transientState.current, transientProps.current)),
200+
{ trackMouse }
201+
),
202+
[trackMouse]
198203
)
204+
205+
transientState.current = updateTransientState(
206+
transientState.current,
207+
transientProps.current,
208+
attachTouch
209+
)
210+
211+
return handlers
199212
}
200213

201214
export class Swipeable extends React.PureComponent {
@@ -228,7 +241,8 @@ export class Swipeable extends React.PureComponent {
228241

229242
render() {
230243
const { className, style, nodeName = 'div', innerRef, children, trackMouse } = this.props
231-
const handlers = getHandlers(this._set, { trackMouse })
244+
const [handlers, attachTouch] = getHandlers(this._set, { trackMouse })
245+
this.transientState = updateTransientState(this.transientState, this.props, attachTouch)
232246
const ref = innerRef ? el => (innerRef(el), handlers.ref(el)) : handlers.ref
233247
return React.createElement(nodeName, { ...handlers, className, style, ref }, children)
234248
}

0 commit comments

Comments
 (0)