流式渲染
pipel-react 提供了强大的流式渲染能力,通过 usePipelRender hook 可以实现细粒度的组件渲染控制,类似于 signal 的渲染机制。
基本概念
传统的 React 渲染是组件级别的:当状态改变时,整个组件会重新渲染。而流式渲染可以实现更细粒度的控制,只更新需要变化的部分。
使用 usePipelRender
usePipelRender 允许你订阅流的变化并返回渲染函数:
tsx
import { usePipel, usePipelRender } from 'pipel-react'
function Example() {
const [count, count$] = usePipel(0)
// 使用 usePipelRender 订阅流的变化
const renderCount = usePipelRender(count$, (value) => <span>Count: {value}</span>)
return (
<div>
<h1>流式渲染示例</h1>
{renderCount}
<button onClick={() => count$.next(count + 1)}>增加</button>
</div>
)
}性能优化
流式渲染的主要优势在于性能优化。当你有多个独立的数据流时,每个流的变化只会触发对应部分的重新渲染:
tsx
import { useStream, usePipelRender } from 'pipel-react'
function Dashboard() {
const user$ = useStream({ name: 'John', age: 25 })
const order$ = useStream({ item: 'Book', price: 29.99 })
// 用户信息渲染
const renderUser = usePipelRender(user$, (user) => (
<div className="card">
<h3>用户信息</h3>
<p>姓名: {user.name}</p>
<p>年龄: {user.age}</p>
<p>渲染时间: {Date.now()}</p>
</div>
))
// 订单信息渲染
const renderOrder = usePipelRender(order$, (order) => (
<div className="card">
<h3>订单信息</h3>
<p>商品: {order.item}</p>
<p>价格: ${order.price}</p>
<p>渲染时间: {Date.now()}</p>
</div>
))
return (
<div>
<h1>仪表盘</h1>
<div className="dashboard">
{renderUser}
{renderOrder}
</div>
<div className="controls">
<button
onClick={() =>
user$.set((v) => {
v.age += 1
})
}
>
增加年龄
</button>
<button
onClick={() =>
order$.set((v) => {
v.price += 10
})
}
>
增加价格
</button>
</div>
</div>
)
}在上面的例子中:
- 点击"增加年龄"只会重新渲染用户信息卡片
- 点击"增加价格"只会重新渲染订单信息卡片
- 主组件不会重新渲染
对比传统渲染
传统 useState 方式
tsx
import { useState } from 'react'
function TraditionalDashboard() {
const [user, setUser] = useState({ name: 'John', age: 25 })
const [order, setOrder] = useState({ item: 'Book', price: 29.99 })
// 任何状态变化都会导致整个组件重新渲染
return (
<div>
<h1>仪表盘</h1>
<p>组件渲染时间: {Date.now()}</p>
<div className="dashboard">
<div className="card">
<h3>用户信息</h3>
<p>姓名: {user.name}</p>
<p>年龄: {user.age}</p>
<p>渲染时间: {Date.now()}</p>
</div>
<div className="card">
<h3>订单信息</h3>
<p>商品: {order.item}</p>
<p>价格: ${order.price}</p>
<p>渲染时间: {Date.now()}</p>
</div>
</div>
<div className="controls">
<button onClick={() => setUser((u) => ({ ...u, age: u.age + 1 }))}>增加年龄</button>
<button onClick={() => setOrder((o) => ({ ...o, price: o.price + 10 }))}>增加价格</button>
</div>
</div>
)
}pipel-react 流式渲染方式
tsx
import { useStream, usePipelRender } from 'pipel-react'
function StreamDashboard() {
const user$ = useStream({ name: 'John', age: 25 })
const order$ = useStream({ item: 'Book', price: 29.99 })
const renderUser = usePipelRender(user$, (user) => (
<div className="card">
<h3>用户信息</h3>
<p>姓名: {user.name}</p>
<p>年龄: {user.age}</p>
<p>渲染时间: {Date.now()}</p>
</div>
))
const renderOrder = usePipelRender(order$, (order) => (
<div className="card">
<h3>订单信息</h3>
<p>商品: {order.item}</p>
<p>价格: ${order.price}</p>
<p>渲染时间: {Date.now()}</p>
</div>
))
// 主组件只渲染一次
return (
<div>
<h1>仪表盘</h1>
<p>组件渲染时间: {Date.now()}</p>
<div className="dashboard">
{renderUser}
{renderOrder}
</div>
<div className="controls">
<button
onClick={() =>
user$.set((v) => {
v.age += 1
})
}
>
增加年龄
</button>
<button
onClick={() =>
order$.set((v) => {
v.price += 10
})
}
>
增加价格
</button>
</div>
</div>
)
}复杂场景
列表渲染
tsx
import { useStream, usePipelRender } from 'pipel-react'
function TodoList() {
const todos$ = useStream([
{ id: 1, text: '学习 React', done: false },
{ id: 2, text: '学习 pipel-react', done: false },
])
const renderTodos = usePipelRender(todos$, (todos) => (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => {
todos$.set((list) => {
const item = list.find((t) => t.id === todo.id)
if (item) item.done = !item.done
})
}}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>{todo.text}</span>
</li>
))}
</ul>
))
return (
<div>
<h2>待办事项</h2>
{renderTodos}
</div>
)
}条件渲染
tsx
import { usePipel, usePipelRender } from 'pipel-react'
function ConditionalRender() {
const [isLoggedIn, isLoggedIn$] = usePipel(false)
const renderContent = usePipelRender(isLoggedIn$, (loggedIn) =>
loggedIn ? (
<div>
<h2>欢迎回来!</h2>
<button onClick={() => isLoggedIn$.next(false)}>登出</button>
</div>
) : (
<div>
<h2>请登录</h2>
<button onClick={() => isLoggedIn$.next(true)}>登录</button>
</div>
)
)
return (
<div>
<h1>应用</h1>
{renderContent}
</div>
)
}过滤列表渲染
tsx
import { useStream, usePipelRender } from 'pipel-react'
import { combine, map } from 'pipeljs'
function FilteredList() {
const items$ = useStream([
{ id: 1, name: 'Apple', category: 'fruit' },
{ id: 2, name: 'Carrot', category: 'vegetable' },
{ id: 3, name: 'Banana', category: 'fruit' },
])
const filter$ = useStream('all')
const filteredItems$ = items$.pipe(
combine(filter$),
map(([items, filter]) =>
filter === 'all' ? items : items.filter((item) => item.category === filter)
)
)
const renderList = usePipelRender(filteredItems$, (items) => (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
))
const renderFilter = usePipelRender(filter$, (filter) => (
<select value={filter} onChange={(e) => filter$.next(e.target.value)}>
<option value="all">全部</option>
<option value="fruit">水果</option>
<option value="vegetable">蔬菜</option>
</select>
))
return (
<div>
{renderFilter}
{renderList}
</div>
)
}异步渲染
渲染异步数据
tsx
import { usePipel } from 'pipel-react'
function AsyncData() {
const [data, data$] = usePipel(fetch('/api/data').then((r) => r.json()))
if (!data) return <div>加载中...</div>
return <div>{data.message}</div>
}使用 useFetch 渲染异步数据
tsx
import { useFetch } from 'pipel-react'
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`/api/users/${userId}`, { immediate: true })
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error.message}</div>
if (!data) return <div>无数据</div>
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
)
}渲染多个异步数据源
tsx
import { useStream, usePipel } from 'pipel-react'
function MultipleAsyncData() {
const user$ = useStream(fetch('/api/user').then((r) => r.json()))
const posts$ = useStream(fetch('/api/posts').then((r) => r.json()))
const [user] = usePipel(user$)
const [posts] = usePipel(posts$)
if (!user || !posts) return <div>加载中...</div>
return (
<div>
<h1>{user.name}</h1>
<PostList posts={posts} />
</div>
)
}实时渲染
WebSocket 实时数据渲染
tsx
import { fromEvent } from 'pipel-react'
import { useEffect } from 'react'
function LiveChat() {
const [messages, messages$] = usePipel([])
useEffect(() => {
const ws = new WebSocket('ws://localhost:8080')
const message$ = fromEvent(ws, 'message')
const subscription = message$.then((event) => {
const newMessage = JSON.parse(event.data)
messages$.next([...messages, newMessage])
})
return () => {
subscription.unsubscribe()
ws.close()
}
}, [])
const renderMessages = usePipelRender(messages$, (msgs) => (
<div className="messages">
{msgs.map((msg, i) => (
<div key={i} className="message">
{msg.text}
</div>
))}
</div>
))
return (
<div>
<h2>实时聊天</h2>
{renderMessages}
</div>
)
}轮询数据渲染
tsx
import { useEffect } from 'react'
import { usePipel, usePipelRender } from 'pipel-react'
function LiveData() {
const [data, data$] = usePipel(null)
useEffect(() => {
// 立即获取一次数据
fetch('/api/data')
.then((r) => r.json())
.then((newData) => data$.next(newData))
// 使用 setInterval 实现轮询
const intervalId = setInterval(async () => {
const newData = await fetch('/api/data').then((r) => r.json())
data$.next(newData)
}, 5000)
return () => clearInterval(intervalId)
}, [data$])
const renderData = usePipelRender(data$, (d) =>
d ? <div>最新数据: {d.value}</div> : <div>加载中...</div>
)
return (
<div>
<h2>实时数据</h2>
{renderData}
</div>
)
}性能最佳实践
1. 使用 key 优化列表渲染
tsx
// ✅ 好:使用稳定的 key
{
items.map((item) => <Item key={item.id} data={item} />)
}
// ❌ 避免:使用索引作为 key
{
items.map((item, index) => <Item key={index} data={item} />)
}2. 拆分大组件
tsx
// ✅ 好:拆分成小组件,每个组件订阅自己的流
function TodoApp() {
const todos$ = useStream([])
return (
<div>
<TodoList todos$={todos$} />
<TodoStats todos$={todos$} />
<TodoFilters />
</div>
)
}
function TodoList({ todos$ }) {
const renderList = usePipelRender(todos$, (todos) => (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
))
return renderList
}
// ❌ 避免:所有逻辑在一个组件
function TodoApp() {
const [todos, setTodos] = useState([])
// 大量的状态和逻辑...
return <div>{/* 大量的 JSX... */}</div>
}3. 使用 change 操作符避免重复渲染
tsx
import { debounce, filter, change } from 'pipeljs'
import { usePipel, useObservable } from 'pipel-react'
function SearchResults() {
const [query, query$] = usePipel('')
// 只在查询真正改变时才搜索
const searchQuery$ = query$.pipe(
debounce(300),
change(), // 避免相同值触发搜索
filter((q) => q.length > 2)
)
const { data: results, loading } = useFetch(searchQuery$.pipe(map((q) => `/api/search?q=${q}`)), {
immediate: false,
})
return (
<div>
<input value={query} onChange={(e) => query$.next(e.target.value)} />
{loading && <div>搜索中...</div>}
<ResultList results={results} />
</div>
)
}4. 使用 useMemo 缓存计算
tsx
import { useMemo } from 'react'
import { usePipel } from 'pipel-react'
function ExpensiveComponent() {
const [data, data$] = usePipel(largeDataSet)
const processedData = useMemo(() => {
return data.map((item) => expensiveOperation(item))
}, [data])
return <DataView data={processedData} />
}5. 延迟渲染非关键内容
tsx
import { lazy, Suspense } from 'react'
const HeavyComponent = lazy(() => import('./HeavyComponent'))
function App() {
return (
<div>
<Header />
<MainContent />
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
</Suspense>
</div>
)
}最佳实践
- 适用场景:当你有多个独立的数据流,且希望它们的变化不互相影响时使用
- 性能考虑:对于频繁更新的数据,使用流式渲染可以显著提升性能
- 代码组织:将相关的数据和渲染逻辑组织在一起,提高代码可维护性
- 避免过度使用:对于简单的场景,传统的 useState 可能更简单直接
注意事项
- 流式渲染的组件会在流的值变化时自动更新
- 组件卸载时会自动取消订阅,无需手动清理
- 渲染函数应该是纯函数,避免副作用
- 对于复杂的渲染逻辑,考虑使用
useMemo优化 usePipelRender返回的是 React 元素,可以直接在 JSX 中使用