State

QDS components have two types of state:
- Signal based (two-way binding)
- Value based (one-way binding)
Signal based state is the default and recommended way for consumers to manage state, however consumers of the library may find value based more intuitive.
Signal based (reactive state)
Consumers can pass a signal directly to the component to manage the component's state.
export const ExampleComp = component$(() => {
const selectedValueSig = signal(null);
return (
<Select.Root bind:value={selectedValueSig}>
{...}
</Select.Root>
)
})
bind:value
under the hood is really just a prop. The convention is to use bind:
for signals, to stay consistent with Qwik's API (which was inspired by frameworks like Svelte and Angular).
Binds in Qwik
In Qwik, you can use bind:value
or bind:checked
to pass your own signal state to update form controls.
Hello
import { component$, useSignal, useStylesScoped$ } from "@builder.io/qwik";
export default component$(() => {
useStylesScoped$(styles);
const myTextSignal = useSignal("Hello");
return (
<>
<input bind:value={myTextSignal} />
<p>{myTextSignal.value}</p>
</>
);
});
import styles from "./two-way.css?inline";
This is two-way data binding. Unlike traditional two-way binding that can be performance-heavy in other frameworks, Qwik's signals make it efficient by updating only what needs to change, with no unnecessary re-renders.
useBoundSignal
Qwik Design System provides a hook to combine signals to one source of truth.
export const SelectRoot = component$((props: SelectRootProps) => {
const {
"bind:value": givenSelectedValueSig,
} = props;
const selectedValueSig = useBoundSignal(givenSelectedValueSig, "Jim");
return (
<div>
<Slot />
</div>
)
})
The first argument is the signal that the consumer passed in. The second argument is the initial value of the signal.
In this case we've provided the initial value of "Jim"
to the signal, but it could be an initial value from a prop passed by the consumer as well from value based state.
Checked: false
import { component$, useSignal, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const isChecked = useSignal(false);
return (
<>
<Checkbox.Root bind:checked={isChecked}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
<p>Checked: {isChecked.value ? "true" : "false"}</p>
<button
type="button"
onClick$={() => {
isChecked.value = true;
}}
>
I check the checkbox above
</button>
</>
);
});
// example styles
import styles from "./checkbox.css?inline";
Above is an example of a component that uses the useBoundSignal
hook to combine the signal from the consumer with our internal signal.
Notice that whenever toggling the checkbox or programmatically changing the signal, the signal value of the checkbox is updated both internally and externally.
Two-way store properties
Stores can also be two-way bound, in this case we use the useStoreSignal
hook to create a signal from the store.
Checked signal: false
Checked store: false
import {
type Signal,
component$,
useSignal,
useStore,
useStyles$,
useTask$
} from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const myStore = useStore({
isChecked: false
});
const isChecked = useStoreSignal<boolean | "mixed">(myStore, "isChecked");
return (
<>
<Checkbox.Root bind:checked={isChecked}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
<p>Checked signal: {isChecked.value ? "true" : "false"}</p>
<p>Checked store: {myStore.isChecked ? "true" : "false"}</p>
<button
type="button"
onClick$={() => {
isChecked.value = true;
}}
>
I check the checkbox above
</button>
</>
);
});
// example styles
import styles from "./checkbox.css?inline";
export function useStoreSignal<T>(
store: Record<string, unknown>,
key: keyof typeof store
): Signal<T> {
const internalSig = useSignal<T>(store[key] as T);
useTask$(function setStoreValue({ track }) {
track(internalSig);
store[key] = internalSig.value;
});
useTask$(function getStoreValue({ track }) {
track(() => store[key]);
internalSig.value = store[key] as T;
});
return internalSig;
}
Now the store property is updated whenever the internal signal is updated and vice versa. This can be useful if you want to use the store as a source of truth for the component's state.
Value based
Value based state is when the value is passed directly to the component as a prop.
For example, the Select.Root
component has a value
prop that can be used to set the selected value of the select.
export const UserSelect = component$((props: UserSelectProps) => {
return (
<Select.Root value="Jim">
{...}
</Select.Root>
)
})
Types of value based state
Value based state can accept literal values, signal reads, stores, and anything under the sun that resolves to a value.
Literal value:
<Select.Root value="Jim">
{...}
</Select.Root>
Signal read:
<Select.Root value={selectedValueSig.value}>
{...}
</Select.Root>
Store read:
<Select.Root value={myStore.property}>
{...}
</Select.Root>
Understanding reactivity
Keep in mind, that value based state is not reactive. This means that the component receiving the value has no idea that you're using a signal read or store property.
This is especially evident when you're passing state through context, where consumers expect to be able to update the state and see the changes.
User disabled: true
import {
type PropsOf,
Slot,
component$,
createContextId,
useContext,
useContextProvider,
useSignal,
useStyles$
} from "@builder.io/qwik";
export default component$(() => {
const isDisabled = useSignal(true);
useStyles$(`
[data-example-state-button]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`);
return (
<>
<Root disabled={isDisabled.value}>
<Child data-example-state-button>I am a button!</Child>
</Root>
<button type="button" onClick$={() => (isDisabled.value = !isDisabled.value)}>
Toggle disabled
</button>
<p>User disabled: {isDisabled.value ? "true" : "false"}</p>
</>
);
});
export const exampleContextId = createContextId<ExampleContext>("example-context");
type ExampleContext = {
disabled?: boolean;
};
type RootProps = PropsOf<"div"> & ExampleContext;
const Root = component$((props: RootProps) => {
const { disabled } = props;
const context: ExampleContext = {
disabled
};
useContextProvider(exampleContextId, context);
return (
<div>
<Slot />
</div>
);
});
const Child = component$((props: PropsOf<"button">) => {
const context = useContext(exampleContextId);
return (
<button disabled={context.disabled} {...props}>
<Slot />
</button>
);
});
In the example above, notice how the button doesn't update when we toggle the state. This is because context values are not reactive - they're just snapshots at render time.
Qwik does not re-render components when reactive values change, only the functions that depend on the reactive values run again.
Getting the latest values
To get the latest values from context, you need to turn value based state into reactive state.
We can do this by tracking whenever the specific property changes on the props object.
User disabled: true
import {
type PropsOf,
type Signal,
Slot,
component$,
createContextId,
useComputed$,
useContext,
useContextProvider,
useSignal,
useStyles$
} from "@builder.io/qwik";
export default component$(() => {
const isDisabled = useSignal(true);
useStyles$(`
[data-example-state-button]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`);
return (
<>
<Root disabled={isDisabled.value}>
<Child data-example-state-button>I am a button!</Child>
</Root>
<button type="button" onClick$={() => (isDisabled.value = !isDisabled.value)}>
Toggle disabled
</button>
<p>User disabled: {isDisabled.value ? "true" : "false"}</p>
</>
);
});
export const exampleContextId = createContextId<ExampleContext>("example-context");
type ExampleContext = {
isDisabledSig: Signal<boolean>;
};
const Root = component$((props: PropsOf<"div"> & { disabled: boolean }) => {
const isDisabledSig = useComputed$(() => props.disabled);
const context: ExampleContext = {
isDisabledSig
};
useContextProvider(exampleContextId, context);
return (
<div>
<Slot />
</div>
);
});
const Child = component$((props: PropsOf<"button">) => {
const context = useContext(exampleContextId);
return (
<button disabled={context.isDisabledSig.value} {...props}>
<Slot />
</button>
);
});
Another example:
export const SelectRoot = component$((props: SelectRootProps) => {
const isDisabledSig = useSignal(props.disabled);
const context: ExampleContext = {
isDisabledSig
}
useContextProvider(exampleContextId, context);
return {...}
})
export const SelectTrigger = component$((props: SelectTriggerProps) => {
const context = useContext(exampleContextId);
return <button disabled={context.isDisabledSig.value}>
<Slot />
</button>
})
Why useComputed$?
useComputed$
will automatically track any reactive values (or props) that are used inside of it. The returned signal is a read-only signal that will update whenever the tracked reactive values change.
It is the equivalent of:
const isDisabledSig = useSignal(props.disabled);
useTask$(({ track }) => {
isDisabledSig.value = track(() => props.disabled);
})
You specifically need to track the property on the props object, similar to how you would track a store property.
Qwik tracks reactivity through proxies. Props are reactive when used directly, but when values (not signals) are passed through context (or destructured), this reactive connection is lost. Always pass reactive state through context or explicitly track prop changes to maintain reactivity across context consumers.