在 React.js 中创建范围滑块

Posted

技术标签:

【中文标题】在 React.js 中创建范围滑块【英文标题】:Creat Range Slider in React.js 【发布时间】:2020-10-24 18:31:45 【问题描述】:

我正在尝试在 react.js 中创建一个范围滑块

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;

然后我在其他组件中使用它

 <RangeSlider onChange=(value) => sliderValueChanged(value) />
如果我想传入自定义标签,我将如何更新状态 这样做? 我必须为此使用 React.memo 吗?我的理解是,每次滑块值更改时,都会创建一个新的滑块实例。 我希望它最终变得强大(步骤、多手柄、工具提示等),任何帮助 不胜感激。

【问题讨论】:

【参考方案1】:

当您想创建可重用组件时,请始终尝试从使用它的位置传递配置,并将所有常用配置保留在组件内部

前: 了解 useMemouseReducer 的工作原理

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>
  );
;

在 Slider 组件中

//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);

查看我的demo

我猜这个答案叫做 3 个问题

    在parent中使用配置传递label等不常见的配置

    使用备忘录?是的,所以 Slider 组件只有在 props 改变时才会被渲染。但是你必须仔细设计它(例如:useMemo 和 useCallback)

    steps ?使用父级中的配置对象来传递这些。


以防万一您需要一种很好的方式来包装范围,我建议您使用自定义挂钩

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];
;

这是demo

这可能会进一步改进

【讨论】:

很好的解释! 很好,这很有帮助。那么,如果我要在一个页面上使用多个范围滑块,我是否需要为每个实例使用 useState、useCallback 和 useMemo?例如,我的父级中有 5 个范围滑块,每个滑块都有自己的 useState、useCallback 和 useMemo。 很高兴它对您有所帮助! @Skeeter62889 是的,因为你必须为不同的滑块设置不同的配置不是吗?所以最好有单独的逻辑,否则代码长大后很难维护 @Skeeter62889 我刚刚用一个不错的方法更新了答案,检查一下,快乐编码..【参考方案2】:

您必须问自己的第一个问题是:我在哪里保存滑块的状态?答:将状态保留在父组件中并将其传递给 RangeSlider 以保持状态受控和一致。在大多数情况下,像这样的实用程序组件永远不应该保持自己的状态。

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 />
   );

还有你的 Range Slider 组件:

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);

关于调用新实例的担忧。当您的通行证道具发生变化时,它不会导致该组件的新实例。它只会导致重新渲染。在像 RangeSlider 这样的小组件中,重新渲染它只需要很少的计算能力,因此没有必要绕过传递的道具,只需从你的父母那里持续传递道具。

通常范围滑块会直接影响您的 UI 或保持表单的状态,因此仅在“mouseup”上触发 onChange 将限制您的组件的可重用性,并且仅涵盖极少数情况。 如果您喜欢 @Kalhan.Toress 解释的行为,我建议您在父组件中处理该逻辑。要启用它,您只需通过回调传递“mouseup”事件,如前所示。

在这种情况下,您真的不需要担心性能。你的 RangeSlider 太小太简单了,会弄乱你的应用程序。

希望对你有帮助

【讨论】:

【参考方案3】:
    您可以使用label 解构您的道具,并使用useEffect 更改您当前的滑块
const RangeSlider = ( onChange, label ) => 
  // other stuff
  useEffect(() => 
    setSlider(current => (
      ...current,
      label
    ));
  , [label]);
;

RangerSlider.js

 <RangeSlider label="Custom label" onChange=(value) => sliderValueChanged(value) />
    不一定,这取决于您如何处理依赖项数组以确保它仅在必要时重新呈现。

编辑: 我还注意到,您实际上并没有更改 RangeSlider 的本地状态以订阅更改。您可能还想在您的 RangerSlider 中添加另一个 useEffect

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 />

如果我要实现这个组件,我会避免在RangerSlider 中创建本地状态,并将所有值和滑块配置作为props 传递以提高性能

【讨论】:

以上是关于在 React.js 中创建范围滑块的主要内容,如果未能解决你的问题,请参考以下文章

如何在 jQuery UI 中创建多范围时间滑块

通用 Windows (UWP) 范围滑块

Flutter - 范围滑块

如何在 react.js 中创建动态网格?

如何使用 React js JSX 在下拉列表中创建年份列表

如何在 React js 中创建状态数组