diff --git a/docs/demos/devtools.tsx b/docs/demos/devtools.tsx
new file mode 100644
index 000000000..b6d894fe2
--- /dev/null
+++ b/docs/demos/devtools.tsx
@@ -0,0 +1,141 @@
+import { useSignal } from "@preact/signals";
+import { Show, For } from "./utils";
+import { computed, signal } from "@preact/signals-core";
+
+type TodoModel = {
+ id: number;
+ get text(): string;
+ get done(): boolean;
+ toggle(): void;
+ updateText(newText: string): void;
+};
+
+const createTodoModel = (id: number, input: string): TodoModel => {
+ const text = signal(input, { name: `todo-${id}-text` });
+ const done = signal(false, { name: `todo-${id}-done` });
+
+ return {
+ id,
+ get text() {
+ return text.value;
+ },
+ get done() {
+ return done.value;
+ },
+ toggle() {
+ done.value = !done.value;
+ },
+ updateText(newText: string) {
+ text.value = newText;
+ },
+ };
+};
+
+const todosModel = (() => {
+ const todos = signal(
+ [
+ createTodoModel(1, "Learn Preact Signals"),
+ createTodoModel(2, "Build something fun"),
+ ],
+ { name: "todos-list" }
+ );
+
+ const allDone = computed(
+ () => todos.value.length > 0 && todos.value.every(t => t.done),
+ { name: "all-done" }
+ );
+
+ return {
+ todos,
+ allDone,
+ add(text: string) {
+ todos.value = [...todos.value, createTodoModel(Date.now(), text)];
+ },
+ };
+})();
+
+export default function DevToolsDemo() {
+ return (
+
+
DevTools Demo
+
+
+
+
+ );
+}
+
+function TodosList() {
+ const newTodoText = useSignal("", { name: "new-todo-text" });
+
+ return (
+
+
Todos
+
+
+
+
+
+ All todos are done! 🎉
+
+
+
+ );
+}
+
+function TodoItem({ todo }: { todo: TodoModel }) {
+ const isEditing = useSignal(false, { name: `todo-${todo.id}-isEditing` });
+ return (
+
+ todo.toggle()}
+ />
+ {todo.text}}>
+ todo.updateText(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
diff --git a/docs/demos/index.tsx b/docs/demos/index.tsx
index 7332f81e5..63417c3e9 100644
--- a/docs/demos/index.tsx
+++ b/docs/demos/index.tsx
@@ -13,6 +13,7 @@ const demos = {
Sum,
GlobalCounter,
DuelingCounters,
+ Devtools: lazy(() => import("./devtools")),
Nesting: lazy(() => import("./nesting")),
Animation: lazy(() => import("./animation")),
Bench: lazy(() => import("./bench")),
diff --git a/docs/demos/utils.ts b/docs/demos/utils.ts
new file mode 100644
index 000000000..785d447b9
--- /dev/null
+++ b/docs/demos/utils.ts
@@ -0,0 +1,71 @@
+import { ReadonlySignal, Signal } from "@preact/signals-core";
+import { useSignal } from "@preact/signals";
+import { Fragment, createElement, JSX } from "preact";
+import { useMemo } from "preact/hooks";
+
+interface ShowProps {
+ when: Signal | ReadonlySignal;
+ fallback?: JSX.Element;
+ children: JSX.Element | ((value: NonNullable) => JSX.Element);
+}
+
+export function Show(props: ShowProps): JSX.Element | null {
+ const value = props.when.value;
+ if (!value) return props.fallback || null;
+ return typeof props.children === "function"
+ ? props.children(value)
+ : props.children;
+}
+
+interface ForProps {
+ each:
+ | Signal>
+ | ReadonlySignal>
+ | (() => Signal> | ReadonlySignal>);
+ fallback?: JSX.Element;
+ children: (value: T, index: number) => JSX.Element;
+}
+
+export function For(props: ForProps): JSX.Element | null {
+ const cache = useMemo(() => new Map(), []);
+ let list = (
+ (typeof props.each === "function" ? props.each() : props.each) as Signal<
+ Array
+ >
+ ).value;
+
+ if (!list.length) return props.fallback || null;
+
+ const items = list.map((value, key) => {
+ if (!cache.has(value)) {
+ cache.set(value, props.children(value, key));
+ }
+ return cache.get(value);
+ });
+
+ return createElement(Fragment, null, items);
+}
+
+export function useLiveSignal(
+ value: Signal | ReadonlySignal
+): Signal | ReadonlySignal> {
+ const s = useSignal(value);
+ if (s.peek() !== value) s.value = value;
+ return s;
+}
+
+export function useSignalRef(value: T): Signal & { current: T } {
+ const ref = useSignal(value) as Signal & { current: T };
+ if (!("current" in ref))
+ Object.defineProperty(ref, "current", refSignalProto);
+ return ref;
+}
+const refSignalProto = {
+ configurable: true,
+ get(this: Signal) {
+ return this.value;
+ },
+ set(this: Signal, v: any) {
+ this.value = v;
+ },
+};