Preact is a small, lightweight alternative to React with the same modern API. As part of its latest updates, Preact recently released Signals, a performant state management library with a set of reactive primitives for managing the application state.
Signals automatically updates the state when the value changes, comes with no dependency arrays, and updates the DOM directly — making it lightning fast. The Preact team claims this makes Signals a better way to manage state.
In this article, we will learn how Signals works, its benefits, features, and more.
Jump ahead:
- What is Signals?
- The motivation behind Signals
- What makes Signals different?
- The benefits of managing state with Signals
- Managing React app state with Signals
- Deriving state using computed signal
- Preact Signals vs. SolidJS Signals
What is Signals?
Signals is a library written in pure JavaScript, which means we can use it in Vue.js, Angular, Svelte, and any application with a JavaScript file. While it can be used in any JavaScript file, the Preact team also developed packages for Preact and React. Signals was built by drawing inspiration from Solid and Vue.
According to the docs, at its core, a signal is an object with a .value
property that holds some value. Accessing a signal’s value property from within a component automatically updates that component when the value of that signal changes. We will understand this more and see how it works later in the article.
At the time of writing, there are currently three versions of Preact Signals packages:
- @preact/signals-core v1.2.2: For the shared core functionality
- @preact/signals v1.1.2: For the Preact bindings
- @preact/signals-react v1.2.1: For the React bindings
The motivation behind Signals
The Signals team comprises developers with years of experience building software applications for startups and large-scale businesses. Over time, they’ve discovered a recurring problem with managing the application state.
To understand the problem Signals aims to solve, let’s consider a real-world scenario. Say a parent component holds some state has two children components, and one of the children has to access that state:
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function addToCount() { setCount((count) => count + 1); } return ( <div className="App"> <Parent count={count} addToCount={addToCount} /> </div> ); } function Parent({ count, addToCount }) { return ( <div> <ChildOne /> <ChildTwo count={count} addToCount={addToCount} /> </div> ); } function ChildOne() { return <p>A Dummy Counter App</p>; } function ChildTwo({ count, addToCount }) { return ( <div> <span>{count}</span> <div> <button onClick={addToCount}>+</button> </div> </div> ); }
In the code snippet below, the Parent
acts as a container that passes the count
state to ChildTwo
, while ChildOne
is a stateless component. The problem lies in what happens when we click the button to update the state.
Parent
, ChildOne
, and ChildTwo
all re-render, even though ChildTwo
is the only component directly using the state. While we can address this problem of unnecessary re-renders using methods such as memoization
, the Preact team aims to eliminate this issue altogether with Signals.
We can address this performance issue of unnecessary re-renders by switching to Signals:
import { signal } from "@preact/signals-react"; //importing signal export default function App() { const count = signal(0); //creating the signal function addToCount() { return (count.value = count.value + 1); //updating the count } return ( <div className="App"> <Parent count={count} addToCount={addToCount} /> </div> ); } function Parent({ count, addToCount }) { return ( <div> <ChildOne /> <ChildTwo count={count} addToCount={addToCount} /> </div> ); } function ChildOne() { return <p>A Dummy Counter App</p>; } function ChildTwo({ count, addToCount }) { return ( <div> <span>{count.value}</span> //accessing the count value <div> <button onClick={addToCount}>+</button> </div> </div> ); }
The application has become more performant by changing only four lines of code. How does Signals achieve this? How does it prevent unnecessary re-rendering and ensure optimal performance when reacting to state updates?
This is possible because a signal is an object that contains a value. Unlike the useState
scenario that passes the state directly through the component tree, Signals only passes the object, which acts as a reference to the value. This ensures that only the components that access the .value
of the signal
object render when the state changes.
Signals track when their value is accessed and updated. Accessing a signal’s .value
property from within a component automatically re-renders the component when that signal’s value changes. As a result, a signal can be updated without re-rendering the components it was passed through because those components see the signal and not its value.
What makes Signals different?
Signals was built with performance in mind, and the following features and behaviors make it distinct. The team behind Signals describes it as follows:
- Lazy by default: Only signals that are currently used somewhere are observed and updated — disconnected signals don’t affect performance
- Optimal updates: If a signal’s value hasn’t changed, components and effects that use that signal’s value won’t be updated, even if the signal’s dependencies have changed
- Optimal dependency tracking: The framework tracks which signals everything depends on for you — no dependency arrays like with Hooks
- Direct access: Accessing a signal’s value in a component automatically subscribes to updates without the need for selectors or Hooks
The benefits of managing state with Signals
The first benefit of managing state with Signals is that Signals does not re-render a whole component or application when state changes. Instead, it updates the part of the application attached to the state value being tracked. This ensures that applications remain performant while ensuring reactivity in response to user actions. According to the documentation, what makes Signals unique and great is that:
State changes automatically update components and UI in the most efficient way possible. Automatic state binding and dependency tracking allows Signals to provide excellent ergonomics and productivity while eliminating the most common state management footguns.
Second, we can declare global Signals and import them into children
components throughout the application. It simplifies the complexities around state management by providing a simple and intuitive plug-and-play API that is easy to use.
Lastly, we don’t have to set up a dependencies array like React’s useEffect
Hook. Signals will automatically detect dependencies and call effects when dependencies change.
Managing React app state with Signals
Let’s implement our knowledge of Signals and use it to manage the state of a simple counter application.
First, start by setting up a fresh React app with npx create-react-app signals-react-app
. Then, install the React package using npm install @preact/signals-react
.
Copy the code below into your App.js
file to set up the counter application:
import { signal } from "@preact/signals-react"; const count = signal(0); function increaseCount() { return (count.value = count.value + 1); } function reduceCount() { return (count.value = count.value - 1); } export default function App() { return ( <div className="App"> <span>{count}</span> <div> <button onClick={reduceCount}>-</button> <button onClick={increaseCount}>+</button> </div> </div> ); }
In the code above, we accessed the signal
object from the @preact/signals-react
package and used that to set up a count
state. The value we passed to the signal is now accessible from its .value
property.
Second, we set up two functions, increaseCount
and reduceCount
, which increase and reduce the value of the count, respectively. And we passed the functions to the buttons’ onClick
handler.
This is a basic implementation of Signals, but it does a good job of showing us how it works.
Deriving state using computed signals
Besides the signal
object, Signals also has a computed
function that we can use to set up derived states. With computed
, we can create a new signal from the other signals’ values. The returned computed signal is read-only and is automatically updated when any signal that depends on it changes.
Let’s set up a dummy user authentication application to see how computed
works. Here are some pictures of what we will build:
We will start by creating an AuthSignal.js
file containing the state and authentication logic. Copy and paste the following code into the file:
import { signal, computed } from "@preact/signals-react"; export const user = signal(null); //the default state //derived state based on whether a user exists export const isLoggedIn = computed(() => { return !!user.value; });
In the code above, we imported signal
and computed
from @preact/signals-react
. We also set up a user
signal with a default value of null
, meaning there is no logged in user at first.
We also used the user
signal to derive the value of isLoggedIn
. If a user exists, isLoggedIn
will be true
. Then, we exported user
and isLoggedIn
for use in other parts of the application.
The Header
component consists of the logged in user’s name and two buttons: one for logging in and the other for logging out.
To set it up, copy and paste the code below:
import { user } from "../AuthSignals"; const dummyUserData = { name: "John Doe", email: "[email protected]" }; export default function Header() { return ( <header> <h1>A cool header</h1> <ul> {user.value && <li>{user.value.name}</li>} {!user.value ? ( <li> <button onClick={() => { user.value = dummyUserData }}> Login </button> </li> ) : ( <li> <button onClick={() => { user.value = null }}> Logout </button> </li> )} </ul> </header> ); }
Let’s break it down and look at what we did:
- Imported the
user
signal fromAuthSignals
- Defined a
dummyUserData
containing some random user details - Conditionally rendered the user’s name if there is a logged in user
- Conditionally rendered the login and logout button based on the state of the
user
signal’s value - When the login button is clicked, we populate the
user
signal with thedummyUserData
- When the logout button is clicked, we set the
user
signal tonull
Setting up the Home
component
The Home
component displays a simple message notifying us if we are logged in or not. Let’s set it up:
import { isLoggedIn } from "../AuthSignals"; export default function Home() { return ( <div> <span> {isLoggedIn.value ? "You are logged in" : "You are not logged in"} </span> </div> ); }
Remember, isLoggedIn
is a derived state with a Boolean
value that changes based on the value of user
signal. We conditionally render a message base on the value of isLoggedIn
.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Finally, we bring the components together in the App.js
file:
import Header from "./components/Header"; import Home from "./components/Home"; export default function App() { return ( <div> <div className="App"> <Header /> <Home /> </div> </div> ); }
With that, we have learned more about computed
and used it to create a dummy user authentication system.
Preact Signals vs. SolidJS Signals
Like Preact, Solid also comes with its own state management solution called Signals. As we saw earlier, Solid is one of the frameworks from which the Preact team drew inspiration.
Solid provides a createSignal
function that is similar to React’s useState
Hook:
import { createSignal } from "solid-js"; const [count, setCount] = createSignal(0); function Counter() { const increment = () => setCount(count() + 1); return ( <button type="button" onClick={increment}> {count()} </button>); }
It will take the initial state as a parameter and return two items we can access by array destructuring. The first item in the array is the getter
and the second item is the state’s setter
.
Note, like Preact Signals, we don’t need to keep createSignal
inside the component function. Similar to Preact Signals’ computed
function, Solid also provides a way to create derived signals:
const doubleCount = () => count() * 2; return ( <button type="button" onClick={increment}> {count()} <div>Count: {doubleCount()}</div>; </button>);
Although the two-state management packages share some similarities, there are some differences between the two.
First, Preact and Solid’s Signals have different APIs, where Solid’s implementation is similar to React’s useState
. Solid’s method for creating derived signals is different from Preact’s. And unlike Preact, Solid’s Signals can only be used in Solid applications.
Conclusion
Preact Signals is a fresh and welcoming solution to the problems related to state management issues. It is still relatively new, but it appears to have a promising future because of its minimalistic and simple architecture.
While you may not be immediately sold on using it for large-scale projects, consider trying it out in personal and practice ones. You may fall in love with it. Grab the code for the demo applications we built through the counter app and user auth system.
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.