Preact

Signals

Signals are reactive primitives for managing application state in Preact.

State changes automatically update components and UI in the most efficient way. Automatic state binding and dependency tracking. Signals are effective in applications of any size.

Install:

npm install @preact/signals

Intro

Pain of state management in JavaScript is reacting to changes for a given value, because values are not directly observable. Solution is to store values in a variable and continuously check if they changed. Not ideal for performance. We want a way to express a value that tells us when it changes. That’s what Signals do.

A signal is an object with a .value property that holds a value. This has an important characteristic: a signal’s value can change, but the signal itself stays the same:

import { signal } from "@preact/signals";
 
const count = signal(0);
 
// Read a signal's value by accessing .value:
console.log(count.value); // 0
 
// Update a signal's value
count.value += 1;
 
// The signal's value has changed:
console.log(count.value); // 1

What does it mean?

In Preact, when a signal is passed down through a tree as props or context, we’re only passing around references to the signal. ???? The signal can be updated without re-rendering any components, since components see the signal and not its value. This lets us skip all of the expensive rendering work and jump immediately to any components in the tree that actually access the signal’s .value property.

So, I guess, it only re-render the components that depend on the signal. Other components that don’t reference the .value property of the signal will not re-render.

Signals have a second important characteristic, which is that they track when their value is accessed and when it is updated. In Preact, accessing a signal’s .value property from within a component automatically re-renders the component when that signal’s value changes.

import { signal } from "@preact/signals";
 
// Create a signal that can be subscribed to:
const count = signal(0);
 
function Counter(){
	// Accessing .value in a component automatically re-renders when it changes:
	const value = count.value;
 
	const increment = () => {
		// A signal is updated by assigning to the `.value` property
		count.value++;
	}
 
	return (
		<div>
			<p>Count: {value}</p>
			<button onClick={increment}>click me</button>
		<div>
	);
}

In example above, we accessed count.value to retrieve the current value of count signal, however this is unnecessary. Instead, we can let Preact do all of the work for us by using the count signal directly in JSX:

import { signal } from "@preact/signals";
 
const count = signal(0);
 
function Counter() {
	return (
		<div>
			<p>Count: {count}</p>
			<button onClick={() => count.value++}>click me</button>
		<div>
	);
}

Usage Example

Build a todo list app, where you can add and remove items in a todo list. Start by modeling the state. We’re going to need a signal that holds a list of todos first, which we can represent with an Array:

import { signal } from "@preact/signals";
 
const todos = signal([
	{ text: "Buy groceries" },
	{ text: "Walk the dog" },
]);

Todo List (Signals):

import { render } from "preact";
import { signal, computed } from "@preact/signals";
 
const todos = signal([
  { text: "Write my first post", completed: true },
  { text: "Buy new groceries", completed: false },
  { text: "Walk the dog", completed: false },
]);
 
const completedCount = computed(() => {
  return todos.value.filter(todo => todo.completed).length;
});
 
const newItem = signal("");
 
function addTodo() {
  todos.value = [...todos.value, { text: newItem.value, completed: false }];
  newItem.value = ""; // Reset input value on add
}
 
function removeTodo(index) {
  todos.value.splice(index, 1)
  todos.value = [...todos.value];
}
 
function TodoList() {
  const onInput = event => (newItem.value = event.target.value);
 
  return (
    <>
      <input type="text" value={newItem.value} onInput={onInput} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.value.map((todo, index) => {
          return (
            <li>
              <label>
                <input
                  type="checkbox"
                  checked={todo.completed}
                  onInput={() => {
                    todo.completed = !todo.completed
                    todos.value = [...todos.value];
                  }}
                />
                {todo.completed ? <s>{todo.text}</s> : todo.text}
              </label>
              {' '}
              <button onClick={() => removeTodo(index)}>❌</button>
            </li>
          );
        })}
      </ul>
      <p>Completed count: {completedCount.value}</p>
    </>
  );
}
 
render(<TodoList />, document.getElementById("app"));