Render Props #

一、Render Props概述 #

1.1 什么是Render Props #

Render Props 是一种在 React 组件之间共享代码的技术,使用一个值为函数的 prop 来实现。

javascript
function Mouse({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
}

// 使用
<Mouse render={({ x, y }) => (
  <h1>Mouse position: {x}, {y}</h1>
)} />

1.2 使用场景 #

场景 说明
状态共享 多个组件共享状态
行为复用 复用组件行为逻辑
动态渲染 根据状态动态渲染内容
灵活组合 组件组合更灵活

1.3 与HOC对比 #

特性 Render Props HOC
嵌套问题 存在
命名冲突 可能存在
灵活性
可读性 一般

二、基本用法 #

2.1 render prop #

javascript
function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return render({ data, loading, error });
}

// 使用
<DataFetcher
  url="/api/users"
  render={({ data, loading, error }) => {
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    return <UserList users={data} />;
  }}
/>

2.2 children prop #

javascript
function Mouse({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
      {children(position)}
    </div>
  );
}

// 使用
<Mouse>
  {({ x, y }) => (
    <h1>Mouse position: {x}, {y}</h1>
  )}
</Mouse>

2.3 自定义prop名 #

javascript
function Toggle({ render, children, ...props }) {
  const [on, setOn] = useState(false);
  const toggle = () => setOn(!on);

  const renderProp = children || render;

  return renderProp({ on, toggle });
}

// 使用方式一
<Toggle render={({ on, toggle }) => (
  <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
)} />

// 使用方式二
<Toggle>
  {({ on, toggle }) => (
    <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
  )}
</Toggle>

三、常见模式 #

3.1 鼠标追踪 #

javascript
function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div
      onMouseMove={handleMouseMove}
      style={{ height: '300px', border: '1px solid #ccc' }}
    >
      {children(position)}
    </div>
  );
}

// 使用
function App() {
  return (
    <MouseTracker>
      {({ x, y }) => (
        <div>
          <h2>追踪鼠标</h2>
          <p>位置: {x}, {y}</p>
          <div
            style={{
              position: 'absolute',
              left: x,
              top: y,
              width: '20px',
              height: '20px',
              background: 'red',
              borderRadius: '50%'
            }}
          />
        </div>
      )}
    </MouseTracker>
  );
}

3.2 切换状态 #

javascript
function Toggle({ initial = false, children }) {
  const [on, setOn] = useState(initial);
  
  const toggle = () => setOn(!on);
  const setOnState = () => setOn(true);
  const setOffState = () => setOn(false);

  return children({
    on,
    toggle,
    setOn: setOnState,
    setOff: setOffState
  });
}

// 使用
function Modal() {
  return (
    <Toggle>
      {({ on, toggle }) => (
        <>
          <button onClick={toggle}>打开弹窗</button>
          {on && (
            <div className="modal">
              <div className="modal-content">
                <h2>弹窗标题</h2>
                <button onClick={toggle}>关闭</button>
              </div>
            </div>
          )}
        </>
      )}
    </Toggle>
  );
}

3.3 表单状态 #

javascript
function Form({ initialValues, children }) {
  const [values, setValues] = useState(initialValues);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));
  };

  const reset = () => setValues(initialValues);

  return children({ values, handleChange, reset });
}

// 使用
function LoginForm() {
  return (
    <Form initialValues={{ email: '', password: '' }}>
      {({ values, handleChange, reset }) => (
        <form onSubmit={(e) => {
          e.preventDefault();
          console.log(values);
        }}>
          <input
            name="email"
            value={values.email}
            onChange={handleChange}
          />
          <input
            name="password"
            type="password"
            value={values.password}
            onChange={handleChange}
          />
          <button type="submit">登录</button>
          <button type="button" onClick={reset}>重置</button>
        </form>
      )}
    </Form>
  );
}

3.4 列表操作 #

javascript
function ListManager({ initialItems, children }) {
  const [items, setItems] = useState(initialItems);

  const addItem = (item) => {
    setItems(prev => [...prev, { id: Date.now(), ...item }]);
  };

  const removeItem = (id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  };

  const updateItem = (id, updates) => {
    setItems(prev => prev.map(item =>
      item.id === id ? { ...item, ...updates } : item
    ));
  };

  const clearItems = () => setItems([]);

  return children({
    items,
    addItem,
    removeItem,
    updateItem,
    clearItems
  });
}

