Create a react carousel using react hooks

June 12, 2019

A post by Will Mayger
Twitter . Instagram

So you want to create a react carousel?

A react carousel is a great way to have a bespoke carousel that you have full control of.

It works by making use of react (and react hooks in this instance), and you can learn a lot whilst putting it together.

Firstly though, it is important to remember that if you are looking to implement a carousel in react quickly for your project, unfortunately this may not be the best option, instead it might be worth looking at using something like the React Slick library, that you can plug and play immediately.

If you do have the time though, creating a react carousel using react hooks is an awesome way to learn how react works whilst creating a nice and smooth carousel that you will have full control over, which I will show you how to create in this tutorial!

With carousels you generally have many options.

  • Do you want an infinite carousel?
  • Do you want it to be a timed carousel?
  • Do you want it to be a calculated carousel (like react spring)?
  • Do you want the carousel to peak the next slide?

There are hundreds of possible options when it comes to creating a carousel, especially when creating a carousel in react.

For this tutorial we are going to use react hooks to help us out when creating our react carousel.

On top of this we are going to make it an infinite react carousel with calculated animations so we can track, manipulate and fine tune it to exactly what we want, and be able to know at the exact moment each transition finishes without having to worry about setTimeout or anything like that.

React hooks carousel?

Don’t let react hooks put you off if you haven’t used them before (this tutorial will walk you through everything step by step). There are only a few bits you will need to remember when it comes to hooks in this tutorial and hooks is very simple and easy to pick up.

useState, and useEffect.

These functions allow us to use react state and lifecycle events within a function rather than a class, it’s as simple as that.

useState can be used like this:

const [stateVariable, setStateVariable] = useState(initialState);

Here the function is returning a variable and a function that will allow us to update the variable which will trigger a re-render.

useEffect can be used like this:

useEffect(() => {
	console.log('stateVariable has been updated!');
}, [stateVariable]);

Here we pass useEffect a function that will be called each time stateVariable has been updated.

For more info on react hooks checkout some of my other blog posts such as: Using window.addEventListener with react hooks React hooks vs Classes

React carousel tutorial

Now we have got the basics of hooks done, let’s make a start on our tutorial for creating an infinite react carousel!

Firstly make sure you are working with the latest working versions of NodeJS, ReactJS, yarn, npx, and create-react-app.

Once you are ready, type into your terminal npx create-react-app create-react-carousel-tutorial.

This will generate a react project that is ready to start working on straight away (if you already have your project, skip this step).

Now let’s create a new component for our carousel within our project, create a new directory within the src directory called carousel and within it add a file called index.js.

This should look like this: create-react-carousel-tutorial > src > carousel > index.js.

Inside this file start by adding the following few lines to initialize our carousel component

import React from 'react';

export default function Carousel() {
  return (
    <div></div>
  );
}

Here we are adding in the bare minimum to make it a valid component.

Next, we should start adding in our JSX to lay out the structure to make it easier to visualize how the react carousel will be working when it comes to adding in the functionality with react hooks.

Before we do that though, we just need to install node-sass so we can make use of sass in our component and install prop-types so we can type check all props passed into our component.

In your terminal cd into the project directory and type yarn add node-sass and then yarn add prop-types to install both of these packages to the project.

Then, create a new file in the carousel directory called carousel.module.scss. This is using css modules which basically allows you to import css class names in javascript.

Back to our JSX structure, open up index.js and change the contents of the file to look like the following:

import React from 'react';
import PropTypes from 'prop-types';

export default function Carousel({ children }) {
  return (
  <div> {/* Parent container */}
      <div> {/* Outer carousel container (visible width) */}
        <div> {/* inner carousel container (width of entire carousel) */}
          {
            children.map(item => (
              <div> {/* carousel item container */}
                {item}
              </div>
            ))
          }
        </div>
      </div>
      <div> {/* carousel controls container */}
        <button>
          Next
        </button>
        <button>
          Prev
        </button>
      </div>
    </div>
  );
}

Carousel.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]).isRequired,
};

This is what we will base our react carousel jsx from.

