使用自定义钩子代替“渲染属性”
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
React 中一个既实用又有时难以掌握的特性是跨组件复用状态逻辑。我们都希望只需编写一次状态逻辑,就能在所有需要的组件中复用,而不是每次需要时都重写一遍。实现这一点的常用模式是“渲染属性”(render props)。
带有渲染属性的组件会接收一个返回 React 元素的函数,并调用该函数来代替自己实现渲染逻辑。这个组件可以称为“容器组件”,而我们返回的 React 元素或组件则可以称为“展示组件”。
// example 1
<Container render={prop => (
<Presentation {...props} />
)} />
// example 2
<Container children={prop => (
<Presentation {...props} />
)} />
// example 3
<Container>
{props => (
<Presentation {...props} />
)}
</Container>
以上三个示例实现了渲染属性模式,其中“Container”是我们的容器组件,它渲染一个展示组件……没错,就是这么简单。我们可以把任何需要复用的有状态逻辑放在Container组件中,并将结果以及必要的“更新函数”传递给它渲染的任何其他组件。这就是“渲染属性”的精髓所在。
还有其他选择吗?
如果我们不用容器,而是使用一个自定义 Hook 来实现这个逻辑,并通过一个“更新函数”返回结果,会怎么样呢?我所说的“更新函数”是指一个更新容器状态或 Hook 返回结果的函数。如何实现这一点正是我们今天讨论的重点。让我们使用我在 React 官方文档中找到的关于渲染属性的“猫捉老鼠”示例。我们将查看“渲染属性”示例,并尝试将其重构为使用自定义 Hook。
渲染属性示例
如果我们有一个组件,它监听鼠标移动并设置指针位置的状态,如下所示:
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.render(this.state)}
</div>
);
}
}
任何需要根据鼠标位置渲染元素的组件都可以由我们的鼠标组件来渲染。我们来定义一个猫组件,它会渲染一只猫追逐鼠标指针的图像。
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top:
mouse.y }} />
);
}
}
我们不需要重写获取指针位置的逻辑,而是可以从 Mouse 组件扩展该逻辑,如下所示:
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
这将渲染 Cat 组件,并将鼠标位置作为 prop 传递。我们可以根据需要,在任意数量的组件中复用此逻辑。
钩子替代方案
我们将移除“鼠标”组件,改为创建一个钩子来实现鼠标逻辑。
export function useMouse(initialValue = {x:0, y:0}) {
const [position, setPosition] = useState(initialValue);
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
}
return [position, handleMouseMove];
}
我们刚刚定义了一个名为 useMouse 的钩子函数。按照惯例,函数名应该以“use”开头,以便人们知道这是一个钩子函数。useMouse 钩子函数会返回鼠标的位置以及一个用于更新该位置的函数。接下来,我们来看看如何在 Cat 组件中使用它。
function Cat() {
const [position, setMousePosition] = useMouse();
return (
<div style={{ height: '100%' }} onMouseMove={setMousePosition}>
<img src="/cat.jpg" style={{ position: 'absolute', left: position.x, top:
position.y }} />
);
</div>
}
你会想到哪个词?简洁?清晰?或许三个词都适用。任何需要获取鼠标位置的组件都可以使用这个 Hook。
这种模式可以提高复杂 React 代码的可读性和可维护性,并且有助于避免构建庞大且嵌套过深的组件树。我们可以通过创建自定义 Hook 来复用身份验证状态、用户信息,甚至是表单处理逻辑。它们还可以替代 React 中的高阶组件 (HOC)。
