Qwik 状态管理怎么用?从 useSignal 到 useResource
Qwik 的状态管理围绕一个核心理念:可恢复性(Resumability)。与传统框架在客户端重新执行组件代码来恢复状态不同,Qwik 在服务端渲染时就将状态序列化到 HTML 中,浏览器可以直接从序列化点恢复执行,无需水合(Hydration)。这个设计决策深刻影响了 Qwik 状态管理 API 的形态。
useSignal:管理原始值
useSignal 是最轻量的响应式状态,适合存储数字、字符串、布尔值等原始类型。它返回一个包含 .value 属性的对象,修改 .value 就能触发更新。
tsximport { 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。它会自动追踪对象属性的变化,同样做到细粒度更新。
tsximport { 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$。它会在依赖变化时自动重新计算,未变化时返回缓存值。
tsximport { 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 消费。
tsximport { 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 值不需要是响应式的,但如果传入的是 useStore 或 useSignal 创建的响应式对象,消费组件同样会自动更新。
useTask$:副作用与状态同步
useTask$ 是 Qwik 中处理副作用的 hook,类似于 React 的 useEffect,但工作机制不同。它在服务端和客户端都会执行,可以通过 track 函数监听状态变化。
tsximport { 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 组件可以方便地渲染不同状态。
tsximport { 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 的编译器会自动处理细粒度更新和状态序列化,开发者只需关注业务逻辑本身。