Skip to content

Instantly share code, notes, and snippets.

@ajsmth
Created August 10, 2019 21:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ajsmth/28a3bfe46e0391c72c8faef049ef8670 to your computer and use it in GitHub Desktop.
Save ajsmth/28a3bfe46e0391c72c8faef049ef8670 to your computer and use it in GitHub Desktop.
pager-performance
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