关于 render-props-pattern
介绍 React render props 模式的学习技能,讲解如何在渲染需求不同的组件间共享状态逻辑与数据,并说明其实现方式、利弊及适用时机。
主要功能
- 01在组件间共享有状态逻辑
- 02通过 render 或 children 传递数据
- 03避免 HOC 命名冲突
- 04比较 render props 与 Hooks
适用场景
- 01让多个显示组件共享输入状态
- 02复用渲染方式不同组件的逻辑
- 03判断使用 render props 还是 Hooks
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill render-props-pattern安装后在 Claude Code 中输入/render-props-pattern确认已加载。
详细介绍
Render Props 模式
目录
让组件高度可复用的另一种方式是使用 render prop 模式。render prop 是组件上的一个 prop,其值是一个返回 JSX 元素的函数。组件本身除了 render prop 外不会渲染任何内容,而是直接调用 render prop,不实现自己的渲染逻辑。
假设我们有一个 Title 组件。在这种情况下,Title 组件只需渲染我们传入的值。我们可以使用 render prop 来实现这一点!将希望 Title 组件渲染的值传给 render prop。
何时使用
- 当需要在具有不同渲染需求的组件之间共享有状态逻辑时,使用此模式
- 当 HOC 模式导致命名冲突或嵌套层级过深时,此模式会很有帮助
何时不使用
- 当自定义 Hook 可以替代此模式时——Hook 无需通过 render prop 嵌套,就能实现相同的逻辑复用
- 当此模式会产生难以阅读和维护的深层嵌套 JSX 时
- 当共享逻辑足够简单,可以用普通工具函数或 Hook 实现时
使用说明
- 传入一个函数作为
renderprop(或childrenprop),该函数接收数据并返回 JSX - 在大多数现代 React 代码中,优先使用自定义 Hook,而不是 render prop
- 使用函数作为 children 的模式,作为显式
renderprop 的更简洁替代方案 - 避免深层嵌套多个 render prop 组件——改用 Hook 重构
详情
<Title render={() => <h1>I am a render prop!</h1>} />
在 Title 组件中,我们可以通过返回调用后的 render prop 来渲染这些数据!
const Title = (props) => props.render();
虽然它们被称为 render prop,但 render prop 不一定要命名为 render。任何用于渲染 JSX 的 prop 都可以视为 render prop!
接受 render prop 的组件通常不只是简单地调用 render prop。我们通常希望将接受 render prop 的组件中的数据,传递给作为 render prop 传入的元素!
function Component(props) {
const data = { ... }
return props.render(data)
}
现在,render prop 可以接收我们作为参数传入的这个值。
<Component render={data => <ChildComponent data={data} />}
来看一个示例!我们有一个简单的应用,用户可以输入摄氏温度。应用会显示该温度对应的华氏温度和开尔文温度。
目前存在一个问题。有状态的 Input 组件保存了用户输入的值,这意味着 Fahrenheit 和 Kelvin 组件无法访问用户输入!
状态提升
让 Fahrenheit 和 Kelvin 组件都能访问用户输入的一种方法是提升状态。
在这种情况下,我们有一个有状态的 Input 组件。不过,兄弟组件 Fahrenheit 和 Kelvin 也需要访问这些数据。我们可以不使用有状态的 Input 组件,而是将状态提升到与 Input、Fahrenheit 和 Kelvin 都有关联的最近公共祖先组件:此处即 App 组件!
function Input({ value, handleChange }) {
return <input value={value} onChange={(e) => handleChange(e.target.value)} />;
}
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input value={value} handleChange={setValue} />
<Kelvin value={value} />
<Fahrenheit value={value} />
</div>
);
}
虽然这是一个有效的解决方案,但在大型应用中,如果组件需要处理许多子组件,状态提升可能会很棘手。每次状态变更都可能导致所有子组件重新渲染,包括那些不处理该数据的子组件,从而可能对应用性能产生负面影响。
Render props
我们可以改用 render props!调整 Input 组件,使其能够接收 render props。
function Input(props) {
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{props.render(value)}
</>
);
}
export default function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input
render={(value) => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
/>
</div>
);
}
太好了,现在 Kelvin 和 Fahrenheit 组件都可以访问用户输入的值了!
将函数作为子元素
除了常规的 JSX 组件,我们还可以将函数作为子元素传递给 React 组件。我们可以通过 children 属性使用这个函数;严格来说,它也是一种渲染属性。
我们来修改 Input 组件。不再显式传递 render 属性,而是直接将函数作为 Input 组件的子元素传递。
export default function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input>
{(value) => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
</Input>
</div>
);
}
我们可以通过 Input 组件上提供的 props.children 属性访问这个函数。不再使用用户输入的值调用 props.render,而是使用该值调用 props.children。
function Input(props) {
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{props.children(value)}
</>
);
}
这样一来,Kelvin 和 Fahrenheit 组件就能访问这个值,而不必关心 render 属性的名称。
Hooks
在某些情况下,我们可以用 Hooks 替代渲染属性。一个很好的例子是 Apollo Client。
使用 Apollo Client 的一种方式是通过 Mutation 和 Query 组件。为了将数据从 Mutation 组件传递给需要这些数据的元素,我们会将一个函数作为子元素传递。该函数通过参数接收数据值。
<Mutation mutation={...} variables={...}>
{addMessage => <div className="input-row">...</div>}
</Mutation>
虽然我们仍然可以使用渲染属性模式,而且它通常比高阶组件模式更受青睐,但它也有缺点。
其中一个缺点是组件嵌套过深。如果某个组件需要访问多个 mutation 或 query,我们可以嵌套多个 Mutation 或 Query 组件。
<Mutation mutation={FIRST_MUTATION}>
{(firstMutation) => (
<Mutation mutation={SECOND_MUTATION}>
{(secondMutation) => (
<Mutation mutation={THIRD_MUTATION}>
{(thirdMutation) => (
<Element
firstMutation={firstMutation}
secondMutation={secondMutation}
thirdMutation={thirdMutation}
/>
)}
</Mutation>
)}
</Mutation>
)}
</Mutation>
Hooks 发布后,Apollo 为 Apollo Client 库添加了对 Hooks 的支持。开发者现在可以通过库提供的 hooks 直接访问数据,而不必使用 Mutation 和 Query 渲染属性。
通过使用 useQuery hook,我们减少了向组件提供数据所需的代码量。
优点
使用渲染属性模式,可以轻松地在多个组件之间共享逻辑和数据。通过使用渲染属性或 children 属性,可以让组件具有很强的复用性。虽然高阶组件模式主要解决相同的问题,即复用性和数据共享,但渲染属性模式还能解决使用 HOC 模式时可能遇到的一些问题。
使用渲染属性模式时,不再会遇到 HOC 模式中的命名冲突问题,因为我们不会自动合并属性。我们会显式地将属性传递给子组件,并使用父组件提供的值。
由于我们会显式传递属性,因此解决了 HOC 的隐式属性问题。应该传递给元素的属性都会显示在渲染属性的参数列表中。这样,我们就能确切知道特定属性的来源。
通过渲染属性,我们可以将应用逻辑与组件渲染分离。接收渲染属性的有状态组件可以将数据传递给无状态组件,由后者单纯负责渲染数据。
缺点
我们尝试通过渲染属性解决的问题,大多已被 React Hooks 取代。Hooks 改变了我们为组件添加复用性和数据共享的方式,因此在许多情况下都可以替代渲染属性模式。
由于我们不能在 render 属性中添加生命周期方法,因此它只能用于不需要修改所接收数据的组件。