// 使用
function TodoList() {
  return (
    <ListManager initialItems={[]}>
      {({ items, addItem, removeItem }) => (
        <div>
          <button onClick={() => addItem({ text: 'New Task' })}>
            添加任务
          </button>
          <ul>
            {items.map(item => (
              <li key={item.id}>
                {item.text}
                <button onClick={() => removeItem(item.id)}>删除</button>
              </li>
            ))}
          </ul>
        </div>
      )}
    </ListManager>
  );
}

四、与Hooks结合 #

4.1 Render Props转Hooks #

javascript
// Render Props方式
function Mouse({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return children(position);
}

// Hooks方式
function useMouse() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return position;
}

// 使用
function Component() {
  const { x, y } = useMouse();
  return <div>{x}, {y}</div>;
}

4.2 混合使用 #

javascript
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn(prev => !prev), []);
  return { on, toggle };
}

function Toggle({ initial, children }) {
  const toggleState = useToggle(initial);
  return children(toggleState);
}

五、最佳实践 #

5.1 提供默认渲染 #

javascript
function List({ items, renderItem, renderEmpty }) {
  if (items.length === 0) {
    return renderEmpty ? renderEmpty() : <div>No items</div>;
  }

  return (
    <ul>
      {items.map((item, index) => (
        <li key={item.id || index}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// 使用
<List
  items={users}
  renderItem={user => <span>{user.name}</span>}
  renderEmpty={() => <div>No users found</div>}
/>

5.2 避免内联函数(性能优化) #

javascript
// ❌ 每次渲染创建新函数
<Mouse>
  {({ x, y }) => <div>{x}, {y}</div>}
</Mouse>

// ✅ 使用useCallback
function App() {
  const renderMouse = useCallback(({ x, y }) => (
    <div>{x}, {y}</div>
  ), []);

  return <Mouse>{renderMouse}</Mouse>;
}

5.3 类型检查 #

javascript
import PropTypes from 'prop-types';

function Mouse({ children, render }) {
  const renderFn = children || render;
  
  if (typeof renderFn !== 'function') {
    console.error('Mouse requires a render prop');
    return null;
  }

  // ...
}

Mouse.propTypes = {
  children: PropTypes.func,
  render: PropTypes.func
};

5.4 组合多个Render Props #

javascript
function App() {
  return (
    <Mouse>
      {mouse => (
        <Toggle>
          {toggle => (
            <div>
              <p>Mouse: {mouse.x}, {mouse.y}</p>
              <button onClick={toggle.toggle}>
                {toggle.on ? 'ON' : 'OFF'}
              </button>
            </div>
          )}
        </Toggle>
      )}
    </Mouse>
  );
}

六、实际案例 #

6.1 分页组件 #

javascript
function Pagination({ total, pageSize, children }) {
  const [current, setCurrent] = useState(1);
  const totalPages = Math.ceil(total / pageSize);

  const go = (page) => {
    if (page >= 1 && page <= totalPages) {
      setCurrent(page);
    }
  };

  const next = () => go(current + 1);
  const prev = () => go(current - 1);

  return children({
    current,
    totalPages,
    go,
    next,
    prev,
    hasNext: current < totalPages,
    hasPrev: current > 1
  });
}

// 使用
<Pagination total={100} pageSize={10}>
  {({ current, totalPages, next, prev, hasNext, hasPrev }) => (
    <div>
      <span>Page {current} of {totalPages}</span>
      <button onClick={prev} disabled={!hasPrev}>Previous</button>
      <button onClick={next} disabled={!hasNext}>Next</button>
    </div>
  )}
</Pagination>

6.2 搜索组件 #

javascript
function Search({ data, searchKey, children }) {
  const [query, setQuery] = useState('');

  const results = useMemo(() => {
    if (!query) return data;
    return data.filter(item =>
      item[searchKey].toLowerCase().includes(query.toLowerCase())
    );
  }, [data, query, searchKey]);

  return children({
    query,
    setQuery,
    results
  });
}

// 使用
<Search data={users} searchKey="name">
  {({ query, setQuery, results }) => (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )}
</Search>

七、总结 #

要点 说明
定义 使用函数prop共享代码
常用prop children或render
灵活性 高,可动态渲染
推荐程度 新项目优先Hooks

核心原则:

  • Render Props 提供灵活的组件复用方式
  • 使用 children prop 更直观
  • 新项目优先使用 Hooks
  • 注意性能优化
最后更新:2026-03-26