在这个React组件的实例教程中,我们将用React Hooks和Function组件创建一个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>
/
{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>
/
{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} />
/
{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的实现。请在评论中告诉我你是如何改进你的组件的,你是如何喜欢这个教程的。