4

I'm trying to create a range slider in ReactJS

rangeSlider.jsx

const RangeSlider = ({onChange}) => {

    const [slider, setSlider] = useState({
        max: 100, 
        min: 0, 
        value: 0, 
        label: ''
    });

    const onSlide = () => {
        onChange(slider.value);
    } 

    return (
        <div className="range-slider">
            <p>{slider.label}</p>
            <input type="range" min={slider.min} max={slider.max} value={slider.value} 
             onChange={() => onSlide()} className="slider" id="myRange"></input>
        </div>
    );
}
export default RangeSlider;

then I use it in other components

 <RangeSlider onChange={(value) => sliderValueChanged(value)} />
  • If I wanted to pass in a custom label, how would I update the state to do that?
  • Do I have to use React.memo for this? My understanding, every time the slider value changes it creates a new instance of the slider.
  • I'd like this to be robust (steps, multi-handles, tool-tips, etc.) eventually, any help is appreciated.
deekeh
  • 675
  • 1
  • 6
  • 21
Skeeter62889
  • 95
  • 1
  • 2
  • 6

3 Answers3

8

When you want to create a reusable component, always try to pass the configuration from where it's uses and keep all common configurations inside the component

EX: Read about how useMemo and useReducer works

useMemo

useReducer

const App = () => {
  //Keep slider value in parent
  const [parentVal, setParentVal] = useState(10);

  //need useCallback why? if this component rendered we don't want to recreate the onChange function
  const sliderValueChanged = useCallback(val => {
    console.log("NEW VALUE", val);
    setParentVal(val);
  });

  // need useMemo why? if this component rendered we don't want to recreate a new instance of the configuration object,
 // but recreate it when parentVal gets changed, so Slider will re-render,
 // and you can remove parentVal from dependency array and once the parent parentVal gets updated slider will not be re-renderd
  const sliderProps = useMemo(
    () => ({
      min: 0,
      max: 100,
      value: parentVal,
      step: 2,
      label: "This is a reusable slider",
      onChange: e => sliderValueChanged(e)
    }),
    // dependency array, this will call useMemo function only when parentVal gets changed,
    // if you 100% confident parentVal only updated from Slider, then you can keep empty dependency array
    // and it will not re-render for any configuration object change 
    [parentVal]
  );

  return (
    <div>
      <h1>PARENT VALUE: {parentVal}</h1>
      <RangeSlider {...sliderProps} classes="additional-css-classes" />
    </div>
  );
};

and in Slider component

//destructive props
const RangeSlider = ({ classes, label, onChange, value, ...sliderProps }) => {
     //set initial value to 0 this will change inside useEffect in first render also| or you can directly set useState(value)
    const [sliderVal, setSliderVal] = useState(0);

    // keep mouse state to determine whether i should call parent onChange or not.
    // so basically after dragging the slider and then release the mouse then we will call the parent onChange, otherwise parent function will get call each and every change
    const [mouseState, setMouseState] = useState(null);

    useEffect(() => {
      setSliderVal(value); // set new value when value gets changed, even when first render
    }, [value]);

    const changeCallback = (e) => {
      setSliderVal(e.target.value); // update local state of the value when changing
    }

    useEffect(() => {
      if (mouseState === "up") {
        onChange(sliderVal)// when mouse is up then call the parent onChange
      }
    }, [mouseState])

    return (
      <div className="range-slider">
        <p>{label}</p>
        <h3>value: { sliderVal }</h3>
        <input
          type="range"
          value={sliderVal}
          {...sliderProps}
          className={`slider ${classes}`}
          id="myRange"
          onChange={changeCallback}
          onMouseDown={() => setMouseState("down")} // When mouse down set the mouseState to 'down'
          onMouseUp={() => setMouseState("up")} // When mouse down set the mouseState to 'up' | now we can call the parent onChnage
        />
      </div>
    );
};

export default memo(RangeSlider);

check my demo

I guess this answer call the 3 questions

  1. use configuration in parent to pass the non-common configuration like label

  2. Use memo ? Yes, so Slider component will only get rendered only when the props gets changed. But you have to carefully design it (ex: useMemo and useCallback)

  3. steps ? use configuration object in parent to pass these.


Just in case if you need a nice way to wrap a range i would suggest you to use a custom hook

const useSlider = ({ value, ...config }) => {
  const [sliderVal, setSliderVal] = useState(value); // keep a state for each slider

  const [configuration, setConfiguration] = useState(config); // keep the configuration for each slider

  const onChange = useCallback(val => {
      setSliderVal(val);
  // useCallback why? we dont need to recreate every time this hook gets called
  }, []);

  useEffect(() => {
    setConfiguration({
      ...config,
      onChange,
      value: sliderVal
    });
  // when sliderVal gets changed call this effect
  // and return a new configuration, so the slider can rerender with latest configuration
  }, [sliderVal]);

  return [sliderVal, configuration];
};

