DEV Community

Sai Krishna
Sai Krishna

Posted on • Edited on

Preact Signals: Managing state with style

Signals, a new feature introduced by Preact Team. With its automatic state binding and dependency tracking makes Signals a better way to manage state. Let's see what it is and how it is unique.

Signals

Signals are reactive primitives for managing application state. It is a unique way to deal with the state with automatic update of components and UI in the most efficient way possible.

Preact says, Signals provide great ergonomics based on automatic state binding and dependency tracking and have a unique implementation optimized for the virtual DOM.

Signal at its core

Signal at its core is an object with a .value property that holds a value. It means the value of the signal object can change, but the signal itself always remains the same.

Signal.value property

The signal can be updated without re-rendering any components, since components see the signal and not its value (we're only passing around references to the signal). It skips heavy rendering work and jumps immediately to actual components in the tree that access the signal's .value property.

The second important characteristic is that they track when their value is accessed and when it is updated. Accessing a signal's .value property from within a component automatically re-renders the component when that signal's value changes.

Counter with signal

State management

Let's build a basic TodoList demo app to understand usage of Signal as state management.

1. Installation
We will use @preact/signals-react with react in our demo. To install the package, we use command npm install @preact/signals-react.

Signals can be integrated into other frameworks as if they were native built-in primitives. Signals can be accessed directly in components, and your component will automatically re-render when the signal's value changes.

# 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
Enter fullscreen mode Exit fullscreen mode

2. Usage
Let's create a state containing a to-do list using Signal, which can be represented by an array:

import { signal } from  "@preact/signals-react";

 const todos =  signal([
  {  text:  "Write my first post on DEV community",  completed:  true },
  {  text:  "Explore more into Preact Signals feature",  completed:  false },
]);
Enter fullscreen mode Exit fullscreen mode

Next, We add two functions to create a new to-do item and remove a to-do. We also need to create a signal for the input value, and then set it directly .value on input text modified.

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];
}
Enter fullscreen mode Exit fullscreen mode

3. Global state with context
Now let's add all these into context and create our app state. Below is context provider and function to consume it with state, addTodo, removeTodo functions.

import React, { useContext } from "react";
import { signal } from "@preact/signals-react";
const AppState = React.createContext();

function AppStateProvider(props) {
  const todos = signal([
    { text: "Write my first post on DEV community", completed: true },
    { text: "Explore more into Preact Signals feature", completed: false }
  ]);
  function addTodo(newItem) {
    todos.value = [...todos.value, { text: newItem.value, completed: false }];
  }

  function removeTodo(index) {
    todos.value.splice(index, 1);
    todos.value = [...todos.value];
  }

  return (
    <AppState.Provider value={{ todos, addTodo, removeTodo }}>
      {props.children}
    </AppState.Provider>
  );
}

function useAppState() {
  const appState = useContext(AppState);
  if (!appState) {
    throw new Error("useAppState must be used within a AppStateProvider");
  }
  return appState;
}

export { AppStateProvider, useAppState };

Enter fullscreen mode Exit fullscreen mode

Now we need to build our user interface which consumes the state from our context and displays our to-do list. We also need to integrate addTodo and removeTodo functionality.
First, let's wrap our TodoList component with context provider in the main app component.

import TodoList from "./Todolist";
import { AppStateProvider } from "./AppStateContext";

export default function App() {
  return (
    <div className="App">
      <h1>Signals</h1>
      <h2>npm install @preact/signals-react</h2>
      <AppStateProvider>
        <TodoList />
      </AppStateProvider>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Below is our TodoList UI component.

import { signal } from "@preact/signals-react";
import { useAppState } from "./AppStateContext";

const newItem = signal("");

export default function TodoList() {
  const { todos, addTodo, removeTodo } = useAppState();

  const onInput = (event) => (newItem.value = event.target.value);

  const onAddClick = () => {
    addTodo(newItem);
    newItem.value = "";
  };

  return (
    <>
      <input type="text" value={newItem.value} onInput={onInput} />
      <button style={{marginLeft:'10px'}} onClick={onAddClick}>Add</button>
      <ul style={{ textAlign: "left" }}>
        {todos.value.map((todo, index) => {
          return (
            <li>
              <input
                type="checkbox"
                checked={todo.completed}
                onInput={() => {
                  todo.completed = !todo.completed;
                  todos.value = [...todos.value];
                }}
              />
              {todo.completed ? <s>{todo.text}</s> : todo.text}{" "}
              <button style={{marginLeft:'10px', color: 'red'}} onClick={() => removeTodo(index)}>x</button>
            </li>
          );
        })}
      </ul>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Below is how it looks after integrating components together in our demo app. You can experience full functionality here

demo screenshot

Other functions from Signals API

computed(fn)
The computed function lets you combine the values of multiple signals into a new signal. It is useful in instances where data is derived from other pieces of existing state.
For instance, we want to compute the number of completed to-do items, the below piece of code should do that.

 const completedCount = computed(() => {
    return todos.value.filter((todo) => todo.completed).length;
  });
Enter fullscreen mode Exit fullscreen mode

When any of the signals used in a computed callback change, the computed callback is re-executed and its new return value becomes the computed signal's value.

effect(fn)
The effect function 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. It is very similar to computed(fn), unlike computed signals, effect() does not return a signal.

import { signal, computed, effect } from "@preact/signals-react";

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";
Enter fullscreen mode Exit fullscreen mode

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.

Sometimes we may have multiple updates at the same time, but we don’t want to trigger multiple renders, so we need to merge state updates into one action.

Let's look at our addTodo and clearing the input's newItem.value, we can merge these two actions into one batch and make it one commit at the end of the callback.

  const onAddClick = () => {
    batch(() => {
      addTodo(newItem);
      newItem.value = "";
    });
  };
Enter fullscreen mode Exit fullscreen mode

signal.peek()
The .peek() function allows getting the signal's current value without subscribing to it. It is useful in instances where you have an effect that should write to another signal based on the previous value, but you don't want the effect to re-run when that signal changes.

const counter = signal(0);
const effectCount = signal(0);

effect(() => {
    //Reacts to `counter` signal but not to `effectCount` signal
    effectCount.value = effectCount.peek() + counter.value;
});
Enter fullscreen mode Exit fullscreen mode

TodoList demo app codesandboxurl

Top comments (0)