Choosing Between useSignal and useStore in Qwik
A practical guide for Qwik developers on when to use useSignal versus useStore for local component state, compare their reactivity and serialization traits, see a concrete component example, and verify correct behavior.
13 Apr 2026, 00:07 UTC

Decision Context and Constraints
When building a Qwik component that needs local reactive state, you must decide whether to use useSignal or useStore. The choice affects how the state is serialized during server‑side rendering (SSR) and client‑side resumption, and it influences the amount of reactivity overhead.
Key Constraints
- State must survive SSR and client hydration without extra work.
- Reactivity granularity should match the data shape to avoid unnecessary renders.
- Serialization format must be stable across Qwik versions.
Option Comparison
| Feature | useSignal | useStore |
|---|---|---|
| Reactivity granularity | Primitive or immutable values; each change replaces the whole value. | Objects or arrays; each property can trigger updates independently. |
| Serialization | Simple primitive (number, string, boolean) – no deep proxy. | Plain object – properties are tracked after hydration. |
| Update style | Replace via .value (explicit). | Mutate in place (e.g., user.age++) or assign directly. |
| Overhead | Low proxy cost for primitives; no deep tracking. | Higher overhead due to deep property tracking. |
| Risk of broken reactivity | Low – you must replace the entire value. | Higher – destructuring outside reactive context can capture a snapshot. |
Trade‑offs Explained
useSignal is ideal for simple counters, flags, or any value that can be treated as a whole. Because it stores a single primitive, the framework can serialize it directly and avoid the deep‑proxy machinery that would otherwise run on every render. The downside is that you must write count.value++ each time, which can feel verbose for nested data.
useStore shines when you need fine‑grained updates inside an object, such as a profile form with separate name and age fields. The store keeps a plain object representation, so after hydration the component can read user.name without extra spreading. However, if you destructure const {name}=user outside of JSX or a useTask$ task, you capture a non‑reactive copy and later changes won’t propagate.
The hybrid pattern recommended by the Qwik team is to keep independent scalars in signals and group related mutable fields in a store. This reduces proxy overhead while preserving concise updates for nested structures.
Concrete Implementation Example
import { useSignal, useStore } from '@builder.io/qwik';
export default function Profile() {
// Primitive scalar – fine‑grained, replace whole value
const isSubscribed = useSignal(false);
// Grouped mutable fields – deep reactivity
const user = useStore({
name: '',
age: 0,
email: '',
});
return (
isSubscribed.value = !isSubscribed.value}>
{isSubscribed.value ? 'Subscribed' : 'Subscribe'}
(user.name = e.target.value)}
/>
(user.age = Number(e.target.value))}
/>
Save
);
}
In this component, isSubscribed is a signal because it is a single boolean that can be toggled by replacing its .value. The user object lives in a store, allowing direct assignment to user.name and user.age without recreating the whole object.
Validation and Verification
- Run the Qwik dev server (
npm run dev) and open the component in a browser. - Inspect the generated HTML source (view page source) and locate the serialized state payload. You should see the signal value as a plain primitive (e.g.,
true) and the store value as a JSON‑like object (e.g.,{\"name\":\"\",\"age\":0,\"email\":\"\"}). - Use browser devtools to mutate a store property (e.g., change
user.age) and observe that only the affected DOM nodes re‑render, while the signal change updates the button text without a full component refresh. - Check the Qwik dev overlay for any warnings about reactivity capture or serialization mismatches; there should be none if the patterns are followed.
Rollback Considerations
If you later decide to replace a store with a signal, you must recreate the state object because signals expect a single value. Conversely, converting a signal to a store requires initializing the store with the current signal value and then replacing updates with direct property assignments. Since the operation changes component state, a manual rollback is required: revert the state definition and, if needed, re‑apply the original data shape.
Summary
Choose useSignal for simple, immutable primitives where replacing the whole value is acceptable and you want minimal reactivity overhead. Choose useStore for complex objects or arrays where property‑level reactivity matters, keeping in mind the risk of broken updates when destructuring. A hybrid approach—signals for independent scalars and stores for grouped mutable fields—offers the best balance of performance and developer ergonomics.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.