Here is a demo

This might can be further improvement

Alan W. Smith
  • 24,647
  • 4
  • 70
  • 96
Kalhan.Toress
  • 21,683
  • 8
  • 68
  • 92
  • Nice explanation! – Vinodhan Jul 04 '20 at 06:09
  • Nice, this is very helpful. So, if I am going to use several range sliders on a page, would I then need to use a useState, useCallback, and useMemo, for each instances? For example, I have 5 range sliders in my parent and each has their own useState, useCallback, and useMemo. – Skeeter62889 Jul 05 '20 at 01:28
  • Glad it helps you! @Skeeter62889 YES because you have to have different set of configurations for different sliders isn't it?, So it's better to have separated logic, otherwise it will be hard to maintain when the code grows up – Kalhan.Toress Jul 05 '20 at 06:40
  • @Skeeter62889 I just updated the answer with a nice approach, check it out, happy coding.. – Kalhan.Toress Jul 05 '20 at 07:20
  • The link to the hook doesn't work. I get this error: Error in /~/index.js (75:9) Cannot access 'slider1' before initialization – Coco Mar 04 '23 at 19:47
1

First question you have to ask yourself is: Where do i keep the state for the slider? Answer: Keep the state in the parent component and pass it to RangeSlider in order to keep state controlled and consistent. Utility components like these in most cases should never keep their own state.

const ParentComponent = () => {
   const [sliderProps, setSliderProps] = useState({
     min: 0,
     max: 100,
     value: 20,
     label: 'This is a reusable slider'
   });
   const [sliderValue, setSliderValue] = useState(0);

   const handleSliderChange = e => {
     setSliderValue(e.target.value);
   };

   const handleMouseUp = e => {
      // do something with sliderValue
   };

   return (
      <RangeSlider 
        {...sliderProps}
        classes=""
        onChange={handleSliderChange}
        onMouseUp={handleMouseUp}
        value={sliderValue} />
   );
}

And you Range Slider component:

const RangeSlider = ({ 
  classes, 
  label, 
  onChange, 
  onMouseUp, 
  value, 
  ...sliderProps 
}) => {
    useEffect(() => {
      // if you dont set your inital state in your parent component 
      // this effect will only run once when component did mount and 
      // passes the initial value back to the parent component.
      onChange(value); 
    }, []);
    
    return (
      <div className="range-slider">
        <p>{label}</p>
        <h3>value: { value }</h3>
        <input
          {...sliderProps}
          type="range"
          value={value}
          className={`slider ${classes}`}
          id="myRange"
          onChange={onChange}
          onMouseUp={onMouseUp} // only if such effect is desired
        />
      </div>
    );
};
export default memo(RangeSlider);

Regarding your worries about calling new instances. When your passes props are changing, it DOESNT cause a new instance for this component. It only causes a rerender. In such a small component like this RangeSlider, it takes very little computing power to rerender this, so its not neccessary to hack around passing props and just pass props consistently from your parent.

Normally Range Sliders have a direct effect on your UI or keep a state for a form, therefore having the onChange triggered only on "mouseup" will restrict your component for reusability purposes and only covers very few cases. In case you like to have a behavior like @Kalhan.Toress explained, i recommend handling that logic in your parent components. To enable that you just have to pass the "mouseup" event through a callback as shown before.

You really dont need to worry about performance in this case. Your RangeSlider is way to small on simple in order to mess up your application.

I hope this helps

Jan-Philipp Marks
  • 1,419
  • 8
  • 13
0
  1. You can destructure your props with label and use useEffect to change your current slider
const RangeSlider = ({ onChange, label }) => {
  // other stuff
  useEffect(() => {
    setSlider(current => ({
      ...current,
      label
    }));
  }, [label]);
};

RangerSlider.js

 <RangeSlider label="Custom label" onChange={(value) => sliderValueChanged(value)} />
  1. Not necessarily, it depends on how you handle your dependency arrays to make sure that it only re-renders when necessary.

EDIT: I also noticed that you are not actually changing the local state of your RangeSlider in order to subscribe to changes. You might also want to put another useEffect in your RangerSlider

const RangeSlider = ({ value, onChange, label }) => {
  // other stuff
  useEffect(() => {
    setSlider(current => ({
      ...current,
      value
    }));
  }, [value]);
};

RangerSlider.js

 <RangeSlider label="Custom label" onChange={(value) => sliderValueChanged(value)} value={INSERT_THE_STATE_HERE} />

If I would implement this component, I would just avoid creating a local state in RangerSlider and pass all values and slider configurations as props to improve performance

blankart
  • 716
  • 5
  • 13