Building a pager component (Part 4)

In the last post, we finished up handling gestures and managing our pager's transitions. At this point our component is fully controlled, meaning it requires a container component to pass it an activeIndex and onChange prop in order to work. But that might not be what we always want, so we'll be adjusting the interface and implementation a little bit to open up our component and make it more configurable.

Uncontrolled Pager

Let's make the component work by itself, without the help of a parent component. To do this, we'll extend our interface:

interface Props {
activeIndex?: number
onChange?: (index: number) => void
children: any
initialIndex?: number
}

Let's implement this change:

// rename the activeIndex and onChange props
// add a default value for initialIndex
function Pager({ children, activeIndex: parentIndex, onChange: parentOnChange, initialIndex = 0 }) {

// 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

//...
})

That's it! Now we can use our pager component without the activeIndex and onChange props. Here's what we can do now:

<Pager initialIndex={2}>
<Screen 1 />
<Screen 2 />
<Screen 3 />
</Pager>

Improving Performance

Suppose we had 100,000 children to render - animating this might get a little hairy when it comes to performance. If we try this in our current implementation, the transitions get really laggy because there's so much JS happening. Since we know the activeIndex, we can choose to only render children that are active or adjacent to the active view, and we'll expose a childOffset prop to allow parent components to specify how many adjacent children to render:

interface Props {
activeIndex?: number
onChange?: (index: number) => void
children: any
initialIndex?: number,
childOffset?: number, // the number of adjacent children to keep mounted
}
// 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=
>
{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=
>
{element}
</div>
);
})}
</animated.div>
);

We've now added some configurability to the pager and improved the performance when handling larger lists. You can take a look at the final result here:

Performance improvement for pager

CodeSandbox

In the next post, we'll take a stab at implementing the Apple Podcasts page referenced in our first article. We will extend our component quite a bit to make it event more flexible than before.

Cheers!