5月27日 17:32

Qwik 状态管理怎么用?从 useSignal 到 useResource

Qwik 的状态管理围绕一个核心理念:可恢复性(Resumability)。与传统框架在客户端重新执行组件代码来恢复状态不同,Qwik 在服务端渲染时就将状态序列化到 HTML 中,浏览器可以直接从序列化点恢复执行,无需水合(Hydration)。这个设计决策深刻影响了 Qwik 状态管理 API 的形态。

useSignal:管理原始值

useSignal 是最轻量的响应式状态,适合存储数字、字符串、布尔值等原始类型。它返回一个包含 .value 属性的对象,修改 .value 就能触发更新。

tsx
import { component$, useSignal } from '@builder.io/qwik'; export const Counter = component$(() => { const count = useSignal(0); return ( <div> <p>当前计数:{count.value}</p> <button onClick$={() => count.value++}>+1</button> <button onClick$={() => count.value--}>-1</button> </div> ); });

几点注意:

  • useSignal 只能存储单个值,如果需要管理对象或数组,应该用 useStore
  • 访问值必须通过 .value,不能解构,否则会丢失响应性
  • Qwik 对 useSignal 的更新是细粒度的,只有真正依赖这个值的 DOM 节点会重新渲染

useStore:管理复杂对象

当状态是嵌套对象或数组时,用 useStore 替代 useSignal。它会自动追踪对象属性的变化,同样做到细粒度更新。

tsx
import { component$, useStore } from '@builder.io/qwik'; export const TodoList = component$(() => { const state = useStore({ items: [ { id: 1, text: '学习 Qwik', done: false }, { id: 2, text: '构建应用', done: false } ], newTodoText: '' }); return ( <div> <input value={state.newTodoText} onInput$={(ev) => (state.newTodoText = (ev.target as HTMLInputElement).value)} /> <button onClick$={() => { if (!state.newTodoText.trim()) return; state.items.push({ id: Date.now(), text: state.newTodoText, done: false }); state.newTodoText = ''; }}> 添加 </button> <ul> {state.items.map((item) => ( <li key={item.id}> <input type="checkbox" checked={item.done} onInput$={() => (item.done = !item.done)} /> {item.text} </li> ))} </ul> </div> ); });

useStore 的一个重要特性:它不仅能追踪顶层属性,也能追踪深层嵌套属性的变化。这意味着修改 item.done 同样会触发对应 DOM 的更新。

useComputed$:派生状态

当某个值依赖其他状态计算得出时,用 useComputed$。它会在依赖变化时自动重新计算,未变化时返回缓存值。

tsx
import { component$, useSignal, useComputed$ } from '@builder.io/qwik'; export const PriceCalculator = component$(() => { const price = useSignal(100); const taxRate = useSignal(0.1); const total = useComputed$(() => { return price.value * (1 + taxRate.value); }); return ( <div> <p>单价:¥{price.value}</p> <p>税率:{taxRate.value * 100}%</p> <p>总价:¥{total.value.toFixed(2)}</p> </div> ); });

useComputed$ 本质上是一个只读的 Signal,你不能直接修改它的 .value,只能通过改变依赖项来间接触发更新。适合用在过滤列表、格式化输出、聚合计算等场景。

useContext:跨组件共享状态

当状态需要在组件树的多个层级间共享时,Qwik 提供了 Context 机制。先用 createContext 定义上下文,再用 useContextProvider 提供值,子组件通过 useContext 消费。

tsx
import { component$, createContext, useContextProvider, useContext } from '@builder.io/qwik'; // 定义 Context 的类型和默认值 const ThemeContext = createContext<string>('light'); export const App = component$(() => { // 在顶层组件提供值 useContextProvider(ThemeContext, 'dark'); return <ChildComponent />; }); export const ChildComponent = component$(() => { // 在任意子组件消费 const theme = useContext(ThemeContext); return <p>当前主题:{theme}</p>; });

与 React Context 的关键区别:Qwik 的 Context 值不需要是响应式的,但如果传入的是 useStoreuseSignal 创建的响应式对象,消费组件同样会自动更新。

useTask$:副作用与状态同步

useTask$ 是 Qwik 中处理副作用的 hook,类似于 React 的 useEffect,但工作机制不同。它在服务端和客户端都会执行,可以通过 track 函数监听状态变化。

tsx
import { component$, useSignal, useTask$ } from '@builder.io/qwik'; export const SearchBox = component$(() => { const keyword = useSignal(''); const results = useSignal<string[]>([]); useTask$(({ track }) => { const query = track(() => keyword.value); // 当 keyword 变化时,重新计算搜索结果 if (query.length < 2) { results.value = []; return; } // 模拟搜索逻辑 results.value = ['结果1', '结果2'].filter((r) => r.includes(query)); }); return ( <div> <input value={keyword.value} onInput$={(ev) => (keyword.value = (ev.target as HTMLInputElement).value)} /> <ul> {results.value.map((r, i) => <li key={i}>{r}</li>)} </ul> </div> ); });

useTask$ 适合用在:根据某个状态变化去修改另一个状态、发起网络请求、操作 DOM 等场景。注意它不返回值,如果需要返回派生状态,应该用 useComputed$

useResource$:异步数据加载

useResource$ 专门处理异步数据获取,内置了 pending、resolved、rejected 三种状态的管理,配合 Resource 组件可以方便地渲染不同状态。

tsx
import { component$, useSignal, useResource$, Resource } from '@builder.io/qwik'; export const UserProfile = component$(() => { const userId = useSignal(1); const userResource = useResource$(async ({ track }) => { const id = track(() => userId.value); const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`); if (!res.ok) throw new Error('加载失败'); return res.json(); }); return ( <div> <button onClick$={() => userId.value++}>下一个用户</button> <Resource value={userResource} onPending={() => <p>加载中...</p>} onRejected={() => <p>加载失败,请重试</p>} onResolved={(user) => ( <div> <p>姓名:{user.name}</p> <p>邮箱:{user.email}</p> </div> )} /> </div> ); });

useResource$track 函数让它能在依赖变化时自动重新获取数据,配合 <Resource> 组件可以清晰地处理三种 UI 状态,避免手动管理 loading/error 状态的样板代码。

Qwik 与 React 状态管理的本质区别

理解 Qwik 状态管理,关键在于理解它与 React 的根本差异:

React 的状态绑定在组件实例上,组件卸载状态就消失,客户端需要通过水合重建组件树和状态。

Qwik 的状态是独立的,它与创建它的组件解耦。状态被序列化到 HTML 的 <script type="qwik/json"> 中,浏览器无需执行任何组件代码就能恢复状态。这意味着:

  • 状态可以在组件间自由传递,不受组件树层级限制
  • 只有用户交互时才会下载对应的处理代码(懒执行)
  • 状态的可序列化是硬性要求,不能存储函数、DOM 引用等不可序列化的值

选择合适的状态管理方式

场景推荐方式
单个原始值(计数器、开关)useSignal
复杂对象或数组(表单、列表)useStore
依赖其他状态的派生值useComputed$
跨组件共享数据useContext + useContextProvider
响应状态变化执行副作用useTask$
异步数据获取与状态管理useResource$

在实际开发中,这些 API 往往组合使用。比如用 useStore 管理全局状态,通过 useContextProvider 注入组件树,子组件用 useContext 消费,再用 useTask$ 响应状态变化执行副作用,用 useResource$ 加载远程数据。Qwik 的编译器会自动处理细粒度更新和状态序列化,开发者只需关注业务逻辑本身。

标签:Qwik