This article is intended to be a first in a series on creating common ui components using React. The objective is to, in the process of creating these components, explore how React APIs help us to achieve this objective.
Today we build a custom Reusable React Slider.
I recently built a custom React Slider using React's context
. So what problem was I trying to solve that made me reach for React’s context? I needed a way to pass state from the slider's container component to each child slider item without knowing before hand what number or types of slide items I am rendering. As far as the slider container is concerned, it is rendering children
components; whatever these components are is of no concern to the it (it is children agnostic).
// Slider container declaration
const SliderContainer = ({ children }) => (<div>{children}</div>)
// Slider container invokation
<SliderContainer>
<SliderItem></SliderItem>
<SliderItem></SliderItem>
</SliderContainer>
While using the context pattern works, it is not the best use of context as recommended by the documentation (when to use context). Our slider component is a level deep, while context shines for cases where the component is many levels deep and nested (before you use context).
I will first go through how I used context to solve the above stated problem. Then I will show how it can be done better using api utilities created by React to aid composition.
The slider consists of two components:
In this implementation of the slider component, because we are not explicitly passing the number of slider items and rendering the items directly in the container slider, we have to find a way to determine this number while being agnostic about the slider items.
React's useRef
helps us with this.
We access the ref of the slider container to determine the number of children it has after render. We save this value as a state in the slider container.
const [totalNumberOfItems, setTotalNumberOfItems] = useState(0);
const containerRef = useRef() as MutableRefObject<HTMLDivElement>;
useEffect(() => {
if (containerRef.current) {
setTotalNumberOfItems(containerRef.current.childNodes.length);
}
});
After determining the total number of slider items, we need a way to navigate to the next item in the list and save in the state.
const [currentItemIndex, setCurrentItemIndex] = useState(0);
const [itemNumber, setItemNumber] = useState(3);
const containerRef = useRef() as MutableRefObject<HTMLDivElement>;
const nextItem = () => {
if (currentItemIndex === itemNumber - 1) {
setCurrentItemIndex(0);
} else {
setCurrentItemIndex(currentItemIndex + 1);
}
};
useEffect(() => {
const duration = 8000;
const id = setTimeout(() => nextItem(), duration);
return () => clearTimeout(id);
});
The nextItem
function serves this purpose. We invoke the function after a duration, this causes the component to re-render, which in turn causes another invokation. This pattern simulates setInterval
. We created an infinite slider without using the setInterval
JavaScript API.
Now that we have the current slide item to be displayed, we need to pass this information to each item. Each slide will use this information to determine if it will be displayed or not. The current item is the active item and will be displayed, while the rest will not be displayed.
So how do we use React's Context
to pass this information to each child component?
First, we create the context for the slider container.
const SliderContext: Context<{
currentItemIndex?: number;
}> = createContext({});
Then we subscribe to the context changes in each slider item. Any change to the current item will reflect in all slider items.
const { currentItemIndex } = useContext(SliderContext);
The complete code for slider container with context.
interface SliderContainerProps {
children: ReactNode;
}
export const SliderContainer: FC<SliderContainerProps> = ({ children }) => {
const [currentItemIndex, setCurrentItemIndex] = useState(0);
const [totalNumberOfItems, setTotalNumberOfItems] = useState(0);
const containerRef = useRef() as MutableRefObject<HTMLDivElement>;
const nextItem = () => {
if (currentItemIndex === itemNumber - 1) {
setCurrentItemIndex(0);
} else {
setCurrentItemIndex(currentItemIndex + 1);
}
};
useEffect(() => {
const duration = 5000;
const id = setTimeout(() => nextItem(), duration);
return () => clearTimeout(id);
});
useEffect(() => {
if (containerRef.current) {
setTotalNumberOfItems(containerRef.current.childNodes.length);
}
});
return (
<div>
<SliderContext.Provider
value={{
setItemNumber,
totalNumberOfItems,
currentItemIndex,
}}
>
{props.children}
</SliderContext.Provider>
</div>
);
};
The complete code for slider items with context
const getIndex = (el) => {
return Array.from(el.parentNode.children).indexOf(el);
};
interface SliderItemProps {
children: ReactElement;
}
export const SliderItem: FC<SliderItemProps> = ({
children
}) => {
const [isActive, setIsActive] = useState(false);
const itemRef = useRef<HTMLDivElement>(null);
const { currentItemIndex } =
useContext(SliderContext);
useEffect(() => {
if (itemRef.current) {
setIsActive(currentItemIndex === getIndex(itemRef.current));
}
}, [isActive, currentItemIndex]);
return (
<div style={{display: isActive ? “block”: “none”}} ref={itemRef}>
{children}
</div>
);
};
So, how do we use composition to achieve what we have with context? Before we proceed to the answer to this question, observe that each slider item is tightly coupled to the slider container through the context. The slider items can’t function independently without breaking. The property they need to function as designed come primarily from the slider container directly and not through an interface/contract/props.
We can improve on what we have and make the slider more loosely coupled and independent by leveraging some of React’s utility apis.
Our primary objective is to decouple the slider item from the slider container. We want our slider item component to be able to function independently of the slider container.
The first thing to do is to unsubscribe the slider item from the slider container context. Any information we need will be provided by the slider item props.
export const SliderItem: FC<SliderItemProps> = ({
children,
currentItemIndex
}) => {
const [isActive, setIsActive] = useState(false);
const itemRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (itemRef.current) {
setIsActive(currentItemIndex === getIndex(itemRef.current));
}
}, [isActive, currentItemIndex]);
return (
<div style={{display: isActive ? “block”: “none”}} ref={itemRef}>
{children}
</div>
);
};
The next issue is how to pass props to a slider item without invoking the component directly. Remember, our slider container is slider item agnostic. It does not care for the implementation details of the slider item, it only needs a set of apis it can pass to the slider item component.
The React utility api that will help us achieve this is the cloneElement
.
cloneElement(child, {
currentItemIndex,
});
But notice that cloneElement
only works with one React element at a time, but we have an array of slider items. Thankfully, React provides us with the Children
utility api. This api has the map method that is similar to the JavaScript array map. And just as the JavaScript array map, we can transform each React element in the list of children.
{
Children.map(children, (child) => {
return cloneElement(child as ReactElement, {
setItemNumber,
itemNumber,
currentItemIndex,
});
});
}
In just a few lines of code, we have composed our slider and the constituent parts are loosely coupled, and thus more reusable.