用React Hooks和Function组件创建一个React Range组件(附代码)

95 阅读4分钟

在这个React组件的实例教程中,我们将用React HooksFunction组件创建一个React Range组件。你可以在这个CodeSandbox或这个GitHub仓库中看到这个实现的最终输出。如果你想一步一步地实现它,只需跟着教程走。

React范围组件

我们从之前的教程开始,在那里我们实现了一个React滑块组件。让我们把它所有的内部结构从Slider/slider重命名为Range/range,以保持我们对事物的命名一致。本教程将把这个组件扩展为Range Slider,它有更多的功能。让我们开始吧。

首先,我们要给范围着色--或者也叫轨道--用于我们的交互式拇指从范围容量的最小值移动到最大值。但我们将只给拇指左边的部分着色。这样,我们就能得到一个视觉反馈,知道哪个范围已经被选中,哪个没有。

...

const StyledRangeProgress = styled.div`
  border-radius: 3px;
  position: absolute;
  height: 100%;
  opacity: 0.5;
  background: #823eb7;
`;

...

const getWidth = percentage => `${percentage}%`;

const Range = ({
  initial,
  max,
  formatFn = number => number.toFixed(0),
  onChange,
}) => {
  const initialPercentage = getPercentage(initial, max);

  const rangeRef = React.useRef();
  const rangeProgressRef = React.useRef();
  const thumbRef = React.useRef();
  const currentRef = React.useRef();

  ...

  const handleMouseMove = event => {
    ...

    const newPercentage = getPercentage(newX, end);
    const newValue = getValue(newPercentage, max);

    thumbRef.current.style.left = getLeft(newPercentage);
    rangeProgressRef.current.style.width = getWidth(newPercentage);
    currentRef.current.textContent = formatFn(newValue);

    onChange(newValue);
  };

  ...

  return (
    <>
      <RangeHeader>
        <strong ref={currentRef}>{formatFn(initial)}</strong>
        &nbsp;/&nbsp;
        {max}
      </RangeHeader>
      <StyledRange ref={rangeRef}>
        <StyledRangeProgress
          style={{ width: getWidth(initialPercentage) }}
          ref={rangeProgressRef}
        />
        <StyledThumb
          style={{ left: getLeft(initialPercentage) }}
          ref={thumbRef}
          onMouseDown={handleMouseDown}
        />
      </StyledRange>
    </>
  );
};

当与Range组件的拇指互动时,你会注意到轨道的进度、拇指的位置和当前值都是正确的--即使min 值不是零。当前显示的值不应该低于定义的min 值。

接下来,我们将为我们的React Range组件做一个重构。到目前为止,当我们的组件第一次渲染时,一切都被初始化一次。我们是用JSX的声明方式来做的--至少React是这样教我们做的。

...

const RangeHeader = styled.div`
  display: flex;
  justify-content: space-between;
`;

...

const Range = ({
  initial,
  min = 0,
  max,
  formatFn = number => number.toFixed(0),
  onChange,
}) => {
  ...

  return (
    <>
      <RangeHeader>
        <div>{formatFn(min)}</div>
        <div>
          <strong ref={currentRef}>{formatFn(initial)}</strong>
          &nbsp;/&nbsp;
          {formatFn(max)}
        </div>
      </RangeHeader>
      ...
    </>
  );
};

const App = () => (
  <div>
    <Range
      initial={10}
      min={5}
      max={25}
      formatFn={number => number.toFixed(2)}
      onChange={value => console.log(value)}
    />
  </div>
);

然而,由于我们已经在使用命令式的方式来更新所有这些值,一旦有人在我们的组件中移动范围,我们也可以使用命令式的方式来做初始渲染的事情。让我们删除初始渲染的JSX,改用React Hook来触发强制更新函数。

首先,让我们把所有需要更新的东西移到它自己的函数中:

const Range = ({ ... }) => {
  ...

  const handleUpdate = (value, percentage) => {
    thumbRef.current.style.left = getLeft(percentage);
    rangeProgressRef.current.style.width = getWidth(percentage);
    currentRef.current.textContent = formatFn(value);
  };

  const handleMouseMove = event => {
    ...

    const newPercentage = getPercentage(newX, start, end);
    const newValue = getValue(newPercentage, min, max);

    handleUpdate(newValue, newPercentage);

    onChange(newValue);
  };

  ...
};

其次,让我们删除声明性的JSX,代之以React useLayoutEffect Hook,它在组件的第一次渲染时运行(以及在每次依赖性变化时),用我们先前提取的更新函数更新所有显示的值。

const Range = ({ ... }) => {
  const initialPercentage = getPercentage(initial, min, max);

  const rangeRef = React.useRef();
  const rangeProgressRef = React.useRef();
  const thumbRef = React.useRef();
  const currentRef = React.useRef();

  const diff = React.useRef();

  const handleUpdate = (value, percentage) => {
    thumbRef.current.style.left = getLeft(percentage);
    rangeProgressRef.current.style.width = getWidth(percentage);
    currentRef.current.textContent = formatFn(value);
  };

  const handleMouseMove = event => { ... };

  const handleMouseUp = () => { ... };

  const handleMouseDown = event => { ... };

  React.useLayoutEffect(() => {
    handleUpdate(initial, initialPercentage);
  }, [initial, initialPercentage, handleUpdate]);

  return (
    <>
      <RangeHeader>
        <div>{formatFn(min)}</div>
        <div>
          <strong ref={currentRef} />
          &nbsp;/&nbsp;
          {formatFn(max)}
        </div>
      </RangeHeader>
      <StyledRange ref={rangeRef}>
        <StyledRangeProgress ref={rangeProgressRef} />
        <StyledThumb ref={thumbRef} onMouseDown={handleMouseDown} />
      </StyledRange>
    </>
  );
};

现在,我们在第一次渲染时运行这个React钩子,并且在其依赖关系之一发生变化时运行这个钩子--因此第二个数组作为参数--以强制性地处理更新,而不是依赖JSX。

最后,我们需要将我们的更新函数包裹到React的useCallback钩子中,否则更新函数会在每次渲染时改变,并一次又一次地运行我们的useLayoutEffect钩子。handleUpdate 函数应该只在它的一个依赖项(这里是formatFn )改变时才被重新定义。

handleUpdate "函数使得useLayoutEffect钩子的依赖关系在每次渲染时都会改变。为了解决这个问题,把'handleUpdate'定义包进它自己的useCallback()Hook。

const Range = ({ ... }) => {
  ...

  const handleUpdate = React.useCallback(
    (value, percentage) => {
      thumbRef.current.style.left = getLeft(percentage);
      rangeProgressRef.current.style.width = getWidth(percentage);
      currentRef.current.textContent = formatFn(value);
    },
    [formatFn]
  );

  ...

  React.useLayoutEffect(() => {
    handleUpdate(initial, initialPercentage);
  }, [initial, initialPercentage, handleUpdate]);

  ...
};

一切都应该恢复正常。然而,请记住,我们建议避免在React中使用命令式的做事方式。所以把这看作是把事情从声明式(JSX)转向命令式(useRef)编程的练习 -- 因为我们需要命令式编程来更新我们的鼠标移动事件的一切,而不使用React的状态管理。在未来,尽量坚持使用React的状态管理和显示值的声明式方法来做事。


React Range组件的灵感来自这个纯JavaScript的实现。请在评论中告诉我你是如何改进你的组件的,你是如何喜欢这个教程的。