PGlite React Hooks 实战指南:用 PGliteProvider 与 useLiveQuery 构建响应式 React 应用
【免费下载链接】pgliteEmbeddable Postgres with real-time, reactive bindings.项目地址: https://gitcode.com/GitHub_Trending/pg/pglite
PGlite 是运行在 WebAssembly 中的可嵌入式 Postgres,而@electric-sql/pglite-react包则把它的 live query 插件封装成了符合 React 习惯的 Hooks API。本文围绕 pglite-react 包的官方文档 与 framework-hooks/react.md,系统讲解PGliteProvider、usePGlite、makePGliteProvider、useLiveQuery、useLiveQuery.sql与useLiveIncrementalQuery六个 API 的完整用法、接口签名、底层实现与测试验证,读完即可在 React 项目中实现"数据库变化即组件重渲染"的响应式数据流。
前置条件:安装与 live 扩展
在 React 项目中使用这些 Hooks 之前,需要安装两个包:
npm install @electric-sql/pglite npm install @electric-sql/pglite-react其中@electric-sql/pglite-react的package.json(见 packages/pglite-react/package.json)声明了对react的 peer 依赖为^18.0.0 || ^19.0.0 || ^19.0.0-rc,即支持 React 18 与 React 19;同时依赖@electric-sql/pglite提供底层数据库能力。
还有一个关键前置:所有响应式 Hooks 都构建在 live query 插件之上,因此创建 PGlite 实例时必须启用live扩展,否则db.live命名空间不存在,Hooks 无法工作。创建方式见 live-queries.md:
import { PGlite } from "@electric-sql/pglite" import { live } from "@electric-sql/pglite/live" const db = await PGlite.create({ extensions: { live } })PGliteProvider:把数据库实例注入组件树
PGliteProvider是一个 Provider 组件,用于把创建好的 PGlite 实例传递给所有子组件,供usePGlite、useLiveQuery、useLiveIncrementalQuery使用。用法是把它包裹在应用根部,并通过db属性传入实例:
import { PGlite } from "@electric-sql/pglite" import { live } from "@electric-sql/pglite/live" import { PGliteProvider } from "@electric-sql/pglite-react" const db = await PGlite.create({ extensions: { live } }) const App = () => { // ... return ( <PGliteProvider db={db}> {/* 子组件 */} </PGliteProvider> ) }从源码看(packages/pglite-react/src/provider.tsx),PGliteProvider本质上是makePGliteProvider以默认类型PGliteWithLive调用生成的实例:它内部通过createContext<T | undefined>(undefined)创建上下文,PGliteProvider组件则渲染<ctx.Provider value={db}>{children}</ctx.Provider>。也就是说,Provider 本身不创建数据库,只负责"传递";数据库的初始化与生命周期仍由你控制。
usePGlite:在任意子组件中获取数据库实例
usePGlite用于在组件内取回 Provider 提供的 PGlite 实例,之后就可以直接对它执行任意 SQL 操作:
import { usePGlite } from "@electric-sql/pglite-react" const MyComponent = () => { const db = usePGlite() const insertItem = () => { db.query("INSERT INTO my_table (name, number) VALUES ('Arthur', 42);") } return ( <> <button onClick={insertItem}>Insert</button> </> ) }usePGlite的源码行为值得注意(packages/pglite-react/src/provider.tsx):
- 它优先返回组件树中上下文里保存的实例;
- 如果显式传入一个
db参数(usePGlite(db)),会直接返回该参数而忽略上下文——这为脱离 Provider 的测试或特殊场景提供了逃生通道; - 如果既没有上下文实例也没有显式参数,会抛出错误:
'No PGlite instance found, use PGliteProvider to provide one'。这提醒我们,忘记包裹PGliteProvider是这类 Hooks 最常见的运行时错误来源。
对应测试见 packages/pglite-react/test/provider.test.tsx:can receive PGlite用例验证了在PGliteProvider包裹下usePGlite()能取回同一个db引用。
makePGliteProvider:为扩展类型创建强类型 Provider
PGlite 支持扩展机制,扩展会在实例上挂载额外的命名空间与类型(例如 live 的db.live、pgvector 的db.vector)。默认导出的PGliteProvider只具备PGliteWithLive类型,若要同时使用多个扩展并保持类型推断,就需要makePGliteProvider。
makePGliteProvider<T>()返回一个带有指定泛型类型T的PGliteProvider组件与usePGliteHook 对。典型用法是在项目里创建一个导出模块:
import { PGlite, PGliteInterfaceExtensions } from '@electric-sql/pglite' import { LiveNamespace } from '@electric-sql/pglite/live' import { VectorNamespace } from '@electric-sql/pglite-pgvector' import { makePGliteProvider } from '@electric-sql/pglite-react' const { PGliteProvider, usePGlite } = makePGliteProvider< PGlite & PGliteInterfaceExtensions<{ live: typeof live vector: typeof vector }> >() export { PGliteProvider, usePGlite }此后项目中导入的usePGlite()返回的db就同时携带live与vector命名空间及其完整类型,不再需要手动断言。需要说明的是,LiveNamespace、VectorNamespace等扩展命名空间类型均从对应扩展包导出(如@electric-sql/pglite/live、@electric-sql/pglite-pgvector),示例中live、vector即对应扩展模块的值,可按实际安装的扩展调整泛型参数。
从 provider.tsx 的实现看,makePGliteProvider每次调用都会创建独立的 Context,因此多个 Provider 体系可以并存而互不干扰。测试can receive PGlite with typed provider(provider.test.tsx)验证了makePGliteProvider<PGliteWithLive>()产出的类型化 Provider/Hook 同样能正确回传实例。
useLiveQuery:让组件随查询结果自动重渲染
useLiveQuery是面向 React 的响应式查询 Hook,它包装了 live 扩展的.live.query()API:当查询所依赖的表发生变化时,组件会自动重新渲染并拿到最新结果。其接口签名如下:
function useLiveQuery<T = { [key: string]: unknown }>( query: string, params: unknown[] | undefined | null, ): Results<T>两个参数分别是:
- SQL 查询字符串;
- 查询的可选参数(对应 SQL 中的
$1、$2占位符),不需要时传null或省略。
import { useLiveQuery } from '@electric-sql/pglite-react' const MyComponent = () => { const maxNumber = 100 const items = useLiveQuery(` SELECT * FROM my_table WHERE number <= $1 ORDER BY number; `, [maxNumber]) return ( <> { items.map((item) => <MyItem item={item} /> ) } </> ) }useLiveQuery 的重载能力:直接接收 LiveQuery 对象或 Promise
文档描述的接口仅覆盖字符串形式,但源码(packages/pglite-react/src/hooks.ts)揭示了更丰富的重载:useLiveQuery除了字符串查询,还可以接收:
- 一个已经创建好的
LiveQuery<T>对象(来自db.live.query(...)的返回值); - 一个
Promise<LiveQuery<T>>(即未 await 的db.live.query(...)调用本身)。
对应测试位于 packages/pglite-react/test/hooks.test.tsx 的can take a live query return value directly与can take a live query returned promise directly用例:两者都能在后续对表执行 INSERT 后,自动把最新行反映到 Hook 返回值中。这三种形态统一由内部实现useLiveQueryImpl处理,详见下文"底层实现"。
useLiveQuery.sql:标签模板语法构造查询
useLiveQuery.sql是useLiveQuery的标签模板函数(tagged template)形态,与 PGlite 核心的 模板查询 API 一一对应。它允许把参数直接内插进模板字符串,由底层负责解析成 SQL 与参数数组,无需手动维护$1占位符:
import { useLiveQuery } from '@electric-sql/pglite-react' const MyComponent = () => { const maxNumber = 100 const items = useLiveQuery.sql` SELECT * FROM my_table WHERE number <= ${maxNumber} ORDER BY number; ` // ... }源码(hooks.ts)显示其实现借助了@electric-sql/pglite/template的query as buildQuery函数,把模板字符串和插值展开为{ query, params },再交给useLiveQueryImpl。测试updates when query parameter changes(hooks.test.tsx)验证了:当模板内插值从'test1'变为'test2'并触发重渲染后,查询结果会随之更新为对应行的数据。
useLiveIncrementalQuery:把 diff 计算下沉到 Postgres
useLiveIncrementalQuery同样提供响应式重渲染,但它包装的是.live.incrementalQuery()API。二者的核心差异在于 diff 的承担位置:
useLiveQuery(live.query)在表变化时于 WASM 内部重跑整个查询;useLiveIncrementalQuery(live.incrementalQuery)在 Postgres 内部维护一张上一状态的临时表,表变化后重跑查询并与上次状态做 diff,只把变化的部分从 WASM 拷贝到 JS。这在结果集大、行宽(wide rows)的场景下性能更好,尤其适合喂给 React 做渲染。
其接口签名:
function useLiveIncrementalQuery<T = { [key: string]: unknown }>( query: string, params: unknown[] | undefined | null, key: string, ): Results<T>三个参数分别是:
- SQL 查询字符串;
- 查询的可选参数;
- diff 算法所依据的键列名(key column),通常是主键。
import { useLiveIncrementalQuery } from '@electric-sql/pglite-react' const MyComponent = () => { const maxNumber = 100 const items = useLiveIncrementalQuery(` SELECT * FROM my_table WHERE number <= $1 ORDER BY number; `, [maxNumber], 'id') return ( <> { items.map((item) => <MyItem item={item} /> ) } </> ) }关于key参数对 diff 的意义,可参考 live/interface.ts 中LiveIncrementalQueryOptions的定义:{ query, params, key, callback, signal }。底层live.changes依赖该键比对行差异,并在ChangeUpdate/ChangeInsert中通过__after__字段记录"该行应排在哪个 key 之后",从而支持有序结果集内的高效移动(详见 live-queries.md 中对Change类型的说明)。
返回值结构:Results 与 LiveQueryResults
两个 live Hooks 的返回值都是查询结果对象。源码(hooks.ts)对原始LiveQueryResults<T>做了裁剪,返回结构为:
{ rows: T[] // 当前结果行 fields: { name: string; dataTypeID: number }[] // 字段元信息 totalCount?: number // 总行数(窗口化查询时存在) offset?: number // 当前偏移(窗口化查询时存在) limit?: number // 当前限制(窗口化查询时存在) }注意它不包含affectedRows字段。测试can receive initial results(hooks.test.tsx)展示了返回值的精确形态,例如fields会携带每个列的name与dataTypeID(如id列对应类型 OID 23,name文本列对应 25)。
此外需要留意一个异步细节:初始渲染时返回值可能是undefined,直到 live 查询建立并返回初始结果后才有值。测试中普遍使用waitFor(() => expect(result.current).not.toBe(undefined))等待结果就绪,实际组件里也应做好空值防御(例如items?.rows或可选链)。
底层实现:useLiveQueryImpl 如何做到响应式
把文档抽象成机制,核心都收敛在 packages/pglite-react/src/hooks.ts 的useLiveQueryImpl中,它同时服务于三个公开 Hook(useLiveQuery字符串形式、useLiveQuery.sql、useLiveIncrementalQuery)。其关键机制可以归纳为四点:
1. 订阅与清理(useEffect)
Hook 在useEffect中根据输入形态建立订阅:
- 字符串查询 +
key未定义 →db.live.query<T>(query, currentParams, cb); - 字符串查询 +
key已定义 →db.live.incrementalQuery<T>(query, currentParams, key, cb); Promise<LiveQuery<T>>→ 在.then中保存 liveQuery、用initialResults初始化 state 并subscribe(cb);- 直接传入
LiveQuery<T>→ 同样用initialResults初始化并订阅。
effect 的清理函数会cancelled = true并调用unsubscribe()/liveQuery.unsubscribe(cb),确保组件卸载或依赖变化时及时断开订阅,避免内存泄漏与对已卸载组件的 setState。
2. 参数变化的浅比较
paramsEqual使用Object.is逐元素比较新旧参数数组,长度不同或任一元素不同都视为变化,从而触发重新订阅。测试updates when query parameters change(hooks.test.tsx)验证了参数值变化与参数个数变化(['test1']→['test1','test2'])两条路径都会让结果正确刷新。
3. 查询语句变化的响应
effect 的依赖数组包含query,因此 SQL 字符串变化时(如测试中SELECT * FROM test改为带 WHERE 的版本)会重建订阅;useLiveQuery.sql通过buildQuery生成的新{ query, params }同样走这条路径。
4. 订阅回调驱动重渲染
live 查询的回调cb每次收到新结果即setResults(results),触发 React 重渲染;返回时再按上文结构裁剪字段。整个链路实现了"Postgres 表数据变化 → WASM 内 live 机制检测 → 回调 → setState → 组件重渲染"的响应式闭环。
从 live 插件看 Hooks 的能力边界
useLiveQuery/useLiveIncrementalQuery的能力上限由 live 扩展决定。参考 live/interface.ts 与 live-queries.md 可知底层LiveNamespace提供三类 API:
live.query():适合小结果集、窄行的基础实时查询,PGlite 内机制开销更小;live.incrementalQuery():从live.changes发出的增量变化物化完整结果集,适合大结果集与宽行,也是 React Hooks 的推荐选择;live.changes():更低层的变更流 API,直接输出INSERT/UPDATE/DELETE变更(带__op__、__changed_columns__、__after__等字段),可用于实现高效的就地 DOM 更新。
在 React Hooks 语境下,如果组件需要实现窗口化分页,可以直接通过db.live.query({ query, offset, limit, callback })创建带totalCount/offset/limit的窗口查询,再把返回的LiveQuery对象(或 Promise)交给useLiveQuery消费——这正是前面提到的重载形态的典型应用场景。
总结:一套 API 打通 React 与嵌入式 Postgres
@electric-sql/pglite-react提供了从"实例注入"到"响应式查询"的完整闭环:
| API | 作用 | 关键参数 |
|---|---|---|
PGliteProvider | 向组件树注入 PGlite 实例 | db |
usePGlite | 取回上下文中的实例 | (可选db覆盖上下文) |
makePGliteProvider<T>() | 生成带扩展类型的 Provider/Hook 对 | 泛型T |
useLiveQuery | 响应式查询(包装live.query) | SQL、可选参数 |
useLiveQuery.sql | 模板字符串形态的响应式查询 | 内插参数 |
useLiveIncrementalQuery | 增量 diff 的响应式查询(包装live.incrementalQuery) | SQL、参数、key 列名 |
实战落地只需四步:启用live扩展创建 PGlite → 用PGliteProvider包裹应用 → 在子组件用usePGlite或扩展类型化的makePGliteProvider获取实例 → 用useLiveQuery/useLiveIncrementalQuery声明式订阅查询。搭配 React 框架 Hooks 文档、live 查询扩展文档 与 测试用例,即可在浏览器中构建数据与界面实时同步的 React 应用。
【免费下载链接】pgliteEmbeddable Postgres with real-time, reactive bindings.项目地址: https://gitcode.com/GitHub_Trending/pg/pglite
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考