useSignal
The useSignal hook creates a signal that persists across component re-renders, similar to React's useState.
Signatures
ts
function useSignal(): Signal<unknown>;
function useSignal(value: null | undefined): Signal<unknown>;
function useSignal<T>(initializer: () => T): Signal<T>;
function useSignal<T extends {}>(value: Exclude<T, Function>): Signal<T>;Basic Usage
tsx
import { useSignal, reactor } from '@signalis/react';
const Counter = () => {
const count = useSignal(0);
return (
<div>
<p>Count: {count.value}</p>
<button onClick={() => count.value++}>Increment</button>
<button onClick={() => count.value--}>Decrement</button>
</div>
);
};
export default reactor(Counter);Lazy Initialization
For expensive initial values, use a function initializer:
tsx
import { useSignal, reactor } from '@signalis/react';
const DataProcessor = reactor(() => {
// ✅ Expensive computation only runs once
const data = useSignal(() => processLargeDataset());
return <div>Processed: {data.value.length} items</div>;
});Note: Functions are always treated as initializers (matching React's useState pattern).
Comparison with useState
Traditional useState
tsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount((c) => c + 1)}>+ (functional)</button>
</div>
);
}With useSignal
tsx
import { useSignal, reactor } from '@signalis/react';
const Counter = reactor(() => {
const count = useSignal(0);
return (
<div>
<p>{count.value}</p>
<button onClick={() => count.value++}>+</button>
<button onClick={() => count.value++}>+ (same syntax)</button>
</div>
);
});Use Cases
Form Inputs
tsx
import { useSignal, reactor } from '@signalis/react';
import { FormEvent } from 'react';
const LoginForm = reactor(() => {
const email = useSignal('');
const password = useSignal('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
login(email.value, password.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email.value} onChange={(e) => (email.value = e.target.value)} />
<input
type="password"
value={password.value}
onChange={(e) => (password.value = e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
});Toggle State
tsx
import { useSignal, reactor } from '@signalis/react';
import { ReactNode } from 'react';
const Expandable = reactor(({ children }: { children: ReactNode }) => {
const isOpen = useSignal(false);
return (
<div>
<button onClick={() => (isOpen.value = !isOpen.value)}>
{isOpen.value ? 'Collapse' : 'Expand'}
</button>
{isOpen.value && <div>{children}</div>}
</div>
);
});List Management
tsx
import { useSignal, reactor } from '@signalis/react';
const TodoList = reactor(() => {
const todos = useSignal<Array<string>>([]);
const input = useSignal('');
const addTodo = () => {
if (input.value.trim()) {
todos.value = [...todos.value, input.value];
input.value = '';
}
};
const removeTodo = (index: number) => {
todos.value = todos.value.filter((_, i) => i !== index);
};
return (
<div>
<input
value={input.value}
onChange={(e) => (input.value = e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.value.map((todo, i) => (
<li key={i}>
{todo}
<button onClick={() => removeTodo(i)}>×</button>
</li>
))}
</ul>
</div>
);
});Inside vs Outside Components
Inside Component (useSignal)
Signal is local to the component instance:
tsx
import { useSignal, reactor } from '@signalis/react';
const Counter = reactor(() => {
const count = useSignal(0); // New signal per component instance
return <div>{count.value}</div>;
});
// Each Counter has its own count
<>
<Counter /> {/* count = 0 */}
<Counter /> {/* count = 0 (different signal) */}
</>;Outside Component (createSignal)
Signal is shared across all instances:
tsx
import { createSignal, reactor } from '@signalis/react';
const sharedCount = createSignal(0);
const Counter = reactor(() => {
return <div>{sharedCount.value}</div>;
});
// All Counters share the same count
<>
<Counter /> {/* Shows shared count */}
<Counter /> {/* Shows same shared count */}
</>;Persisting Across Renders
useSignal uses useMemo internally to ensure the signal is only created once:
tsx
import { useSignal, reactor } from '@signalis/react';
const Component = reactor(() => {
const count = useSignal(0);
// Same signal instance across all renders
console.log('Rendering...');
return (
<div>
<p>{count.value}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
});TypeScript
useSignal is fully type-safe:
tsx
import { useSignal, reactor } from '@signalis/react';
const Counter = reactor(() => {
const count = useSignal(0); // Signal<number>
const name = useSignal('Jane'); // Signal<string>
const user = useSignal({ name: 'Jane', age: 30 }); // Signal<{ name: string; age: number }>
count.value = 'hello'; // ❌ Type error
name.value = 42; // ❌ Type error
user.value = { name: 'Jane' }; // ❌ Type error (missing age)
});Common Patterns
Derived State
tsx
import { useSignal, useDerived, reactor } from '@signalis/react';
const ShoppingCart = reactor(() => {
const items = useSignal<Array<{ price: number; quantity: number }>>([]);
const total = useDerived(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0),
);
return (
<div>
<p>Total: ${total.value.toFixed(2)}</p>
</div>
);
});Validation
tsx
import { useSignal, useDerived, reactor } from '@signalis/react';
const Form = reactor(() => {
const email = useSignal('');
const isValid = useDerived(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value));
return (
<div>
<input value={email.value} onChange={(e) => (email.value = e.target.value)} />
{!isValid.value && email.value && <p>Invalid email</p>}
</div>
);
});Gotchas
Don't Destructure
tsx
// ❌ Wrong - loses reactivity
const { value } = useSignal(0);
// ✅ Correct - keep the signal
const count = useSignal(0);
const value = count.value; // Read when neededImmutable Updates for Objects
tsx
const user = useSignal({ name: 'Jane', age: 30 });
// ❌ Won't trigger updates
user.value.name = 'Janet';
// ✅ Create new object
user.value = { ...user.value, name: 'Janet' };Function Initialization
tsx
// ❌ This treats the function as an initializer
const fn = useSignal(() => console.log('hello'));
// fn.value is undefined (return value of the function)
// ✅ Wrap it if you want to store a function
const fn = useSignal(() => () => console.log('hello'));
// fn.value is the functionSee Also
- reactor - Making components reactive
- useDerived - Computed values
- useSignalEffect - Effects with signals
- Signals - Understanding signals