ワンクリックで
signals
Guidelines for state management with createModel, Show, and For using Preact Signals.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Guidelines for state management with createModel, Show, and For using Preact Signals.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | signals |
| description | Guidelines for state management with createModel, Show, and For using Preact Signals. |
Guidelines for state management with createModel, Show, and For using @preact/signals and @preact/signals/utils.
Signals are reactive primitives from @preact/signals that provide fine-grained reactivity. @preact/signals/utils adds ergonomic helpers like Show and For for declarative rendering, while createModel comes from @preact/signals for model-driven state.
createModelimport { computed, createModel, signal } from '@preact/signals';
export const CounterModel = createModel(() => {
const count = signal(0);
const name = signal<string | null>(null);
const items = signal<Item[]>([]);
const hasItems = computed(() => items.value.length > 0);
const increment = () => {
count.value++;
};
const addItem = (item: Item) => {
items.value = [...items.value, item];
};
return {
count,
name,
items,
hasItems,
increment,
addItem,
};
});
import { useModel } from '@preact/signals';
const model = useModel(CounterModel);
// Read with .value
<span>{model.count.value}</span>
// Write with .value assignment
<button onClick={model.increment}>Increment</button>
<button onClick={() => (model.name.value = 'Alice')}>Set Name</button>
Show and Forimport { useModel } from '@preact/signals';
import { For, Show } from '@preact/signals/utils';
function ItemList() {
const model = useModel(CounterModel);
return (
<>
<Show when={model.hasItems} fallback={<p>No items yet.</p>}>
<ul>
<For each={model.items}>
{(item) => <li key={item.value.id}>{item.value.label}</li>}
</For>
</ul>
</Show>
</>
);
}
UI state that changes frequently
State used in event handlers
State derived from async operations
Toggle state (FAQ accordion model):
const FAQItemModel = createModel(() => {
const open = signal(false);
const toggle = () => {
open.value = !open.value;
};
return { open, toggle };
});
function FAQItem({ question, answer }) {
const model = useModel(FAQItemModel);
return (
<div>
<button onClick={model.toggle}>
{question}
</button>
<Show when={model.open}>
<div>{answer}</div>
</Show>
</div>
);
}
Auth state model:
const AuthModel = createModel(() => {
const isAuthenticated = signal<boolean | null>(null);
const checkingAuth = signal(true);
const loadSession = async () => {
const res = await authClient.getSession();
isAuthenticated.value = !!res.data?.user;
checkingAuth.value = false;
};
return { isAuthenticated, checkingAuth, loadSession };
});
const auth = useModel(AuthModel);
useEffect(() => {
auth.loadSession();
}, []);
Upload progress:
const UploadModel = createModel(() => {
const isDragging = signal(false);
const isUploading = signal(false);
const uploadError = signal<string | null>(null);
const uploadProgress = signal(0);
return { isDragging, isUploading, uploadError, uploadProgress };
});
Window drag state:
const WindowModel = createModel(() => {
const isDragging = signal(false);
const position = signal({ x: 0, y: 0 });
const size = signal({ width: 800, height: 600 });
return { isDragging, position, size };
});
Encapsulate complex signal logic in models:
// models/WindowDragModel.ts
export const WindowDragModel = createModel(({ defaultWidth, defaultHeight }) => {
const isDragging = signal(false);
const position = signal({ x: 0, y: 0 });
const size = signal({ width: defaultWidth, height: defaultHeight });
// Event handlers that mutate signals...
return {
isDragging,
position,
size,
handleMouseDown,
};
});
// component
const drag = useModel(WindowDragModel);
| Use Case | Prefer |
|---|---|
| Simple boolean toggle | signal in createModel |
| Object with multiple fields updated together | signal in createModel |
| State updated in event handlers | signal in createModel |
| State passed deep into children | createModel + Show/For |
| State that rarely changes | Either works |
| State managed by external library | Follow library conventions |
Always provide types for non-obvious signal values:
const user = signal<User | null>(null);
const status = signal<'idle' | 'loading' | 'error'>('idle');
Signals don't need functional updates like useState:
// With signals - direct mutation is fine
onClick={() => count.value++}
onClick={() => items.value = [...items.value, newItem]}
// Reading current value in handler
onClick={() => {
if (count.value < 10) {
count.value++;
}
}}
Show and For for RenderingPrefer Show and For for common conditional and list rendering:
<Show when={isLoading}>
<Spinner />
</Show>
<Show when={error}>
<ErrorMessage>{error.value}</ErrorMessage>
</Show>
<For each={items}>
{(item) => <Item key={item.value.id} {...item.value} />}
</For>
You can also use a callback in the
whenandeachto simulate acomputedsignal if needed.
Signals work alongside traditional hooks:
useEffect(() => {
if (isOpen) {
checkingAuth.value = true;
fetchData().then(data => {
result.value = data;
checkingAuth.value = false;
});
}
}, [isOpen]);
Prefer local signals over global state. Only lift state when multiple components need to share it.
.value// Wrong - won't update
{isOpen && <Modal />}
// Correct
{isOpen.value && <Modal />}
When using Show, pass a signal or computed signal to when:
<Show when={isOpen}>
<Modal />
</Show>
// Wrong - won't trigger update
position.value.x = 100;
// Correct
position.value = { ...position.value, x: 100 };
// or
position.value = { x: 100, y: position.value.y };