React Carousel with Server Side Rendering Support Part 2

React Carousel with Server Side Rendering Support Part 2

07 March, 2019
React Carousel with Server Side Rendering Support  Part 2

This is part two of the series “React Carousel Component with Server Side Rendering Support”. In this article, I am going to show you how you can create your own Carousel component using React that supports multiple items and server-side rendering with the implementation in codes.

If you didn’t read our part one, you can find it at w3js.

If you prefer to jump right to the code, I have made two versions of the Carousel, first version is the one we will be building in this tutorial with basic usage and the bare minimal amount of code that’s easier for you to understand and beginner friendly. For the second version, it is made as a npm package which is an improved version of the first one with more advanced usage.

The source code for the Carousel we are building is here on Github. The Carousel npm package with more advanced usage.

Content:

Technical approach

Implementation

Recap

At part one of the article, we decided that we will be using “flex-basis” to render our components on the server-side, and replace the “flex-basis”to “width” once we entered the browser environment and our JavaScript has been downloaded.

Implementation

For the sake of simplicity, I am building the component using Nextjs which is a React framework that server-side renders you application automatically. If you know React, you will be able to follow along as we will be using pure react almost 100%. (You can also use create-react-app)

As this article will be about building the Carousel so will skip the boilerplate for now, you can either create a Nexjts application similar to this one . Or you can just simply create a component called Carousel.js in your create-react-app.

In Carousel.js:

import React, { Component } from "react";

