DEV Community

Rahul Sharma
Rahul Sharma

Posted on • Updated on

React.js state management using signals

A signal is an object that has a value and can be observed for changes. It is similar to a state, but it is not bound to a component. It can be used to share data between components. It updates the components when the signal changes and updates the UI without re-rendering the whole component.

This lets us skip all of the expensive rendering work and jump immediately to any components in the tree that access the signal's value property.

In this article, I'll be using signals with React.

Installation

npm i @preact/signals-react
Enter fullscreen mode Exit fullscreen mode

Create a signal

We can create state(signal) using signal function, signal function takes default signal(value) as an parameter and returns Proxy object. The value of the signal can be accessed using the signal.value property. We can also set the value of the signal using signal.value = newValue.

import { signal } from "@preact/signals-react";
const count = signal(0);
Enter fullscreen mode Exit fullscreen mode

Counter Component

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

const count = signal(0);
const Counter = () => <button onClick={() => count.value++}>{count}</button>;
Enter fullscreen mode Exit fullscreen mode
NOTE: React Hooks can only be called inside the root of the component, Signal can be used outside of a component.

Effect

We don't have to pass dependencies array like the useEffect hook. It'll automatically detect dependencies and call effect only when dependencies change.

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

const count = signal(0);
const Counter = () => {
  effect(() => console.log(count.value));
  return <button onClick={() => count.value++}>{count}</button>;
};
Enter fullscreen mode Exit fullscreen mode

Advanced Usage

When working with signals outside of the component tree, you may have noticed that computed signals don't re-compute unless you actively read their value.

const count = signal(0);
const double = computed(() => count.value * 2);

const Counter = () => {
  effect(() => console.log(count.value));
  return (
    <div>
      <h1>{double}</h1>
      <button onClick={() => count.value++}>{count}</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Live Demo: Counter Demo


Thank you for reading 😊

Got any questions or additional? please leave a comment.


Must Read If you haven't

Catch me on

Youtube Github LinkedIn Medium Stackblitz Hashnode HackerNoon

Top comments (16)

Collapse
 
elpddev profile image
Eyal Lapid

This is only for preact? Because of the custom jsx renderer that can hook into singals?

Collapse
 
elpddev profile image
Eyal Lapid

Ok, I have just found out there is a specfic version for React also from the same authors.

npmjs.com/package/@preact/signals-...

Collapse
 
marcoselopez profile image
Marcos Emmanuel López • Edited

Hi Rahul!
Thank you so much for this guide. I have a little problem, well, I don't know if it's a problem but it seems odd.
I was messing with creating a signal that contained an object like this:
const counters = signal({
counter1: 0,
counter2: 0,
})

and in my component I've declared an effect to see that value change, like this:
effect(() => console.log(count.value))

And then I want to update one of those values like this:
const increment = (val) => {
counters.value = {
...counters.value,
counter1: count.value.counter1 += val
}
}

But what I notice in the console is that the value of the signal prints more and more everytime it changes, for example, when I press increment, it will print the object 1 time, if I press it again, it prints the object 2 times, If I press it againt, it prints the object 3 times, and so on.

Do you know why this happens?

EDIT:
Ok, I've just found out that if instead of using effect I use batch, the console log fires only 1 time per change:
batch(() => {
console.log(count.value)
})

But I still don't understand why that happens?

Collapse
 
danvladandreev profile image
danVladAndreev • Edited

This is probably because you effect isn't destroyed after the re-render.
You can try this:

import {useSignalEffect} from '@preact/signals-react';
...
useSignalEffect(() => console.log(count.value));

This should create the effect only once.

Collapse
 
indrajitbnikam profile image
Indrajeet Nikam

Hi Rahul, Great article.

I have a question though, Is there any way to restrict access to signals so that signals are not-misused by team members in the future (let's say after 2-3 years).

Because the way I understand it, anybody can change it right? It's usually not an issue for senior folks in team but junior devs tend to misuse such highly flexible mechanism.

How can I prevent that? that's my biggest concern for using signals in our massive codebase at company.

Collapse
 
devsmitra profile image
Rahul Sharma

I don't think we can restrict anyone from using it, because it's the public library.
One thing you can do is add an eslint rule for such imports. I usually do that to forcefully adopt "Tree Shaking" from any library.

FYR: mui.com/material-ui/guides/minimiz...

{
  "rules": {
    "no-restricted-imports": [
      "error",
      {
        "patterns": ["@mui/*/*/*"]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
indrajitbnikam profile image
Indrajeet Nikam

This is certainly a way to enforce imports but my main concern was slightly different.

I want to avoid concerns like somebody changing the state of signal from outside the component where it is not supposed to be accessed. I searched a lot for the solution,

I came to the conclusion that, only sensible way of doing that is through this pattern/recipe

Image description

You can read more here, preactjs.com/guide/v10/signals#man...

I tested it in code-sandbox and it works as expected while creating different signal instances for different components. you can play with it here,
codesandbox.io/s/react-16-9-0-fork...

Thread Thread
 
devsmitra profile image
Rahul Sharma

I got your point.

We can create small stores (like user, todo, etc) using the signal. All the actions related to user/todo should use that. Svelte and Solid.js follow the same pattern for creating custom stores.

Collapse
 
nhd2106 profile image
Duoc95

hi Rahul, can you use object with signal. thanks in advance.

Collapse
 
rsmelo92 profile image
Rafael Melo

yes you can, check this section here on docs

Combining multiple updates into one
preactjs.com/guide/v10/signals#usa...

Collapse
 
nhd2106 profile image
Duoc95 • Edited
import React from 'react'
import {  signal } from '@preact/signals-react'

function Todos(props) { 
    const todos = signal([
        { name: '123'},
    ])

    const addToDo = () => {
        todos.value = [...todos.value, { name: '123123'}]
    }

    return (
    <div>
        <button onClick={() => {
            addToDo();
        }}>add to do</button>
        {
            todos.value.map(({ name }, index) => (<div key={index} style={{
                fontWeight: 'bold'
            }}>{name}</div>))
        }
    </div>
  )
}

export default Todos

Enter fullscreen mode Exit fullscreen mode

i have tried, but my UI didn't update after click add new Todo.

Thread Thread
 
karrotkardiachain profile image
Dang Khoa

I found that todos value will be create new when you change value. Just move it outside a Todos component to make it works properly.

const todos = signal([
        { name: '123'},
    ]);
function Todos(props)
...
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
nhd2106 profile image
Duoc95

thank you, Khoa, i will try it.

Thread Thread
 
nhd2106 profile image
Duoc95

i've tried but not working like you said, could you plz create the sandbox this case? thanks in advance.

Thread Thread
 
devsmitra profile image
Rahul Sharma

@nhd2106 I've added working example.

Collapse
 
rsmelo92 profile image
Rafael Melo

isnt this 1:1 with the preact docs?
preactjs.com/guide/v10/signals#usa...