All that we have here is a parent container that is joining the carousel and the controls together. Then a carousel container to set the width of the visible carousel, and one inner carousel container that will take the full width of all the items put together.

We are also using the children prop so that nesting can be used to ensure that it follows nice, clean and maintainable react patterns, as well as making it easier to use for anyone who might need to work on this later on.

With nesting, for example it will look like this:

<Carousel>
	<div>item 1</div>
	<div>item 2</div>
</Carousel>

Without nesting, our carousel component would look more like this:

<Carousel
	items={[
		(<div>item 1</div>),
		(<div>item 2</div>),
]}
/>

It is of course opinion as to which is preferred, but personally, I much prefer nesting with jsx because it feels more natural and clean.

Next, let’s add in a few basic styles that we will need for our react carousel, or most carousels for that matter.

Open up the carousel.module.scss file that we created and add in these styles:

.parent {
  position: relative;
  margin: 0 auto;
}

.container {
  position: relative;
  margin: 0 auto;
  overflow: hidden;
  max-width: 100%;
  width: 100%;
  z-index: 1;
}

.inner {
  display: flex;
  flex-direction: row;
}

.item {
  flex: 1;
  overflow: hidden;
}

.controls {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;

  * {
    pointer-events: auto;
  }
}

.button {
  position: absolute;
  top: 50%;
}

.next {
  composes: button;
  right: 0;
  transform: translateY(-50%) translateX(50%);
}

.prev {
  composes: button;
  left: 0;
  transform: translateY(-50%) translateX(-50%);
}

Firstly we are making the react carousel container hide all overflow.

Secondly, we are using flex on the inner container so we can have all carousel items aligned next to each other.

Finally the controls container has been absoluted over the whole carousel container, and made so the container is not clickable but the buttons inside will be. This is done with the use of pointer-events.

Now we can add this into our react carousel index.js file using css modules like this:

import React from 'react';
import PropTypes from 'prop-types';
import styles from './carousel.module.scss';

export default function Carousel({ children }) {
  return (
  <div className={styles.parent}> {/* Parent container */}
      <div className={styles.container}> {/* Outer carousel container (visible width) */}
        <div className={styles.inner}> {/* inner carousel container (width of entire carousel) */}
          {
            children.map(item => (
              <div className={styles.item}> {/* carousel item container */}
                {item}
              </div>
            ))
          }
        </div>
      </div>
      <div className={styles.controls}> {/* carousel controls container */}
        <button className={styles.next}>
          Next
        </button>
        <button className={styles.prev}>
          Prev
        </button>
      </div>
    </div>
  );
}

Carousel.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]).isRequired,
};

There is currently one major floor in this code, and that is we need to know the maximum width for the carousel so each item within the carousel can fit to that size so we get nice and consistent behaviour.

The easiest way to do this is be accepting a few props from the user of the carousel and provide a standard default option so it works out of the box as well.

The props we need to add in are width and unit, where width is a number and unit is a string that defines the unit such as px.

Add that in now like so:

import React from 'react';
import PropTypes from 'prop-types';
import styles from './carousel.module.scss';

export default function Carousel({ children, width, unit }) {
  return (
  <div
    className={styles.parent}
    style={{
      width: `${width}${unit}`,
    }}
  > {/* Parent container */}
      <div className={styles.container}> {/* Outer carousel container (visible width) */}
        <div className={styles.inner}> {/* inner carousel container (width of entire carousel) */}
          {
            children.map(item => (
              <div className={styles.item}> {/* carousel item container */}
                {item}
              </div>
            ))
          }
        </div>
      </div>
      <div className={styles.controls}> {/* carousel controls container */}
        <button className={styles.next}>
          Next
        </button>
        <button className={styles.prev}>
          Prev
        </button>
      </div>
    </div>
  );
}

Carousel.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]).isRequired,
  width: PropTypes.number,
  unit: PropTypes.string,
};

Carousel.defaultProps = {
  width: 500,
  unit: 'px',
};

Finally it is about time to see what this gives us, so open up your App.js and enter the following code to see this load up on localhost:

