question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Slider props value changes not rendering correctly

See original GitHub issue

Hello,

I’m having problems with Slider every time props value changes. I have already update react-native-elements to last version (3.4.2) to resolve this bug #2995 but I’m still having problems. Now every time props value changes, the position of the thumb changes correctly but it seems props value is not changing. Here it is a video that explains it better.

https://user-images.githubusercontent.com/51318357/122426276-657f1680-cf90-11eb-85ab-5d3d4a5a5933.mp4

To control Slider value, there is a parent component who stores in state the value and listens to every element that change this. Here it is the parent code:

import { View, StyleSheet } from 'react-native';
import { Button, Slider } from 'react-native-elements';

const LightDimmerDetailScreen = () => {
  const [currentValue, setCurrentValue] = useState(0);

  const handleOnSliderChange = (newValue: number) => {
    setCurrentValue(newValue);
  };

  return (
    <View style={styles.screen}>
      <Slider
        value={currentValue}
        minimumValue={0}
        maximumValue={100}
        step={1}
        minimumTrackTintColor={'red'}
        onSlidingStart={(newSliderValue) => console.log('on sliding start: ', newSliderValue)}
        onSlidingComplete={(newSliderValue) => handleOnSliderChange(newSliderValue)}
      />
      <Button onPress={() => setCurrentValue(50)} title={'set to 50%'} />
    </View>
  );
};

export default LightDimmerDetailScreen;

const styles = StyleSheet.create({
  screen: {
    marginTop: 100,
    width: 300,
    marginHorizontal: 57,
  },
});

I’m using:

  • react-native-elements: 3.4.2
  • react-native: 0.64.1

Thanks in advanced for your help!

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:3
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
menssencommented, Jul 20, 2021

Also, I don’t know if this helps at all, but here’s the diff from our forked version which fixes this (I might try to apply it directly and PR, but unfortunately we de-typescript-ified the whole thing so it’s nearly impossible, but maybe it’s a starting place).

Basically:

  1. Change the useRef() around the PanResponder to a useMemo(), and declare all the handlers as dependencies.
  2. Remove the fireChangeEvent function and inline literal function calls to the four callbacks that were previously referenced as string names, so they can be implicitly declared as useCallback dependencies.
  3. Work back through all those dependencies and wrap them in useCallback, declaring their correct dependencies. This required re-ordering most of the file and destructuring a lot more props. The eslint rule react-hooks/exhaustive-deps helps a ton here – basically just do what it says.
  4. Change the AnimatedValue that was stored in the “value” variable with a useState to being stored in a useRef. Without doing this, the state update triggers all the callbacks that are now wrapped in useCallback to be reconstructed. (I also changed the name to animatedValue to avoid confusion/conflicting with the “value” prop, which is now destructured.)
  5. Remove some of the refs that were declared at the top of the component (trackSizeValue et al.). These seem to have been wrapped as refs to solve the exact problem that we are trying to solve more holistically, and are now unnecessary.

Doing all this, the PanResponder won’t be recreated when you move the slider around, but will be recreated if its props change externally. Theoretically, this might cause issues if one of the callbacks causes a Slider prop (other than the value) to change (like maybe a style?), which seems like a less common case than just wanting the thing to update correctly.

diff --git a/native-components/Slider.js b/native-components/Slider.js
index 412153c..5ada680 100644
--- a/native-components/Slider.js
+++ b/native-components/Slider.js
@@ -58,38 +58,47 @@ class Rect {
 }
 /* eslint-enable functional/no-class, functional/no-this-expression */
 
