Qwik JS Tutorial – Part 5: State Management in Qwik
Introduction
State management is one of the most important aspects of building modern web applications. Whether you are handling form inputs, UI toggles, API responses, or complex user interactions, managing state efficiently directly impacts performance and maintainability.
In Qwik, state management is designed with performance and resumability in mind. Instead of relying on heavy client-side reactivity systems that require full hydration, Qwik provides lightweight, serializable primitives that work seamlessly with its resumable architecture.
In this tutorial, we will explore:
- How state works in Qwik
- The difference between
useSignal()anduseStore() - Local vs shared state
- Reactive updates
- Best practices for scalable applications
This guide is written for beginners as well as developers transitioning from other frameworks.
Understanding Reactivity in Qwik
Qwik uses a fine-grained reactivity model. Instead of re-rendering entire components, Qwik updates only the parts of the UI that depend on specific reactive values.
This approach reduces unnecessary DOM updates and aligns with Qwik’s goal of minimizing JavaScript execution.
In Qwik, reactivity is primarily handled using:
useSignal()useStore()
Both are designed to be serializable and compatible with server-side rendering.
Using useSignal() – Simple Reactive State
useSignal() is used to manage simple, single-value state such as:
- Numbers
- Strings
- Booleans
- Single objects
Example: Counter with useSignal
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<div>
<p>Count: {count.value}</p>
<button onClick$={() => count.value++}>Increment</button>
</div>
);
});How It Works
useSignal(0)creates a reactive value- The actual value is stored in
count.value - Updating
count.valueautomatically updates the UI
Only the part of the DOM that depends on count.value is updated.
When to Use useSignal()
Use useSignal() when:
- You need simple reactive values
- Managing form inputs
- Handling toggles (open/close)
- Tracking small UI state
It is lightweight and ideal for most small state needs.
Using useStore() – Managing Complex State
useStore() is useful when managing structured or nested state.
Example: User Object
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const user = useStore({
name: 'John',
age: 25,
isLoggedIn: false
});
return (
<div>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<button onClick$={() => user.isLoggedIn = true}>
Login
</button>
</div>
);
});Key Characteristics
- Reactive object
- Supports nested properties
- Automatically tracks property-level changes
Only properties that change trigger updates.
useSignal vs useStore – Key Differences
| Feature | useSignal | useStore |
|---|---|---|
| Best for | Single values | Complex objects |
| Structure | .value required | Direct property access |
| Performance | Very lightweight | Slightly heavier than signal |
| Nested state | Not ideal | Well-suited |
Choosing the right primitive improves readability and performance.
Reactive Rendering in Qwik
Qwik tracks dependencies automatically.
If a component references a signal or store property, Qwik ensures that only the affected DOM nodes update when that value changes.
This fine-grained rendering reduces:
- Unnecessary re-renders
- Memory usage
- CPU usage
This design aligns with Qwik’s resumable architecture.
Local State vs Shared State
Local State
Local state lives inside a component using useSignal() or useStore().
Example use cases:
- Modal open/close state
- Input field value
- Local UI interactions
Shared State
Shared state can be lifted up to a parent component and passed down via props.
Qwik also supports context APIs for larger applications when multiple components need access to shared state.
Choosing between local and shared state depends on your application structure.
State and Resumability
One important requirement in Qwik is that state must be serializable.
Because Qwik resumes execution on the client, it needs to:
- Serialize state on the server
- Transfer it inside HTML
- Restore it on the client
Avoid storing:
- DOM references
- Non-serializable class instances
- Complex closures inside state
Keeping state serializable ensures smooth resumability.
Handling Forms with State
Forms are common use cases for reactive state.
Example: Controlled Input
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const name = useSignal('');
return (
<div>
<input
value={name.value}
onInput$={(e) => name.value = e.target.value}
/>
<p>Hello {name.value}</p>
</div>
);
});This keeps the UI synchronized with state changes.
Performance Best Practices
1. Prefer Signals for Simple Values
Avoid using useStore() when a signal is sufficient.
2. Avoid Deeply Nested Objects
Deep nesting may make state harder to maintain.
3. Keep State Minimal
Only store what is required for rendering.
4. Avoid Large Mutable Objects
Large mutable structures can increase complexity.
5. Think Server-First
Since Qwik emphasizes SSR, ensure your state logic works in both server and client environments.
Common Mistakes to Avoid
- Forgetting
.valuewhen using signals - Storing non-serializable objects
- Overusing global state
- Mutating deeply nested state unnecessarily
- Mixing unrelated concerns into a single store
Clear separation of concerns improves maintainability.
Real-World Example: Toggle + Counter
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const isOpen = useSignal(false);
const count = useSignal(0);
return (
<div>
<button onClick$={() => isOpen.value = !isOpen.value}>
Toggle
</button>
{isOpen.value && (
<div>
<p>Count: {count.value}</p>
<button onClick$={() => count.value++}>
Increase
</button>
</div>
)}
</div>
);
});This example demonstrates fine-grained updates where only affected UI fragments re-render.
Summary
In this part of the Qwik JS tutorial, we explored:
- How state management works in Qwik
- The role of
useSignal()anduseStore() - Differences between simple and complex state
- Local vs shared state strategies
- Performance best practices
- Common pitfalls to avoid
Qwik’s state management is intentionally lightweight and designed to support resumability and performance optimization.
In the next part, we will explore routing in Qwik and understand how file-based navigation works in real applications.