import React from 'react';
import './App.css';
import Carousel from './carousel';

function App() {
  return (
    <div className="App">
      <Carousel>
        <img src="https://picsum.photos/id/100/500/500" />
        <img src="https://picsum.photos/id/200/500/500" />
      </Carousel>
    </div>
  );
}

export default App;

Here we are literally just adding in the component to App.js and supplying a few images to it.

Great, so we have our basic styles done, next we need to get this react carousel actually working!

To do this we are going to use react hooks along with a few calculations for the infinte carousel part as well as the transitions.

Thinking logically, the first thing we are going to need to know is which item is currently active in the carousel, so we know what comes before and after it.

Create a useState hook at the top of our Carousel component called index by typing const [index, setIndex] = useState(0);

Next in the inner carousel jsx element add in the following styles so we can calculate the width of all the carousel items added together, and display the one that is currently active using our index hook.

style={{
            width: `${width * children.length}${unit}`,
transform: `translateX(-${width * index}${unit})`,
}}

This works by having a smaller container that hides the larger inner contents, then we simply move the larger inner contents left or right so the active item is showing in the smaller visible window created by the outer container.

For this to work we need to add the width to each item as well to ensure the carousel items are exactly the right width. We do that by adding the following style to the items:

          {
            children.map(item => (
              <div
                className={styles.item}
                style={{
                  width: `${width}${unit}`,
                }}
              >
                {item}
              </div>
            ))
          }

Finally, we need to add some onClick events to our buttons so that we can change the current active index, when the buttons are clicked.

This should look like this:

<div className={styles.controls}>
        <button
          className={styles.next}
          onClick={
            () =>
              setIndex(index >= children.length - 1 ? index : index + 1)
          }
        >
          Next
        </button>
        <button
          className={styles.prev}
          onClick={
            () =>
              setIndex(index <= 0 ? index : index - 1)
          }
        >
          Prev
        </button>
      </div>

Awesome!

That is now a simple working react carousel, but we need to make it transition a little smoother, to do this, you could use css transitions and animations, but we are going to do this controlled with JavaScript, React and react hooks.

The idea behind animating the react carousel using react means we will need to use lifecycle methods, to create the transition smoothly rather than jumping to the end transitional value.

For example, if our item is 500px wide and we move to the next item, instead of immediately jumping to 1000px, we would do 501px, 502px, 503px, and so on.

Because there is a lot of code that goes into this, it is worth extracting this into its own hook, so create a new file called useTransition.js in the same directory as the carousel.

And add the following code into it:

import { useState, useEffect } from 'react';

// this function works as our way to control the animation speed
function getNextTransition(width, index, translate, direction, setTranslate) {
  const to = width * (index + (direction === 'next' ? 1 : -1));

  // within half the width, slow down
  if (
    (to - translate < (width / 2) && direction === 'next')
    || (to - translate > -(width / 2) && direction === 'prev')) {
      setTranslate(direction === 'next' ? translate + 1 : translate - 1);
  }

  setTranslate(direction === 'next' ? translate + 2 : translate - 2);
}