-const Slider = (props) => {
+const Slider = ({
+  onSlidingStart,
+  onValueChange,
+  onSlidingComplete,
+  disabled,
+  minimumValue,
+  maximumValue,
+  step,
+  allowTouchTrack,
+  thumbTouchSize,
+  trackStyle,
+  minimumTrackTintColor,
+  maximumTrackTintColor,
+  thumbTintColor,
+  containerStyle,
+  style,
+  thumbStyle,
+  thumbProps,
+  debugTouchArea,
+  orientation,
+  value,
+  animateTransitions,
+  animationType,
+  animationConfig,
+  ...other
+}) => {
   const _previousLeft = useRef(0)
   const [allMeasured, setAllMeasured] = useState(false)
   const [containerSize, setContainerSize] = useState({ width: 0, height: 0 })
   const [trackSize, setTrackSize] = useState({ width: 0, height: 0 })
   const [thumbSize, setThumbSize] = useState({ width: 0, height: 0 })
-  const containerSizeValue = useRef(containerSize)
-  const trackSizeValue = useRef(trackSize)
-  const thumbSizeValue = useRef(thumbSize)
-  const isVertical = useRef(props.orientation === 'vertical')
-  const [value] = useState(
+  const isVertical = useMemo(() => orientation === 'vertical', [orientation])
+  const animatedValue = useRef(
     new Animated.Value(
       getBoundedValue(
-        props.value || 0,
-        props.maximumValue || 1,
-        props.minimumValue || 0,
+        value || 0,
+        maximumValue || 1,
+        minimumValue || 0,
       ),
     ),
-  )
-
-  useEffect(() => {
-    // eslint-disable-next-line functional/immutable-data
-    containerSizeValue.current = containerSize
-  }, [containerSize])
-  useEffect(() => {
-    // eslint-disable-next-line functional/immutable-data
-    trackSizeValue.current = trackSize
-  }, [trackSize])
-  useEffect(() => {
-    // eslint-disable-next-line functional/immutable-data
-    thumbSizeValue.current = thumbSize
-  }, [thumbSize])
+  ).current
 
   const measureContainer = (x) => {
     handleMeasure('containerSize', x)
@@ -101,10 +110,10 @@ const Slider = (props) => {
     handleMeasure('thumbSize', x)
   }
 
-  const handleMeasure = (name, x) => {
+  const handleMeasure = useCallback((name, x) => {
     const { width: layoutWidth, height: layoutHeight } = x.nativeEvent.layout
-    const width = isVertical.current ? layoutHeight : layoutWidth
-    const height = isVertical.current ? layoutWidth : layoutHeight
+    const width = isVertical ? layoutHeight : layoutWidth
+    const height = isVertical ? layoutWidth : layoutHeight
     const size = { width, height }
     if (name === 'containerSize') {
       setContainerSize(size)
@@ -118,35 +127,35 @@ const Slider = (props) => {
     if (thumbSize && trackSize && containerSize) {
       setAllMeasured(true)
     }
-  }
+  }, [isVertical, containerSize, thumbSize, trackSize])
 
-  const currentPropValue = useRef(props.value || 0)
+  const currentPropValue = useRef(value || 0)
   const prevPropValue = useRef(0)
 
   const didMountRef = useRef(false)
 
   const setCurrentValue = React.useCallback(
     (value1) => {
-      value.setValue(value1)
+      animatedValue.setValue(value1)
     },
-    [value],
+    [animatedValue],
   )
 
   React.useEffect(() => {
-    setCurrentValue(props.value || 0)
-  }, [props.value, setCurrentValue])
+    setCurrentValue(value || 0)
+  }, [value, setCurrentValue])
 
   useEffect(() => {
     // eslint-disable-next-line functional/immutable-data
-    prevPropValue.current = props.value || 0
+    prevPropValue.current = value || 0
     if (didMountRef.current) {
       const newValue = getBoundedValue(
-        props.value || 0,
-        props.maximumValue || 1,
-        props.minimumValue || 0,
+        value || 0,
+        maximumValue || 1,
+        minimumValue || 0,
       )
       if (prevPropValue.current !== newValue) {
-        if (props.animateTransitions) {
+        if (animateTransitions) {
           setCurrentValueAnimated(new Animated.Value(newValue))
         } else {
           setCurrentValue(newValue)
@@ -159,131 +168,184 @@ const Slider = (props) => {
   })
 
   const setCurrentValueAnimated = (value1) => {
-    const { animationType } = props
-    const animationConfig = Object.assign(
+    const combinedAnimationConfig = Object.assign(
       {},
       DEFAULT_ANIMATION_CONFIGS[animationType || 'timing'],
-      props.animationConfig,
+      animationConfig,
       {
         toValue: value1,
       },
     )
-    Animated[animationType || 'timing'](value, animationConfig).start()
+    Animated[animationType || 'timing'](animatedValue, combinedAnimationConfig).start()
   }
 
-  const handleMoveShouldSetPanResponder = () => {
+  const getRatio = useCallback((value1) => {
+    return (
+      (value1 - (minimumValue || 0)) /
+      ((maximumValue || 1) - (minimumValue || 0))
+    )
+  }, [minimumValue, maximumValue])
+
+  const getThumbLeft = useCallback((value1) => {
+    const ratio = getRatio(value1)
+    return (
+      ratio * (containerSize.width - thumbSize.width)
+    )
+  }, [getRatio, containerSize.width, thumbSize.width])
+
+  const getValueForTouch = useCallback((location) => {
+    const length =
+      containerSize.width - thumbSize.width
+    const ratio = location / length
+    // eslint-disable-next-line functional/no-let
+    let newValue =
+      ratio * ((maximumValue || 1) - (minimumValue || 0))
+    if (step) {
+      newValue = Math.round(newValue / step) * step
+    }
+
+    return getBoundedValue(
+      newValue + (minimumValue || 0),
+      maximumValue || 1,
+      minimumValue || 0,
+    )
+  }, [minimumValue, maximumValue, step, containerSize.width, thumbSize.width])
+
+  const getValue = useCallback((gestureState) => {
+    const location =
+      _previousLeft.current +
+      (isVertical ? gestureState.dy : gestureState.dx)
+    return getValueForTouch(location)
+  }, [getValueForTouch, isVertical])
+
+  const handleMoveShouldSetPanResponder = useCallback(() => {
     // Should we become active when the user moves a touch over the thumb?
     if (!TRACK_STYLE) {
       return true
     }
     return false
-  }
+  }, [])
 
-  const handlePanResponderGrant = () => {
+  const handlePanResponderGrant = useCallback(() => {
     // eslint-disable-next-line functional/immutable-data
     _previousLeft.current = getThumbLeft(currentPropValue.current)
-    fireChangeEvent('onSlidingStart')
-  }
 
-  const handlePanResponderMove = (
+    if (onSlidingStart) {
+      onSlidingStart(currentPropValue.current)
+    }
+  }, [getThumbLeft, onSlidingStart])
+
+  const handlePanResponderMove = useCallback((
     _,
     gestureState,
   ) => {
-    if (props.disabled) {
+    if (disabled) {
       return
     }
     setCurrentValue(getValue(gestureState))
-    fireChangeEvent('onValueChange')
-  }
+    if (onValueChange) {
+      onValueChange(currentPropValue.current)
+    }
+  }, [getValue, onValueChange, disabled, setCurrentValue])
 
-  const handlePanResponderRequestEnd = () => {
+  const handlePanResponderRequestEnd = useCallback(() => {
     // Should we allow another component to take over this pan?
     return false
-  }
+  }, [])
 
-  const handlePanResponderEnd = (
+  const handlePanResponderEnd = useCallback((
     _,
     gestureState,
   ) => {
-    if (props.disabled) {
+    if (disabled) {
       return
     }
     setCurrentValue(getValue(gestureState))
-    fireChangeEvent('onSlidingComplete')
-  }
-
-  const thumbHitTest = ({ nativeEvent }) => {
-    const thumbTouchRect = getThumbTouchRect()
-    return thumbTouchRect.containsPoint(
-      nativeEvent.locationX,
-      nativeEvent.locationY,
-    )
-  }
-  const handleStartShouldSetPanResponder = (
-    e, /* gestureState: Object */
-  ) => {
-    // Should we become active when the user presses down on the thumb?
-
-    if (!props.allowTouchTrack && !TRACK_STYLE) {
-      return thumbHitTest(e)
-    }
-    if (!trackStyle) {
-      setCurrentValue(getOnTouchValue(e))
-    }
-    fireChangeEvent('onValueChange')
-    return true
-  }
-
-  const fireChangeEvent = (event) => {
-    if (props?.[event]) {
-      props?.[event]?.(currentPropValue.current)
+    if (onSlidingComplete) {
+      onSlidingComplete(currentPropValue.current)
     }
-  }
+  }, [onSlidingComplete, getValue, disabled, setCurrentValue])
 
-  // get value of where some touched on slider.
-  const getOnTouchValue = ({ nativeEvent }) => {
-    const location = isVertical.current
-      ? nativeEvent.locationY
-      : nativeEvent.locationX
-    return getValueForTouch(location)
-  }
-
-  const getValueForTouch = (location) => {
-    const length =
-      containerSizeValue.current.width - thumbSizeValue.current.width
-    const ratio = location / length
-    // eslint-disable-next-line functional/no-let
-    let newValue =
-      ratio * ((props.maximumValue || 1) - (props.minimumValue || 0))
-    if (props.step) {
-      newValue = Math.round(newValue / props.step) * props.step
-    }
-
-    return getBoundedValue(
-      newValue + (props.minimumValue || 0),
-      props.maximumValue || 1,
-      props.minimumValue || 0,
-    )
-  }
-
-  const getTouchOverflowSize = () => {
-    const { thumbTouchSize } = props
+  const getTouchOverflowSize = useCallback(() => {
     const size = {}
     if (allMeasured === true) {
       // eslint-disable-next-line functional/immutable-data
       size.width = Math.max(
         0,
-        (thumbTouchSize?.width || THUMB_SIZE) - thumbSizeValue.current.width,
+        (thumbTouchSize?.width || THUMB_SIZE) - thumbSize.width,
       )
       // eslint-disable-next-line functional/immutable-data
       size.height = Math.max(
         0,
         (thumbTouchSize?.height || THUMB_SIZE) -
-        containerSizeValue.current.height,
+        containerSize.height,
       )
     }
     return size
-  }
+  }, [allMeasured, thumbTouchSize, thumbSize.width, containerSize.height])
+
+  const getThumbTouchRect = useCallback(() => {
+    const touchOverflowSize = getTouchOverflowSize()
+    const height =
+      touchOverflowSize?.height / 2 +
+      (containerSize.height -
+        (thumbTouchSize?.height || THUMB_SIZE)) /
+      2
+    const width =
+      touchOverflowSize.width / 2 +
+      getThumbLeft(currentPropValue.current) +
+      (thumbSize.width - (thumbTouchSize?.width || THUMB_SIZE)) /
+      2
+    if (isVertical) {
+      return new Rect(
+        height,
+        width,
+        thumbTouchSize?.width || THUMB_SIZE,
+        thumbTouchSize?.height || THUMB_SIZE,
+      )
+    }
+    return new Rect(
+      width,
+      height,
+      thumbTouchSize?.width || THUMB_SIZE,
+      thumbTouchSize?.height || THUMB_SIZE,
+    )
+  }, [getThumbLeft, getTouchOverflowSize, thumbTouchSize?.height, thumbTouchSize?.width, thumbSize.width, containerSize.height, isVertical])
+
+  const thumbHitTest = useCallback(({ nativeEvent }) => {
+    const thumbTouchRect = getThumbTouchRect()
+    return thumbTouchRect.containsPoint(
+      nativeEvent.locationX,
+      nativeEvent.locationY,
+    )
+  }, [getThumbTouchRect])
+
+  // get value of where some touched on slider.
+  const getOnTouchValue = useCallback(({ nativeEvent }) => {
+    const location = isVertical
+      ? nativeEvent.locationY
+      : nativeEvent.locationX
+    return getValueForTouch(location)
+  }, [getValueForTouch, isVertical])
+
+  const hasTrackStyle = !!trackStyle
+
+  const handleStartShouldSetPanResponder = useCallback((
+    e, /* gestureState: Object */
+  ) => {
+    // Should we become active when the user presses down on the thumb?
+
+    if (!allowTouchTrack && !TRACK_STYLE) {
+      return thumbHitTest(e)
+    }
+    if (!hasTrackStyle) {
+      setCurrentValue(getOnTouchValue(e))
+    }
+    if (onValueChange) {
+      onValueChange(currentPropValue.current)
+    }
+    return true
+  }, [allowTouchTrack, getOnTouchValue, onValueChange, setCurrentValue, thumbHitTest, hasTrackStyle])
 
   const getTouchOverflowStyle = () => {
     const { width, height } = getTouchOverflowSize()
@@ -300,7 +362,7 @@ const Slider = (props) => {
       // eslint-disable-next-line functional/immutable-data
       touchOverflowStyle.marginRight = horizontalMargin
     }
-    if (props.debugTouchArea === true) {
+    if (debugTouchArea === true) {
       // eslint-disable-next-line functional/immutable-data
       touchOverflowStyle.backgroundColor = 'orange'
       // eslint-disable-next-line functional/immutable-data
@@ -309,66 +371,16 @@ const Slider = (props) => {
     return touchOverflowStyle
   }
 
-  const getValue = (gestureState) => {
-    const location =
-      _previousLeft.current +
-      (isVertical.current ? gestureState.dy : gestureState.dx)
-    return getValueForTouch(location)
-  }
-
   React.useEffect(() => {
-    const listenerID = value.addListener((obj) => {
+    const listenerID = animatedValue.addListener((obj) => {
       // eslint-disable-next-line functional/immutable-data
       currentPropValue.current = obj.value
     })
     return () => {
-      value.removeListener(listenerID)
+      animatedValue.removeListener(listenerID)
     }
   })
 
-  const getRatio = (value1) => {
-    return (
-      (value1 - (props.minimumValue || 0)) /
-      ((props.maximumValue || 1) - (props.minimumValue || 0))
-    )
-  }
-
-  const getThumbLeft = (value1) => {
-    const ratio = getRatio(value1)
-    return (
-      ratio * (containerSizeValue.current.width - thumbSizeValue.current.width)
-    )
-  }
-
-  const getThumbTouchRect = () => {
-    const { thumbTouchSize } = props
-    const touchOverflowSize = getTouchOverflowSize()
-    const height =
-      touchOverflowSize?.height / 2 +
-      (containerSizeValue.current.height -
-        (thumbTouchSize?.height || THUMB_SIZE)) /
-      2
-    const width =
-      touchOverflowSize.width / 2 +
-      getThumbLeft(currentPropValue.current) +
-      (thumbSizeValue.current.width - (thumbTouchSize?.width || THUMB_SIZE)) /
-      2
-    if (isVertical.current) {
-      return new Rect(
-        height,
-        width,
-        thumbTouchSize?.width || THUMB_SIZE,
-        thumbTouchSize?.height || THUMB_SIZE,
-      )
-    }
-    return new Rect(
-      width,
-      height,
-      thumbTouchSize?.width || THUMB_SIZE,
-      thumbTouchSize?.height || THUMB_SIZE,
-    )
-  }
-
   const renderDebugThumbTouchRect = (
     thumbLeft,
   ) => {
@@ -387,11 +399,11 @@ const Slider = (props) => {
     const minimumTrackStyle = {
       position: 'absolute',
     }
-    if (isVertical.current) {
+    if (isVertical) {
       // eslint-disable-next-line functional/immutable-data
       minimumTrackStyle.height = Animated.add(
         thumbStart,
-        thumbSizeValue.current.height / 2,
+        thumbSize.height / 2,
       )
       // eslint-disable-next-line functional/immutable-data
       minimumTrackStyle.marginLeft = trackSize.width * TRACK_STYLE
@@ -399,42 +411,34 @@ const Slider = (props) => {
       // eslint-disable-next-line functional/immutable-data
       minimumTrackStyle.width = Animated.add(
         thumbStart,
-        thumbSizeValue.current.width / 2,
+        thumbSize.width / 2,
       )
       // eslint-disable-next-line functional/immutable-data
       minimumTrackStyle.marginTop = trackSize.height * TRACK_STYLE
     }
     return minimumTrackStyle
   }
-  const panResponder = useRef(
-    PanResponder.create({
-      onStartShouldSetPanResponder: handleStartShouldSetPanResponder,
-      onMoveShouldSetPanResponder: handleMoveShouldSetPanResponder,
-      onPanResponderGrant: handlePanResponderGrant,
-      onPanResponderMove: handlePanResponderMove,
-      onPanResponderRelease: handlePanResponderEnd,
-      onPanResponderTerminationRequest: handlePanResponderRequestEnd,
-      onPanResponderTerminate: handlePanResponderEnd,
-    }),
-  ).current
 
-  const {
-    minimumValue,
-    maximumValue,
-    minimumTrackTintColor,
-    maximumTrackTintColor,
-    thumbTintColor,
-    containerStyle,
-    style,
-    trackStyle,
-    thumbStyle,
-    thumbProps,
-    debugTouchArea,
-    ...other
-  } = props
+  const panResponder = useMemo(() => PanResponder.create({
+    onStartShouldSetPanResponder: handleStartShouldSetPanResponder,
+    onMoveShouldSetPanResponder: handleMoveShouldSetPanResponder,
+    onPanResponderGrant: handlePanResponderGrant,
+    onPanResponderMove: handlePanResponderMove,
+    onPanResponderRelease: handlePanResponderEnd,
+    onPanResponderTerminationRequest: handlePanResponderRequestEnd,
+    onPanResponderTerminate: handlePanResponderEnd,
+  }), [
+    handleStartShouldSetPanResponder,
+    handleMoveShouldSetPanResponder,
+    handlePanResponderGrant,
+    handlePanResponderMove,
+    handlePanResponderEnd,
+    handlePanResponderRequestEnd,
+  ])
+
   const mainStyles = containerStyle || styles
   const appliedTrackStyle = StyleSheet.flatten([styles.track, trackStyle])
-  const thumbStart = value.interpolate({
+  const thumbStart = animatedValue.interpolate({
     inputRange: [minimumValue || 0, maximumValue || 1],
     outputRange: [0, containerSize.width - thumbSize.width],
   })
@@ -456,7 +460,7 @@ const Slider = (props) => {
     <View
       {...other}
       style={StyleSheet.flatten([
-        isVertical.current
+        isVertical
           ? mainStyles.containerVertical
           : mainStyles.containerHorizontal,
         style,
@@ -466,7 +470,7 @@ const Slider = (props) => {
       <View
         style={StyleSheet.flatten([
           mainStyles.track,
-          isVertical.current
+          isVertical
             ? mainStyles.trackVertical
             : mainStyles.trackHorizontal,
           appliedTrackStyle,
@@ -478,7 +482,7 @@ const Slider = (props) => {
       <Animated.View
         style={StyleSheet.flatten([
           mainStyles.track,
-          isVertical.current
+          isVertical
             ? mainStyles.trackVertical
             : mainStyles.trackHorizontal,
           appliedTrackStyle,
@@ -491,7 +495,7 @@ const Slider = (props) => {
         style={thumbStyle}
         color={thumbTintColor}
         start={thumbStart}
-        vertical={isVertical.current}
+        vertical={isVertical}
         {...thumbProps}
       />
       <View
1reaction
arpitBhallacommented, Jul 2, 2021

Go for it

Read more comments on GitHub >

github_iconTop Results From Across the Web

React slider not rendering correctly - Stack Overflow
It seems like you forgot to import rc-slider CSS. import 'rc-slider/assets/index.css'. I tried to reproduce your issue here and removing ...
Read more >
Docs • Svelte
Reactive statements run after other script code and before the component markup is rendered, whenever the values that they depend on have changed....
Read more >
Uncontrolled Components - React
In the React rendering lifecycle, the value attribute on form elements will override the value in the DOM. With an uncontrolled component, you...
Read more >
React slider tutorial using react-slider - LogRocket Blog
Learn how to create different sliders using react-slider, a React headless component that's easy to build and customize.
Read more >
@react-native-community/slider - npm
Properties ; onSlidingComplete, Callback that is called when the user releases the slider, regardless if the value has changed. The current value ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found