Created
August 10, 2019 21:23
-
-
Save ajsmth/28a3bfe46e0391c72c8faef049ef8670 to your computer and use it in GitHub Desktop.
pager-performance
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
import React, { useState, useEffect } from 'react' | |
import { useSpring, animated, interpolate } from 'react-spring' | |
import { useDrag } from 'react-use-gesture' | |
const PAGE_SIZE = 200 | |
// arbitrary value that will determine if we should transition after dragging | |
const DRAG_THRESHOLD = Math.floor(PAGE_SIZE * 0.3) | |
function Pager({ | |
children, | |
activeIndex: parentIndex, // rename to parentIndex for simple refactor | |
onChange: parentOnChange, // rename to parentOnChange for simple refactor | |
initialIndex = 0, // defualt to index 0 | |
childOffset, | |
}) { | |
// determine if the component is controlled | |
// we'll assume that if activeIndex prop is defined then it's being controlled: | |
const isControlled = parentIndex !== undefined | |
// create our own internal activeIndex to manage when uncontrolled | |
const [_activeIndex, setActiveIndex] = useState(initialIndex) | |
// determine which activeIndex number and onChange function we should use in our implementation | |
const activeIndex = isControlled ? parentIndex : _activeIndex | |
const onChange = isControlled ? parentOnChange : setActiveIndex | |
const offset = activeIndex * 100 * -1 | |
// dragX will represent the current drag value to animate | |
const [{ translateX, dragX }, set] = useSpring(() => ({ | |
translateX: offset, | |
dragX: 0, | |
})) | |
// this might look a bit strange but it's part of the api for useDrag | |
// bind() is a function we'll add to our container div that gives us a bunch of gesture state data | |
// think of this as an event listener for gestures | |
const bind = useDrag(({ delta, last, vxvy }) => { | |
// this is the drag value | |
const [x] = delta | |
// the velocity of the drag -- important to track to prevent jank after user releases | |
const [vx] = vxvy | |
// we want the value to immediate update w/ a user drag event, not spring to the value | |
set({ dragX: x }) | |
// last is true when the user releases from dragging | |
if (last) { | |
// user has dragged beyond our threshold to transition (either left or right) | |
const shouldTransition = Math.abs(x) >= DRAG_THRESHOLD | |
if (!shouldTransition) { | |
// restore to initial position when user started dragging: | |
set({ dragX: 0, immediate: false }) | |
} else { | |
// determine the next position based on the drag value (left or right) | |
let nextPosition | |
if (x > DRAG_THRESHOLD) { | |
// transition to previous page | |
nextPosition = offset + 100 | |
// update our controller component w/ the previous index | |
onChange(activeIndex - 1) | |
} | |
if (x < DRAG_THRESHOLD) { | |
// transition to next page | |
nextPosition = offset - 100 | |
// update our controller component w/ the next index | |
onChange(activeIndex + 1) | |
} | |
// start spring transition to next position | |
// we want to spring the drag value back to 0 as we translate to the next position | |
set({ | |
dragX: 0, | |
translateX: nextPosition, | |
immediate: false, | |
config: { | |
velocity: vx, | |
}, | |
}) | |
} | |
} | |
}) | |
useEffect(() => { | |
set({ translateX: offset, dragX: 0 }) | |
}, [offset, set]) | |
// slice our children array and return children adjacent to activeIndex based on childOffset prop | |
const adjacentChildren = | |
childOffset !== undefined | |
? children.slice( | |
Math.max(activeIndex - childOffset, 0), | |
Math.min(activeIndex + childOffset + 1, children.length), | |
) | |
: children | |
return ( | |
<animated.div | |
{...bind()} | |
style={{ | |
position: 'relative', | |
height: '100%', | |
width: '100%', | |
transform: interpolate( | |
[translateX, dragX], | |
(translateX, dragX) => | |
`translateX(calc(${translateX}% + ${dragX}px))`, | |
), | |
}}> | |
{React.Children.map(adjacentChildren, (element, index) => { | |
// compute offset of child based on childOffset and index | |
let offset = index | |
if (childOffset !== undefined) { | |
offset = | |
activeIndex <= childOffset | |
? index | |
: activeIndex - childOffset + index | |
} | |
return ( | |
<div | |
style={{ | |
...absoluteFill, | |
transform: `translateX(${offset * 100}%)`, | |
}}> | |
{element} | |
</div> | |
) | |
})} | |
</animated.div> | |
) | |
} | |
const absoluteFill = { | |
position: 'absolute', | |
left: 0, | |
right: 0, | |
bottom: 0, | |
top: 0, | |
} | |
const children = Array.from({ length: 100000 }).map((c, i) => ( | |
<h1 key={i} style={{ textAlign: 'center' }}> | |
Index {i} | |
</h1> | |
)) | |
// this will represent a consumer component or any part of your application | |
function App() { | |
// all we need to pass are children and an activeIndex prop to our pager component | |
const [activeIndex, setActiveIndex] = useState(4000) | |
function handleChange(index) { | |
setActiveIndex(index) | |
} | |
return ( | |
<div style={{}}> | |
<div | |
style={{ | |
width: PAGE_SIZE, | |
height: PAGE_SIZE, | |
display: 'flex', | |
margin: 'auto', | |
border: 'thin solid red', | |
}}> | |
<Pager | |
childOffset={2} | |
activeIndex={activeIndex} | |
onChange={handleChange}> | |
{children} | |
</Pager> | |
</div> | |
<div | |
style={{ | |
display: 'flex', | |
flexDirection: 'column', | |
alignItems: 'center', | |
justifyContent: 'space-around', | |
}}> | |
<strong style={{ margin: '5px 0' }}>activeIndex: {activeIndex}</strong> | |
<button | |
style={{ margin: '5px 0' }} | |
onClick={() => setActiveIndex(activeIndex + 1)}> | |
Increment | |
</button> | |
<button | |
style={{ margin: '5px 0' }} | |
onClick={() => setActiveIndex(activeIndex - 1)}> | |
Decrement | |
</button> | |
</div> | |
</div> | |
) | |
} | |
export default App |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment