skip to Main Content

In an app I am building a custom range slider. I have a state that maintains the current value and also a prop that is exposes a callback triggered on change of this internal state. Below is how I am currently doing this:

const Slider = ({onChange}) => {
  const [value, setValue] = useState(0)
  // ...
  useEffect(() => onChange?.(value), [value, onChange])
  // ...
}

The issue here is that the onChange is triggered on component load since onChange is part of the dependency list. But if I remove this from the list, it will not trigger on changing the definition of the callback.

What is the correct approach here?

  1. The above is correct and the subscriber of the onChange should take into account that it will be called even when change actually doesn’t happen.
  2. The onChange is triggered in the same function that sets the value state – but it can happen in multiple places and I need to ensure it is called in all the required places.

3

Answers


  1. Updated answer, here is a slider from scratch fully functional.

    import {useState} from "react";
    const CustomSlider = ({min, max, value, onChange}) => {
        const [sliderValue, setSliderValue] = useState(value);
        const handleSliderChange = (event) => {
            const newValue = parseInt(event.target.value);
            setSliderValue(newValue);
            onChange(newValue);
        }
        return (
            <div>
                <input
                    type="range"
                    min={min}
                    max={max}
                    value={sliderValue}
                    onChange={handleSliderChange}
                />
            </div>
        )
    }
    
    export default function App() {
        const [value, setValue] = useState(0)
        return (
            <div>
                <CustomSlider
                    min={0}
                    max={100}
                    value={value}
                    onChange={val => setValue(val)}
                />
                {value}
            </div>
        );
    }
    
    Login or Signup to reply.
  2. There are two key points:
    The first one, OnChange() has two different meanings here: one is to feedback the change of value, and the other is to feedback the change of onChange() itself. The two should probably be kept separate.
    The second point is that useEffect() is designed to perform behaviors for a parameter that has been given, and its focus is "get a new value and do something", such as querying based on a new url data. useEffect() is actually a design that tries to decouple code based on some specific concerns. If you don’t use useEffect() to process business logic, you should consider the problem of repeated code for "first run". But OnChange() focuses on different things. OnChange() is inherently highly bound to setState(). As you said, OnChange() should only care about data changes, which is different from useEffect(). Therefore, a better design maybe is not to use useEffect(), but to wrap setState(), and call OnChange() in this wrapped function. Use useEffect() if you have something to do every time you get a value you care about. The function hook of "prepare to end the side effect of the last value concerned" that comes with useEffect() can handle the things to be done every time the value changes.

    Login or Signup to reply.
  3. To make your implementation work as you intended you need to do 2 things:

    1. Prevent unnecessary call of onChange callback on the first render, to do so you can check whether Slider is mounted or not, and call onChange only if Slider was already mounted, following changes required to the useEffect inside Slider component:
    const Slider = ({ onChange }) => {
      const [value, setValue] = useState(0)
      // ...
      const isMounted = useRef(false)
      useEffect(() => {
        if (isMounted.current === false) {
          isMounted.current = true
          return
        }
        onChange?.(value)
    
        return () => {
          isMounted.current = false
        }
      }, [value, onChange])
      // ...
    }
    
    1. Make sure onChange callback that you pass to the Slider component is memoized using React’s useCallback hook, it prevents unnecessary calls of useEffect inside Slider component where onChange callback in a dependecy list due to rerender of the outer component.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search