class Carousel extends Component { constructor(props) {
super(props);
this.state = {
// if domLoaded is true if means we are not longer at the server-side. domLoaded: false, // itemWidth will be the average width of our Carousel items // Its default to 0 as we don't have access to it at the beginning itemWidth: 0, // container width will be the total width of our entire Carousel component. containerWidth: 0, // slidesToShow will be the number of items we are showing to the user at a time. slidesToShow: 0, // slidesToSlide means how many slides we are sliding at a time. slidesToSlide: 1, // currentSlide is the current index of our arrow of Carousel items // which we are using later on in conjunction with the transform property. currentSlide: 0, // totalItems is the total number of Carousel items we have in the Carousel, this is done // by the React.Children api. totalItems: React.Children.count(props.children), // deviceType will either be "mobile", "tablet" or "desktop". deviceType: "", // transform here will be used in conjunction with "translate3d" transform: 0, // breakpoint here is hard-coded for the sake of simplicity // for screen size that's larger than 900px, and smaller than 3000px, we are showing 3 items at the time. // at 500px to 900px screen sizes and at less than 500px screen size we are showing 2 items and one item repspectively breakpoint: { desktop: { min: 900, max: 3000, itemsToShow: 3 }, talet: { min: 500, max: 900, itemsToShow: 2 }, mobile: { min: 0, max: 500, itemsToShow: 1} } }; }

componentDidMount() { // our React codes have been executed and downloaded by the browser this.setState({ domLoaded: true }); }

render() { // children will be all of our Carousel items. const { children } = this.props; return <div>{children}</div>; } } export default Carousel;

The purpose of state that we

declared abovewill be clearer once we are piecing everything together in a short while.

Next we will be using the React children api to transform our Carousel items to add a wrapper for each of them so that we can have better control of the component. See the following:

render() {
  // children will be all of our Carousel items.
  const { children } = this.props;
  return (
    <div>
      <ul>
        {React.Children.toArray(children).map((child, index) => (
          <li
            key={index}
          >
            {child}
          </li>
        ))}
      </ul>
    </div>
  );
}

By giving a wrapper to each of the item using the “React.Children.toArray” method allows us add a html list makeup to pass custom style for whatever items that we are showing, so that we can decide the width and animation of the item ourselves.

Next, we want to detect the user’s device type, and based on that we decided how many items we are showing on the server-side. You can usually get the user agent on the server-side with the request header, on the browser-side you can use navigator.userAgent.

And user agent usually contains information about the user’s device, and based on that we can determine if the user is using a tablet, mobile, or desktop.

let userAgent;
let deviceType;
if (req) {
  userAgent = req.headers["user-agent"];
} else {
  userAgent = navigator.userAgent;
}
const md = new MobileDetect(userAgent);
if (md.tablet()) {
 deviceType = "tablet";
} else if (md.mobile()) {
 deviceType = "mobile";
} else {
 deviceType = "desktop";
}

In the above example i am using a library to handle this for me, you can gain access to the “req” object very easily in a server-side rendered app with this example herefrom line 23 to 36.

Next, we want to add different stylings to our component based on whether or not we are on the server-side as the following:

render() {
    // children will be all of our Carousel items.
    const deviceType = 'desktop' // presuming this deviceType is the result after our user-agent detection for the sake of simplicity.
    let itemWidth;
    const isServerSide = !this.state.domLoaded && deviceType;
    if (isServerSide) {
      itemWidth = (100 / this.state.breakpoint[deviceType].itemsToShow).toFixed(1);
      // we are on desktop, then the item width here will be 33.3% based // on our pre-defined breakpoint that we declared in the constructor.
    } else {
      itemWidth = this.state.containerWidth / this.state.slidesToShow;
    }
    const { children } = this.props;
    return (
      <div>
        <ul
          style={{
            overflow: isServerSide ? "hidden" : "unset",
            transform: 

translate3d(${this.state.transform}px,0,0)

}} > {React.Children.toArray(children).map((child, index) => ( <li style={{ flex: isServerSide ?

1 0 ${itemWidth}%

: "auto", width: !isServerSide ?

${itemWidth}px

: "auto" }} key={index} > {child} </li> ))} </ul> </div> ); } }

In the above code snippets, we got the server-side part covered, but once we enter the client-side, we need to get the total width of our Carousel component so that we know how much width to assign to each of the item so that they all look nice and even.

For this we will be using the React’s ref api adding the following to our constructor method.

constructor(props){
  super(props);
  this.containerRef = React.createRef();
}

And then we are assigning the ref to the very first “div” in our render method which is the container of our entire Carousel component as the following:

<div ref={this.containerRef}>
    <ul
      style={{
        overflow: isServerSide ? "hidden" : "unset",
        transform: 

translate3d(${this.state.transform}px,0,0)

}} > …rest of our stuff

And in our componentDidMount method we add the following to get the total width of our container:

componentDidMount() {
  this.setState({ domLoaded: true });
  if(this.containerRef && this.containerRef.current) {
    // here we get the total width of the container
    this.setState({ containerWidth: this.containerRef.current.offsetWidth});
  }
}

You can read more about the React ref api in this link.

Now that we have the total width of the container, we are lacking one more thing which is to decide how many items we are showing at each break point by doing a comparison between the window.innerWidth with our pre-defined breakpoint with the following:

omponentDidMount() {
    this.setState({ domLoaded: true });
    if(this.containerRef && this.containerRef.current) {
      // here we get the total width of the container
      this.setState({ containerWidth: this.containerRef.current.offsetWidth});
    }
    Object.keys(this.state.breakpoint).forEach(breakpoint => {
      const { min, max, itemsToShow } = this.state.breakpoint[breakpoint];
      if(window.innerWidth >= min && window.innerWidth <= max) {
        this.setState({ slidesToShow: itemsToShow });
      }
    })
  }

Now if we pass in some images to our Carousel component and run the application, you will see them being shown nicely in the browser with even width, and the rest of the items are hidden as we only want to show them as we sliding it through.

<Carousel>
      <img style={{ padding: 10, width: '100%' }} src='/image1' />
      <img style={{ padding: 10, width: '100%' }} src='/image2' />
      <img style={{ padding: 10, width: '100%' }} src='/image3' />
    </Carousel>

If you are using server-side rendering, you can test if it works on the server-side by disabling the Javascript temporarily in your browser setting, and you can still see our items are showing nicely still.

If you don’t want to show images, you can pass in whatever components you prefer that suits your website’s need, for instance a list of card components showing different information.

Next, we shall implement our navigation functionality being able to slide through between items to items. So, let’s create two buttons with an “onClick” handler that are going to have the navigation functionality and place them at the bottom of the render method with the following:

// how to style them is up to you.

<button onClick={() => this.next()}>Next</button> <button onClick={() => this.previous()}>Previous</button>

Next let’s create two functions called “next” and “previous” that allows us to navigation to the next or previous slides.

For the next function, we need to at first get the new few items that we are sliding into with the following:

next(){ const { slidesToShow, slidesToSlide, containerWidth } = this.state; const itemWidth = containerWidth / slidesToShow; // the reason for the 1 is because our index starts at 0. const nextItems = this.state.currentSlide + 1 + slidesToShow; }

There are two edge cases that we would like to handle here:

  1. If we are reaching the end of the Carousel, we want to reset all the items back to the beginning as they were.
  2. If we are not reaching the end of the Carousel, we want to show the next items using translate3d.

if (nextItems <= this.state.totalItems) { // if we are not reaching the end of the Carousel. const nextCarouselPosition = -itemWidth * (this.state.currentSlide + slidesToSlide); // notice that our nextCarouselPosition will be a nagative value this.setState({ transform: nextCarouselPosition, currentSlide: this.state.currentSlide + slidesToSlide }); } else { this.setState({ transform: 0, currentSlide: 0 }); }

And in our render method, we wanted to utilize the “transform” property that we are setting to the state so show the next list of items, and hide the past list of items.

<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", alignItems: "center", transition: 'all .5s', overflow: isServerSide ? "hidden" : "unset", transform:

translate3d(${this.state.transform}px,0,0)

}} >

...rest of our stuff.

That was our “next” function and we want to implement the similar logic but with the opposite value to our “previous” function:

previous() { const { slidesToShow, slidesToSlide, containerWidth } = this.state; const itemWidth = containerWidth / slidesToShow; const previousItems = this.state.currentSlide - slidesToSlide; if (previousItems >= 0 ) { // if we are not passing the beginning of the Carousel. const nextCarouselPosition = -itemWidth * previousItems; this.setState({ transform: nextCarouselPosition, currentSlide: previousItems }); } else { const nextItems = this.state.totalItems - slidesToShow; const nextCarouselPosition = -itemWidth * nextItems; // otherwise, we want to go straight to the end of the Carousel to achieve the infinite effects. this.setState({ transform: nextCarouselPosition, currentSlide: nextItems }); } }

That’s pretty much it for our basic Carousel functionality.

Recap

In this part 2, we have learnt how Carousel works under the hood by using the “transform” property with “translate3d” to navigate between items to items, and using “overflow”: hidden at the parent element to hide the items that we are not showing at the moment.

As most of the Carousel components out there nowadays don’t support server-side rendering as they are shown purely depending on the dom access. But however we broke that limit by designing a solution and we implement it in this series, we gained some amount of understanding on how server-side rendering works by making our Carousel show up on the server-side without the need to gain access to the dom.

What should be done next

The Carousel that we just implemented works, but to take it further, for example making it reusable to share at NPM for other people to use. You might want to do the followings:

  1. Handle screen resizing. This can be done easily by adding a resize handler as shown in here at line 48 . Whenever we are resizing, we want to recalculate the width of our container, and making sure we are still showing the valid number of items at the “resized” breakpoint so that our Carousel items are at the correct position.
  2. Swiping on mobile and Dragging on desktop. This can be done by adding the according event handlers which can also be found at the example.
  3. Allow passing of custom breakpoint and device type through props. Currently in the example they are hard-coded, here is an exampleon how to allow for customization.
  4. Add automated testing. As we are improving the Carousel component continuously by adding new features, for every changes we made, we are not certain anymore if some existing functionalities that were previous working before don’t work anymore because of the new changes we have introduced. Having testing can save you some time to going back to do regression testing manually.

In the next and final post of this series, i will be adding testing on all the functionalities we implemented in this tutorial for those interested, stay tuned!

CarouselDOMNPMReactServer-side Rendering