TLDR

Make a stepped Slider component provide immediate visual feedback by utilizing onSlidingComplete, Math.round and setNativeProps.

Introduction

While working on a React Native project that utilized a Slider component we received some feedback that the Slider didn’t feel very smooth. For some background, the Slider displayed 3 different values that a user could choose from and the goal was to only allow these three values as a possible selection on the slider. This meant that we had to lock the Slider down using the step property. Here’s a simplified version of the configuration:

<Slider
  minimumValue={1}
  maximumValue={3}
  step={1}
  onSlidingComplete={this.onSlidingComplete}
/>

Which when shown in an Expo snack renders the following:

slider gif

As you can see the Slider is displaying correctly and we have locked down our possible values to only to a specific range using the minimumValue, maximumValue, and step properties. There’s a UX issue with this setup though - the user is not provided with immediate visual feedback that the slider is moving until they get about half between the current value and the next. Now sure if we used a bigger range and a smaller step we could alleviate this issue, but that doesn’t allow us to keep our possible values to the initial 3 that we wanted. The step property is what’s causing our slider to not report immediate visual feedback. If we remove the step property the slider shows immediate feedback upon moving, but that causes our value to fall out of line with the 3 values that we’re looking for. Here’s what we get after removing the step property:

slider gif

As you can see our slider’s value isn’t one of the 3 values we’re looking for and even worse our Slider no longer snaps to one of the three values.

At first glance the solution should have been fairly quick (and the final one still is): Just utilize the onSlidingComplete callback and a value state prop to set the value of the Slider. To get the value to snap correctly we set the state value based on Math.round. So now we have the following code:

onSlidingComplete = (v) => {
  const rounded = Math.round(v);
  this.setState({sliderValue: rounded});
}

render() {
  const {sliderValue} = this.state;

  ...

  <Slider
    minimumValue={1}
    maximumValue={3}
    step={1}
    onSlidingComplete={this.onSlidingComplete}
    value={sliderValue}
  />

  ...
}

Well it didn’t work, but it didn’t fail completely. Our value is actually snapping to one of our values, but our Slider isn’t really honoring the rounded value. It’s snapping to a value some of the time, but not all of the time. Looking at the documentation for the Slider a little closer we see that the value prop is only used for setting the initial value. The Slider in the end is an uncontrolled component and we can only get values reported back to us.

This is where setNativeProps comes in. Since we can set the value through our state we need to set the value directly by accessing the native value prop of the Slider. Luckily working with setNativeProps is really easy. All we have to is add a reference to our component and then call setNativeProps in our onSlidingComplete method. Here’s what we have now:

onSlidingComplete = (v) => {
  const rounded = Math.round(v);
  this.setState({sliderValue: rounded});
  this.slider.setNativeProps({value: rounded});
}

render() {
  const {sliderValue} = this.state;

  ...

  <Slider
    ref={(c) => this.slider = c}
    minimumValue={1}
    maximumValue={3}
    step={1}
    onSlidingComplete={this.onSlidingComplete}
    value={sliderValue}
  />

  ...
}

And here’s our result:

slider gif

Perfect! Our user gets immediate feedback that the Slider is moving and we get the satisfaction that the value will always be one of the 3 possible values we want.

Here’s our complete code for this example from the following Expo snack:

import React, {PureComponent} from 'react';
import { Text, View, Slider, StyleSheet } from 'react-native';

export default class App extends PureComponent {
  state = {
    sliderValue: 1,
  }

  onSlidingComplete = (v) => {
    const rounded = Math.round(v);

    this.setState({sliderValue: rounded});
    this.slider.setNativeProps({value: rounded});
  }

  render() {
    const {sliderValue} = this.state;

    return (
      <View style={styles.container}>
        <Text>Choose a value</Text>
        <Slider
          ref={(c) => this.slider = c}
          minimumValue={1}
          maximumValue={3}
          onSlidingComplete={this.onSlidingComplete}
          value={sliderValue}
        />
        <View style={styles.values}>
          <Text>1</Text>
          <Text>2</Text>
          <Text>3</Text>
        </View>
        <Text>{`Slider Value: ${sliderValue}`}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    padding: 25,
  },
  values: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: 12,
  }
});

Conclusion

In the end by using setNativeProps we were able to solve the issue without adding another dependency to the project. There was several solutions out there that were more than capable of solving the issue, but I didn’t really think this justified the additional library and bundle bloat.