人人都会AI编程

10.2 渲染属性(Render Props):设计思路与使用场景

更新时间:2026-07-11

什么是 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 依旧是一个强劲的武器。理解它有助于你读懂大量历史代码和开源库的设计。