什么是 Render Props
Render Props 是一种在 React 中复用组件逻辑的技术。它的核心非常简单:一个组件接受一个函数作为 prop,并用该函数的返回值作为它要渲染的内容。
这个概念的名字来源于一个常见的 prop 名称 render,但实际上任何用于渲染UI的 prop(比如 children 也可以是函数)都属于 Render Props 模式。
<DataProvider render={data => (
<h1>Hello {data.name}</h1>
)} />
在这个例子中,DataProvider 组件负责获取数据和管理状态,但它自己不做 UI 呈现,而是调用 render prop 并将数据传递出去,由使用方决定具体渲染什么。
设计思路:关注点分离,逻辑与视图解耦
Render Props 背后的设计思想是:将“做什么”和“长什么样”彻底分离。
- “做什么”:由提供 render prop 的组件负责,它封装可复用的状态逻辑、数据获取、事件处理等。
- “长什么样”:由调用方的 render 函数决定,它拿到数据后自由渲染 UI。
这种模式让逻辑复用变得极其灵活——同一个数据获取组件,可以渲染出完全不同的界面:
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = useCallback((e) => {
setPosition({ x: e.clientX, y: e.clientY });
}, []);
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
}
// 用法1:显示坐标文字
<MouseTracker render={({ x, y }) => (
<p>鼠标位置:{x}, {y}</p>
)} />
// 用法2:让一个猫猫图片跟随鼠标
<MouseTracker render={({ x, y }) => (
<img src="/cat.png" style={{ position: 'absolute', left: x, top: y }} />
)} />
MouseTracker 只负责追踪鼠标位置——这是它的“做什么”。至于鼠标位置被用来显示文字、移动图片还是画 Canvas,它完全不关心。这种极致的解耦是 Render Props 的最大价值。
与 children 结合的更自然写法
很多时候,我们不会专门定义一个 render prop,而是直接使用 children 作为渲染函数,让组件看起来更“原生”:
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
// ... 一样的逻辑
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{children(position)}
</div>
);
}
// 使用 children 写法,视觉上更干净
<MouseTracker>
{({ x, y }) => <p>鼠标位置:{x}, {y}</p>}
</MouseTracker>
这种写法在 React Router 中很常见,比如 <Route> 的 children 就是函数,可以接收路由参数动态渲染。
典型使用场景
1. 跨组件逻辑复用
当多个组件需要用到相同的状态逻辑或副作用(如鼠标位置、窗口大小、数据获取、表单校验状态等),Render Props 可以将这些逻辑提取到一个可复用的组件中,避免重复代码。
function WindowSize({ children }) {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => setSize({
width: window.innerWidth,
height: window.innerHeight,
});
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <>{children(size)}</>;
}
// 复用:根据不同组件需要,拿到窗口尺寸
<WindowSize>
{({ width, height }) => (
width > 768 ? <DesktopLayout /> : <MobileLayout />
)}
</WindowSize>
2. 数据获取与加载状态封装
可以用 Render Props 封装一套完整的数据请求生命周期(loading、error、data),让调用方完全专注于渲染不同的状态 UI。
function Fetch({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return <>{children({ data, loading, error })}</>;
}
// 使用
<Fetch url="/api/user">
{({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserProfile user={data} />;
}}
</Fetch>
3. 决策反转,提供插槽式扩展
某些组件只提供框架,具体的内容完全由调用方决定。比如一个 Droppable 拖放容器,它只负责处理拖放事件并计算被拖拽物品是否可以放置,但怎么渲染“放置高亮区域”由使用者通过 render prop 决定。
与高阶组件的对比
Render Props 和 HOC(高阶组件)是 React 生态中解决逻辑复用的两大经典模式。它们都能达到类似的效果,但在灵活性上有区别:
| 对比维度 | Render Props | HOC |
|----------|---------------|-----|
| 传参方式 | 动态的,运行时通过函数参数传入 | 静态的,通过 Props 传入(可能冲突) |
| 组合难度 | 灵活嵌套,但容易“回调地狱” | 组合多个 HOC 会产生深度嵌套的组件树 |
| Props 冲突 | 无,数据通过函数参数显式传递 | 可能会覆盖被包裹组件的同名 Props |
| 类型推导(TS) | 函数参数类型可以直接推导 | 多层包裹后类型推导复杂 |
在实践中,Render Props 比 HOC 更灵活、不会产生“Props 命名冲突”问题,所以 React 官方曾一度推荐使用 Render Props 代替某些场景下的 HOC。但现在两者都大面积被自定义 Hooks 取代了。
在现代 React 中的位置:何时还用 Render Props?
自从 Hooks 出现,绝大部分的逻辑复用都可以用自定义 Hooks 更简洁地实现,如上面的 MouseTracker 可以非常直接地写成一个 Hook:
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return position;
}
Hook 不需要额外的组件层级,也没有嵌套问题,使得 Render Props 的许多传统场景失去了必要性。
但 Render Props 并没有完全过时,它在以下情况下仍然有价值:
- 组件需要封装复杂的渲染行为,并且这个渲染行为本身就是一个可复用的 UI 片段,而不仅仅是数据逻辑。例如,一个
VirtualList组件需要调用方决定每一项如何渲染,用 render prop(或 children 函数)就很自然。 - 需要在渲染过程中动态决定渲染哪个子组件,而 Hooks 只能提供数据,不能直接输出 JSX。
- 作为库的 API 设计,给用户最大的渲染自由度(如
react-router的<Route>、formik的<Field>)。
注意事项:性能与闭包
使用 Render Props 时,每次父组件渲染,传给 render prop 的函数都会重新创建,如果该函数被传给 React.memo 包裹的子组件,会导致子组件不必要的重渲染。可以使用 useCallback 包裹渲染函数来避免这种情况:
const renderCat = useCallback(({ x, y }) => (
<img src="/cat.png" style={{ position: 'absolute', left: x, top: y }} />
), []);
不过多数情况下,这种微小的开销不值得过早优化,只在实际发现性能瓶颈时才需要处理。
总结
Render Props 是一种逻辑复用模式,它通过将一个函数作为 prop 传入组件,让组件用它来渲染 UI,实现了逻辑与视图的解耦。在 Hooks 普及之前,它是跨组件共享状态逻辑的主要方式。如今,大部分逻辑复用应优先考虑自定义 Hooks,但在需要渲染极度灵活、或者设计库 API 时,Render Props 依旧是一个强劲的武器。理解它有助于你读懂大量历史代码和开源库的设计。