React Responsive Slider

Joe Keohan
ITNEXT
Published in
5 min readDec 19, 2019

--

Introduction

In my last article 3 Ways To Implement Responsive Design In Your React App I discussed several ways to implement responsive design in a React app. Inspiration for that app was mars.nasa.gov, and in this article I continue to rebuild additional portions of the site with the main focus on implementing the bottom slider functionality in the header section on the main page.

Here is a deployed version and the codesandbox:

App: https://02nz9.csb.app/

Codesandbox: https://codesandbox.io/s/mars-slider-demo-02nz9

The “BottomSlider” Component

Creating a Component to encompass all the elements and functionality for the slider seems the most practical approach, especially since I was considering including it other apps I’d like to build.

As I’m always looking to keep myself up to date with the latest and greatest React has to offer I’ve decided to incorporate Hooks. I’ve included useReducer to manage PREV/NEXT/RESET states, useEffect to initiate the RESET state when the window.innerWidth value changes, and useRef to grab a reference the .slick-track element that holds the sliders.

Here is the basic structure. Additional content such as the reducer’s inner workings or the sliderController object will be explained later in the article.

import React, { useReducer, useEffect, useRef } from  "react";
import './BottomSlider.css'
function BottomSlider() { // useRef
const slideTrackRef = useRef()
// useReducer
const reducer = (state, action) => {
...rest of code
}
const [sliders, dispatch] = useReducer(reducer, sliderController)
// useEffect
useEffect( () => {
dispatch('RESET')
}, [windowWidth()])
...rest of code
}

The Content

The content is an array of objects that I’ve mirrored from the mars site. For the sake of organization I’ve placed the content into its own file called BottomSliderData.js and imported it into the main BottomSlider.js Component.

// BottomSlider.js Component
import sliderContent from './BottomSliderData'
// BottomSliderData.js
export default[{
title: 'Curiosity',
value: '2440 SOLS ON MARS'
},...additional objects]

Render The Content

As I like to extrapolate any looping constructs(.map) from the Components return statement, I opted to create a variable called renderSlickTrackSlides that stores the results of mapping over the sliderContent array and creating the individual slider elements.

const renderSlickTrackSlides = sliderContent.map( (d,i) => (
<article className="slide" key={i}>
<div className="image_and_description_container">
<div className="readout">
<div className="title">{d.title}</div>
<div className="value">{d.value}</div>
</div>
</div>
<span className="circle_plus"></span>
</article>
))

The .slick-track will render those elements and has also been assigned the slideTrackRef reference that was defined earlier.

<div className="slick-track" ref={slideTrackRef}>  
{renderSlickTracks}
</div>

Buttons have been placed on either side of the .slick-list element and have been assigned background images that were pulled directly from the mars site. Thank you NASA!

<section className="dashboard">
<div className="slide_container">
<button className="slick-prev"></button>
<div className="slick-list">
<div className="slick-track" ref={slideTrackRef} >
{renderSlickTracks}
</div>
<button className="slick-next"</button>
</div>
</div>
</section>

Adding Event Listeners

Click events were added to both the .slick-prev and .slick-next elements and are configured to call the useReducer’s dispatch method passing it one of two values: “PREV” or “NEXT”.

<section className="dashboard">
<div className="slide_container">
<button className="slick-prev" onClick={() => dispatch('PREV')}>
</button>
...slick-list
<button className="slick-next" onClick={() => dispatch('NEXT')}>
</button>
</div>
</section>

Showing The Right Number Of Slides

Responsive design has always taken precedence in this project and so I’ve created the windowWidth() function that returns the current value of window.innerWidth.

The numOfVisibleSlides() function calls windowWidth() and returns the value equal to the number of slides currently being shown based on a specific breakpoint.

const windowWidth = () => {
return window.innerWidth;
};
const numOfVisibleSides = () => {
switch (true) {
case windowWidth() <= 767 :
return 1
case windowWidth() <= 1023 :
return 2
default :
return 3
}
}

A sliderController object was created to keep track of the following properties:

  • numOfVisibleSlides: the number of slides visible at the current window width
  • numOfSliders: the current length of the sliderContent array
  • translateValue: the sliders’ current transition value
const sliderController = {
numOfVisibleSlides: numOfVisibleSldies(),
numOfSliders: sliderContent.length,
translateValue: 0
}

UseReducer To Manage The NEXT & PREV States

The useReducer Hook is a great choice for more complex state management. If you’re new to useReducer then the additional code overhead might seem much at first, however, it’s a great tool for managing your state and application logic, thereby making it a bit more intuitive and readable.

useReducer is passed a reducer function which is bound to the dispatch method and the sliderController array bound to the sliders variable.

const [sliders, dispatch] = useReducer(reducer, sliderController)

The “NEXT” and “PREV” actions are tied to event listeners currently assigned to the forward/back buttons. “RESET” was added to reset the useReducers state thereby transitioning the slider back to its original placement when the window.innerWidth value changes.

function reducer(state,action) {
switch(action) {
case 'RESET' :
return sliderController
case 'PREV' :
return handlePrev(state)
case 'NEXT' :
return handleNext(state)
default :
return state
}
}

useReducer leverages switch statements as it’s more intuitive than trying to read a series of if/else if/else statements.

Moving Forward

The handleNext() function needs to do the following:

  • Calculate the distance to transition left along the horizontal axis
  • Apply the transition and display the next slide in the sequence
  • Prevent further transitions once the the max limit of slides is reached
  • Return a new version of state with those changes
function handleNext({numOfVisibleSlides, numOfSlides, translateValue}) {  if (numOfVisibleSlides >= numOfSlides) {
return { numOfVisibleSlides, numOfSlides, translateValue }
}
let value = numOfVisibleSides() translateValue += -(slideTrackWidth() / value); return {
numOfVisibleSlides: numOfVisibleSlides += 1,
numOfSlides,
translateValue
}
}

Moving Backwards

The handlePrev() function needs to do the following:

  • Calculate the distance to transition right along the horizontal axis
  • Apply the transition and display the next slide in the sequence
  • Prevent further transitions once the base limit of slides is reached
  • Return a new version of state with those changes
function handlePrev({numOfVisibleSlides, numOfSlides, translateValue}) { if (numOfVisibleSlides <= numOfVisibleSides()) {
return { numOfVisibleSlides, numOfSlides, translateValue }
}
let value = numOfVisibleSides() translateValue += slideTrackWidth() / value; return {
numOfVisibleSlides: numOfVisibleSlides -= 1,
numOfSlides,
translateValue
}
}

Both functions call the slideTrackWidth function which returns the current width of .slick-track. Its width will change in response to window width.

const slideTrackWidth = () => {
return slideTrackRef.current.clientWidth;
};

Conclusion

After several refactors and a little bit of Hook magic, I’ve created a reusable and responsive Component.

--

--

Software Engineering Instructor at General Assembly. React and D3 evangelist and passionate about sharing all this knowledge with others.