06-高级模式与实战项目——01. Render Props - 共享渲染逻辑
📅 2026/7/6 3:56:18
👁️ 阅读次数
📝 编程学习
01. Render Props - 共享渲染逻辑
概述
Render Props 是一种 React 组件模式,通过将一个函数作为 prop传递给组件,让组件决定如何渲染。这个函数接收组件的内部状态作为参数,返回要渲染的 UI。
| 维度 | 内容 |
|---|---|
| What | 使用函数作为 prop 来共享渲染逻辑的模式 |
| Why | 实现代码复用,将渲染逻辑与业务逻辑分离 |
| When | 需要共享组件行为但渲染方式不同时 |
| Where | 组件内部通过 children 或 render prop 接收函数 |
| Who | 需要高度复用逻辑的开发者 |
| How | <DataProvider render={(data) => <UI data={data} />} /> |
1. 什么是 Render Props
1.1 基本概念
Render Props 是指一个组件接收一个函数作为 prop,这个函数返回 React 元素。
// Render Props 模式 <MouseTracker render={(mouse) => ( <div> 鼠标位置: {mouse.x}, {mouse.y} </div> )} />1.2 为什么需要 Render Props
// ❌ 问题:重复的鼠标跟踪逻辑 function ComponentA() { const [mouse, setMouse] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMove = (e) => setMouse({ x: e.clientX, y: e.clientY }); window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, []); return <div>鼠标位置: {mouse.x}, {mouse.y}</div>; } function ComponentB() { // 同样的逻辑重复写一遍 const [mouse, setMouse] = useState({ x: 0, y: 0 }); // ... 重复代码 }// ✅ 使用 Render Props 复用逻辑 function MouseTracker({ render }) { const [mouse, setMouse] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMove = (e) => setMouse({ x: e.clientX, y: e.clientY }); window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, []); return render(mouse); } // 使用 <MouseTracker render={(mouse) => <div>位置: {mouse.x}, {mouse.y}</div>} /> <MouseTracker render={(mouse) => <img style={{ left: mouse.x, top: mouse.y }} />} />2. 基础示例
2.1 使用 render prop
// 数据获取组件 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(data => { setData(data); setLoading(false); }) .catch(err => { setError(err); setLoading(false); }); }, [url]); return render({ data, loading, error }); } // 使用 <DataFetcher url="https://api.example.com/users" render={({ data, loading, error }) => { if (loading) return <div>加载中...</div>; if (error) return <div>错误: {error.message}</div>; return <UserList users={data} />; }} />2.2 使用 children 作为函数
// 使用 children 作为 render prop function DataFetcher({ url, children }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch(url) .then(res => res.json()) .then(data => { setData(data); setLoading(false); }) .catch(err => { setError(err); setLoading(false); }); }, [url]); return children({ data, loading, error }); } // 使用 <DataFetcher url="https://api.example.com/users"> {({ data, loading, error }) => { if (loading) return <div>加载中...</div>; if (error) return <div>错误: {error.message}</div>; return <UserList users={data} />; }} </DataFetcher>3. 实际应用场景
3.1 鼠标位置跟踪
function MouseTracker({ 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); } // 显示坐标 <MouseTracker> {({ x, y }) => <div>鼠标位置: ({x}, {y})</div>} </MouseTracker> // 跟随鼠标的图片 <MouseTracker> {({ x, y }) => ( <img src="/cursor.png" style={{ position: 'absolute', left: x, top: y }} /> )} </MouseTracker>3.2 窗口尺寸监听
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 }) => ( <div> 窗口大小: {width} x {height} </div> )} </WindowSize>3.3 滚动位置监听
function ScrollPosition({ children }) { const [scroll, setScroll] = useState({ x: 0, y: 0 }); useEffect(() => { const handleScroll = () => { setScroll({ x: window.scrollX, y: window.scrollY, }); }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); return children(scroll); } // 使用 <ScrollPosition> {({ x, y }) => ( <div> 滚动位置: ({x}, {y}) <ProgressBar progress={y / (document.body.scrollHeight - window.innerHeight)} /> </div> )} </ScrollPosition>3.4 表单状态管理
function Form({ children, onSubmit }) { const [values, setValues] = useState({}); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const handleChange = (e) => { const { name, value } = e.target; setValues(prev => ({ ...prev, [name]: value })); }; const handleBlur = (e) => { const { name } = e.target; setTouched(prev => ({ ...prev, [name]: true })); }; const handleSubmit = (e) => { e.preventDefault(); onSubmit(values); }; return children({ values, errors, touched, handleChange, handleBlur, handleSubmit, }); } // 使用 <Form onSubmit={(values) => console.log(values)}> {({ values, handleChange, handleSubmit }) => ( <form onSubmit={handleSubmit}> <input name="email" value={values.email || ''} onChange={handleChange} placeholder="邮箱" /> <input name="password" type="password" value={values.password || ''} onChange={handleChange} placeholder="密码" /> <button type="submit">登录</button> </form> )} </Form>4. Render Props vs HOC
| 特性 | Render Props | HOC |
|---|---|---|
| 代码复用 | 通过函数传递 | 通过组件包装 |
| 命名冲突 | 无 | 可能冲突 |
| 组合性 | 嵌套较深 | 可链式调用 |
| 性能 | 可能内联函数 | 可能需要 memo |
| 调试 | 组件树清晰 | 多层包装 |
// Render Props <MouseTracker> {(mouse) => ( <WindowSize> {(size) => ( <div>鼠标: {mouse.x}, 窗口: {size.width}</div> )} </WindowSize> )} </MouseTracker> // HOC const EnhancedComponent = withMouse(withWindowSize(MyComponent));5. 完整示例:数据表格组件
// 可配置的数据表格组件 function DataTable({ fetchData, renderTable, pageSize = 10 }) { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); useEffect(() => { setLoading(true); fetchData({ page, pageSize }) .then(({ data, total }) => { setData(data); setTotal(total); setLoading(false); }); }, [page, pageSize, fetchData]); const nextPage = () => setPage(p => p + 1); const prevPage = () => setPage(p => Math.max(1, p - 1)); return renderTable({ data, loading, page, total, totalPages: Math.ceil(total / pageSize), nextPage, prevPage, }); } // 使用 <DataTable fetchData={({ page, pageSize }) => fetch(`/api/users?page=${page}&limit=${pageSize}`).then(res => res.json()) } renderTable={({ data, loading, page, totalPages, nextPage, prevPage }) => ( <div> {loading ? ( <div>加载中...</div> ) : ( <> <table> <thead> <tr><th>ID</th><th>姓名</th><th>邮箱</th></tr> </thead> <tbody> {data.map(user => ( <tr key={user.id}> <td>{user.id}</td> <td>{user.name}</td> <td>{user.email}</td> </tr> ))} </tbody> </table> <div> <button onClick={prevPage} disabled={page === 1}>上一页</button> <span>第 {page} / {totalPages} 页</span> <button onClick={nextPage} disabled={page === totalPages}>下一页</button> </div> </> )} </div> )} />6. 总结
核心要点
| 要点 | 说明 |
|---|---|
| 核心价值 | 高度复用组件逻辑 |
| 实现方式 | render prop 或 children 函数 |
| 适用场景 | 数据获取、鼠标跟踪、表单状态 |
| 替代方案 | HOC、自定义 Hooks |
记忆口诀
Render Props 函数传,逻辑复用不困难
children 也能当函数,灵活渲染随便换
7. 相关资源
- React Render Props 文档
- Render Props 模式
编程学习
技术分享
实战经验