export default function useTransition(width, children) {
  const len = children.length;

  // declare state variables
  const [index, setIndex] = useState(1);
  const [translate, setTranslate] = useState(width);
  const [action, setAction] = useState({ lastAction: '', currentAction: '' });
  const [items, setItems] = useState([ children[len - 1], ...children ]);
  const setNextAction = (currentAction) => {
      setAction({
        lastAction: action.currentAction,
        currentAction,
      });
    }

  // this effect hook will be triggered every time the "translate variable will change"
  useEffect(() => {
    // if the transition has not completed, continue with transition
    if ((translate < (index + 1) * width && action.currentAction === 'next') || (translate > (index - 1) * width && action.currentAction === 'prev')) {
      getNextTransition(
        width,
        index,
        translate,
        action.currentAction,
        setTranslate,
      );
    } else if (action.currentAction !== '') {
      // otherwise set the next action to be ''
      setNextAction('');
    }
  }, [translate]);

  // this effect hook will be triggered every time the action object changes.
  useEffect(() => {
    // if we click next when we are at the end of our carousel.
    if (action.currentAction === 'next' && index + 1 > len) {
      // add the first item to the end of the array
      setItems([ children[len - 1], ...children, children[0]]);
      // start transition
      setTranslate(translate + 1);

      // if we clicked next
    } else if (action.currentAction === 'next') {
      // start transition
      setTranslate(translate + 1);

      // if we clicked prev
    } else if (action.currentAction === 'prev') {
      // start transition
      setTranslate(translate - 1);

    // if we have gone past the last item (and onto the extra one)
    } else if (index + 1 > len && action.lastAction === 'next') {
      // reset items to initial state
      setItems([ children[len - 1], ...children ]);
      // set translate to the 1 index of the array
      setTranslate(width);
      // and set the current index to one.
      setIndex(1);

    // if we reached the first (duplicate) item in the array and want to go back
    } else if (index - 1 === 0 && action.lastAction === 'prev') {
      // set index to last item in array
      setIndex(len);
      // set translate to last item in array
      setTranslate(len * width);

    // if transition next happened
    } else if (action.lastAction === 'next') {
      // update index
      setIndex(index + 1);

    // if transition prev happened
    } else if (action.lastAction === 'prev') {
      // update index
      setIndex(index - 1);
    }
  }, [action]);

  // return all variables to be used with the hook.
  return {
    index,
    translate,
    setIndex,
    setTranslate,
    items,
    setAction: setNextAction,
  };
}

We are doing quite a few things here, as you can see from the code.

Briefly, we are using react itself to create our transition, by adding a hook that detects changes to the translate variable.

When it changes, it will check to see if it needs to continue transitioning, if it does it will update again, causing the effect to be triggered again.

This will go on until the transition has been completed which is how the transition works.

Next, to make it infinite, we have achieved this by adding another set of conditions.

If we click the next button when we are at the end of our carousel, we need to copy the first item to the end, and then start the transition so it looks like we are moving on to the first item, from the last.

Once the transition finishes, we immediately delete this item, and jump to the actual first item and then set the index accordingly.

Going backwards in our infinite react carousel is handled slightly differently, if you look at the code on mount, we actually start from the 1st index rather than the 0th, because from the very beginning we are duplicating the last item in the array to the start of the array.

From here, if the user presses the prev button, it looks like it has moved to the last item, and once again, when the transition finishes it immediately sets the translate and index to the last item in the array.

And that is how the infinite react carousel functionality comes into play.

Now that is done, finally head back in your index.js file make the following changes to make use of this:

import React from 'react';
import PropTypes from 'prop-types';
import styles from './carousel.module.scss';
import useTransition from './useTransition';

export default function Carousel({ children, width, unit }) {
  // here we extracted the carousel functionality into its own hook
  const {
    translate,
    items,
    setAction,
  } = useTransition(width, children);

  const handleNext = () => setAction('next');
  const handlePrev = () => setAction('prev');

  return (
  <div
    className={styles.parent}
    style={{
      width: `${width}${unit}`,
    }}
  >
      <div className={styles.container}>
        <div
          className={styles.inner}
          style={{
            width: `${width * items.length}${unit}`,
            transform: `translateX(-${translate}${unit})`,
          }}
        >
          {
            items.map(item => (
              <div
                className={styles.item}
                style={{
                  width: `${width}${unit}`,
                }}
              >
                {item}
              </div>
            ))
          }
        </div>
      </div>
      <div className={styles.controls}>
        <button
          className={styles.next}
          onClick={handleNext}
        >
          Next
        </button>
        <button
          className={styles.prev}
          onClick={handlePrev}
        >
          Prev
        </button>
      </div>
    </div>
  );
}

Carousel.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]).isRequired,
  width: PropTypes.number,
  unit: PropTypes.string,
};

Carousel.defaultProps = {
  width: 500,
  unit: 'px',
};

And there we have our finished infinite react carousel!

You can view the final code here on github ready to clone: https://github.com/WillMayger/create-react-carousel-tutorial

Any questions don’t hesitate to send me a tweet!

Will