Top Related Projects
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
web development for the rest of us
Deliver web apps with confidence 🚀
Simple, scalable state management.
Quick Overview
Preact Signals is a state management library for Preact applications. It provides a simple and efficient way to manage reactive state, offering automatic updates and fine-grained reactivity without the need for complex state management solutions.
Pros
- Lightweight and performant, with minimal overhead
- Seamless integration with Preact components
- Fine-grained reactivity for efficient updates
- Simple API that's easy to learn and use
Cons
- Primarily designed for Preact, limiting its use in other frameworks
- May require a mental shift for developers used to traditional state management
- Limited ecosystem compared to more established state management solutions
- Potential for overuse in simple applications where it might not be necessary
Code Examples
- Creating and using a signal:
import { signal } from "@preact/signals";
const count = signal(0);
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
- Using signals in a Preact component:
import { signal } from "@preact/signals";
import { useSignal } from "@preact/signals-react";
const count = signal(0);
function Counter() {
const localCount = useSignal(0);
return (
<div>
<p>Global count: {count}</p>
<p>Local count: {localCount}</p>
<button onClick={() => count.value++}>Increment global</button>
<button onClick={() => localCount.value++}>Increment local</button>
</div>
);
}
- Computed signals:
import { signal, computed } from "@preact/signals";
const firstName = signal("John");
const lastName = signal("Doe");
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
console.log(fullName.value); // "John Doe"
firstName.value = "Jane";
console.log(fullName.value); // "Jane Doe"
Getting Started
To start using Preact Signals, first install it in your Preact project:
npm install @preact/signals
Then, import and use signals in your components:
import { signal } from "@preact/signals";
import { render } from "preact";
const count = signal(0);
function App() {
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
render(<App />, document.body);
This sets up a basic Preact application using Signals for state management.
Competitor Comparisons
A declarative, efficient, and flexible JavaScript library for building user interfaces.
Pros of Solid
- More comprehensive framework with built-in state management and rendering system
- Offers a wider range of reactive primitives and utilities
- Better performance in larger applications due to fine-grained reactivity
Cons of Solid
- Steeper learning curve for developers new to reactive programming
- Smaller community and ecosystem compared to Preact
- May be overkill for simple applications or components
Code Comparison
Solid:
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}
Signals:
import { signal } from "@preact/signals";
function Counter() {
const count = signal(0);
return <button onClick={() => count.value++}>{count}</button>;
}
Both libraries offer reactive state management, but Solid provides a more comprehensive framework with additional features and optimizations. Signals, being part of the Preact ecosystem, is lighter and easier to integrate into existing Preact projects. The code comparison shows similar syntax for creating and using reactive values, with Solid using a function-based approach and Signals using a more object-oriented style.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
Pros of Vue.js Core
- More comprehensive framework with built-in routing, state management, and tooling
- Larger ecosystem and community support
- Better documentation and learning resources
Cons of Vue.js Core
- Heavier bundle size and potentially slower performance
- Steeper learning curve for beginners
- More opinionated structure, which may limit flexibility in some cases
Code Comparison
Vue.js Core:
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
data() {
return { count: 0 }
}
}
</script>
Preact Signals:
import { signal } from "@preact/signals";
const count = signal(0);
export function Counter() {
return <div>{count}</div>;
}
The Vue.js Core example uses a more traditional component structure with separate template and script sections, while Preact Signals employs a more concise, functional approach with signals for state management. Preact Signals offers a simpler API for reactive state, potentially leading to more readable and maintainable code in certain scenarios. However, Vue.js Core provides a more complete framework with additional features out of the box, which may be beneficial for larger applications or teams that prefer a more structured approach.
web development for the rest of us
Pros of Svelte
- Compiler-based approach, resulting in smaller bundle sizes and better performance
- Built-in state management and reactivity system
- More comprehensive framework with routing and other features included
Cons of Svelte
- Steeper learning curve due to its unique syntax and concepts
- Smaller ecosystem compared to React-based libraries
- Less flexibility for integrating with existing projects
Code Comparison
Svelte:
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Clicks: {count}
</button>
Signals:
import { signal } from "@preact/signals";
const count = signal(0);
const increment = () => count.value++;
return (
<button onClick={increment}>
Clicks: {count}
</button>
);
Key Differences
- Svelte uses a compiler-based approach, while Signals is a runtime library
- Svelte has its own templating syntax, whereas Signals integrates with JSX
- Svelte's reactivity is built into the language, while Signals uses explicit signal objects
- Svelte offers a more complete framework, while Signals focuses on state management
Both libraries aim to simplify state management and improve performance, but they take different approaches to achieve these goals.
Deliver web apps with confidence 🚀
Pros of Angular
- Comprehensive framework with built-in tools for routing, forms, and HTTP
- Strong TypeScript integration and support
- Large ecosystem and community support
Cons of Angular
- Steeper learning curve due to its complexity
- Larger bundle size compared to lightweight alternatives
- More opinionated, which can limit flexibility in some cases
Code Comparison
Angular:
@Component({
selector: 'app-counter',
template: `<button (click)="increment()">Count: {{ count }}</button>`
})
export class CounterComponent {
count = 0;
increment() { this.count++; }
}
Signals:
import { signal } from "@preact/signals";
const count = signal(0);
const Counter = () => (
<button onClick={() => count.value++}>Count: {count}</button>
);
Key Differences
- Angular uses a more verbose, class-based approach with decorators
- Signals offers a simpler, functional API for state management
- Angular provides a full framework, while Signals focuses on reactive state
- Signals integrates seamlessly with Preact, offering a lightweight solution
- Angular's change detection is more complex compared to Signals' fine-grained updates
Simple, scalable state management.
Pros of MobX
- More mature and battle-tested, with a larger ecosystem and community support
- Provides a more comprehensive state management solution, including computed values and reactions
- Offers better performance for complex state updates and large-scale applications
Cons of MobX
- Steeper learning curve due to its more extensive API and concepts
- Requires more boilerplate code for setup and configuration
- Can lead to less predictable behavior due to its automatic tracking system
Code Comparison
MobX:
import { makeAutoObservable } from "mobx";
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
}
Signals:
import { signal } from "@preact/signals";
const todos = signal([]);
const addTodo = (text) => {
todos.value = [...todos.value, { text, completed: false }];
};
Both MobX and Signals offer reactive state management solutions, but they differ in complexity and approach. MobX provides a more comprehensive toolkit for managing complex application states, while Signals offers a simpler, more lightweight solution that integrates well with Preact. The choice between them depends on the specific needs of your project and your preferred development style.
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME
Signals
Signals is a performant state management library with two primary goals:
- Make it as easy as possible to write business logic for small up to complex apps. No matter how complex your logic is, your app updates should stay fast without you needing to think about it. Signals automatically optimize state updates behind the scenes to trigger the fewest updates necessary. They are lazy by default and automatically skip signals that no one listens to.
- Integrate into frameworks as if they were native built-in primitives. You don't need any selectors, wrapper functions, or anything else. Signals can be accessed directly and your component will automatically re-render when the signal's value changes.
Read the announcement post to learn more about which problems signals solves and how it came to be.
Installation:
# Just the core library
npm install @preact/signals-core
# If you're using Preact
npm install @preact/signals
# If you're using React
npm install @preact/signals-react
# If you're using Svelte
npm install @preact/signals-core
Guide / API
The signals library exposes four functions which are the building blocks to model any business logic you can think of.
signal(initialValue)
The signal
function creates a new signal. A signal is a container for a value that can change over time. You can read a signal's value or subscribe to value updates by accessing its .value
property.
import { signal } from "@preact/signals-core";
const counter = signal(0);
// Read value from signal, logs: 0
console.log(counter.value);
// Write to a signal
counter.value = 1;
Writing to a signal is done by setting its .value
property. Changing a signal's value synchronously updates every computed and effect that depends on that signal, ensuring your app state is always consistent.
signal.peek()
In the rare instance that you have an effect that should write to another signal based on the previous value, but you don't want the effect to be subscribed to that signal, you can read a signals's previous value via signal.peek()
.
const counter = signal(0);
const effectCount = signal(0);
effect(() => {
console.log(counter.value);
// Whenever this effect is triggered, increase `effectCount`.
// But we don't want this signal to react to `effectCount`
effectCount.value = effectCount.peek() + 1;
});
Note that you should only use signal.peek()
if you really need it. Reading a signal's value via signal.value
is the preferred way in most scenarios.
computed(fn)
Data is often derived from other pieces of existing data. The computed
function lets you combine the values of multiple signals into a new signal that can be reacted to, or even used by additional computeds. When the signals accessed from within a computed callback change, the computed callback is re-executed and its new return value becomes the computed signal's value.
import { signal, computed } from "@preact/signals-core";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
console.log(fullName.value);
// Updates flow through computed, but only if someone
// subscribes to it. More on that later.
name.value = "John";
// Logs: "John Doe"
console.log(fullName.value);
Any signal that is accessed inside the computed
's callback function will be automatically subscribed to and tracked as a dependency of the computed signal.
effect(fn)
The effect
function is the last piece that makes everything reactive. When you access a signal inside its callback function, that signal and every dependency of said signal will be activated and subscribed to. In that regard it is very similar to computed(fn)
. By default all updates are lazy, so nothing will update until you access a signal inside effect
.
import { signal, computed, effect } from "@preact/signals-core";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
effect(() => console.log(fullName.value));
// Updating one of its dependencies will automatically trigger
// the effect above, and will print "John Doe" to the console.
name.value = "John";
You can destroy an effect and unsubscribe from all signals it was subscribed to, by calling the returned function.
import { signal, computed, effect } from "@preact/signals-core";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
const dispose = effect(() => console.log(fullName.value));
// Destroy effect and subscriptions
dispose();
// Update does nothing, because no one is subscribed anymore.
// Even the computed `fullName` signal won't change, because it knows
// that no one listens to it.
surname.value = "Doe 2";
The effect callback may return a cleanup function. The cleanup function gets run once, either when the effect callback is next called or when the effect gets disposed, whichever happens first.
import { signal, effect } from "@preact/signals-core";
const count = signal(0);
const dispose = effect(() => {
const c = count.value;
return () => console.log(`cleanup ${c}`);
});
// Logs: cleanup 0
count.value = 1;
// Logs: cleanup 1
dispose();
batch(fn)
The batch
function allows you to combine multiple signal writes into one single update that is triggered at the end when the callback completes.
import { signal, computed, effect, batch } from "@preact/signals-core";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
effect(() => console.log(fullName.value));
// Combines both signal writes into one update. Once the callback
// returns the `effect` will trigger and we'll log "Foo Bar"
batch(() => {
name.value = "Foo";
surname.value = "Bar";
});
When you access a signal that you wrote to earlier inside the callback, or access a computed signal that was invalidated by another signal, we'll only update the necessary dependencies to get the current value for the signal you read from. All other invalidated signals will update at the end of the callback function.
import { signal, computed, effect, batch } from "@preact/signals-core";
const counter = signal(0);
const double = computed(() => counter.value * 2);
const triple = computed(() => counter.value * 3);
effect(() => console.log(double.value, triple.value));
batch(() => {
counter.value = 1;
// Logs: 2, despite being inside batch, but `triple`
// will only update once the callback is complete
console.log(double.value);
});
// Now we reached the end of the batch and call the effect
Batches can be nested and updates will be flushed when the outermost batch call completes.
import { signal, computed, effect, batch } from "@preact/signals-core";
const counter = signal(0);
effect(() => console.log(counter.value));
batch(() => {
batch(() => {
// Signal is invalidated, but update is not flushed because
// we're still inside another batch
counter.value = 1;
});
// Still not updated...
});
// Now the callback completed and we'll trigger the effect.
untracked(fn)
In case when you're receiving a callback that can read some signals, but you don't want to subscribe to them, you can use untracked
to prevent any subscriptions from happening.
const counter = signal(0);
const effectCount = signal(0);
const fn = () => effectCount.value + 1;
effect(() => {
console.log(counter.value);
// Whenever this effect is triggered, run `fn` that gives new value
effectCount.value = untracked(fn);
});
License
MIT
, see the LICENSE file.
Top Related Projects
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
web development for the rest of us
Deliver web apps with confidence 🚀
Simple, scalable state management.
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot