Why Preact Signals Better than React Hooks for Me

As a React developer for 5 years, I have challenges managing state. React hooks improved the situation, but dealing with nested hooks, manual caching, and complex logic was still a struggle. Then came @preact/signals-react, a little-known library that turned my development chaos into a more peaceful and organized experience.

preact-signal

Getting stuck in the world of React hooks had its exciting beginning. The idea of managing the state functionally brought a sense of accomplishment. However, as projects expanded, so did the complications with hooks. Dealing with nested useEffect hooks felt like navigating through dense jungles, conditional rendering logic turned messy, and the constant threat of re-renders impacted performance and sanity.

I discovered @preact/signals-react, and at first, it appeared almost too good to be true. “Managing state without hooks? Can that really work?” However, as I explored further, a feeling of relief came over me. It wasn’t some magic trick; it was a whole new way of doing things, centered around reactive data streams known as @preact/signals-react.

Here’s what blew me away:

Performance optimization: @preact/signals-react helps reduce unnecessary re-renders of components, which leads to faster app loading, smoother interactions, and better overall.

Simplified State Management: In small to large-sized apps, @preact/signals-react can often replace more complex state management libraries, making your code easier to understand and maintain.

Improve code organization: @preact/signals-react keeps track of data changes clearly and predictably, making our code more organized and less prone to error.


Let’s jump into a small example,

React code using regular hooks

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch('/api/data')
    .then(response => response.json())
    .then(data => {
      setData(data);
      setLoading(false);
    });
}, []);

if (loading) {
  return <p>Loading...</p>;
}

return (
  <ul>
    {data.map(item => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
);

The same code using @preact/signals-react

const data = useSignal(() => 
  fetch('/api/data')
    .then(response => response.json())
);
const loading = useSignal(() => data.loading);

return (
  <ul>
    {loading.get() && <p>Loading...</p>}
    {data.get().map(item => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
);

Look at how the @preact/signals-reactcode is shorter, clearer, and simpler. You set up @preact/signals-react for data and loading, and their related values update automatically. No tangled hooks, no intricate conditionals — just straightforward, declarative code.


Signals also enhance performance by default. That’s not the only thing @preact/signals-react do. At this point, you don’t really need to memorize React hooks, like memo, useMemo, and all that stuff you would normally need. And even manual caching.

If you’ve worked with React, you’ve probably encountered the problem of unnecessary re-renders.


Here, a simple react application using hooks.

// app.jsx

import React, { useState } from 'react';
import { ChildOne } from './child-one';
import { ChildTwo } from './child-two';

const App = () => {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <button onClick={incrementCount}>Increment Count</button>
      <ChildOne count={count} />
      <ChildTwo  />
    </div>
  );
}

export default App;
// child-one.jsx

const ChildOne = ({ count }) => {
  console.log('ChildComponent 1 is rendered');

  return <div>{`Count: ${count}`}</div>
}
// child-two.jsx

const ChildTwo = ({ count }) => {
  console.log('ChildComponent 2 is rendered');

  return <p>Child 2</p>
}

When we look at the console, we see that both Child-One and Child-Two components re-render every time we click the ‘Increment Count’ button. Based on the logic, the re-rendering of Child-Two each time is unnecessary.

So, by using @preact/signals-react , we can prevent this unnecessary re-rendering issue.

First, you need to install @preact/signals-react

npm i @preact/signals-react
// app.jsx

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

import { ChildOne } from './child-one';
import { ChildTwo } from './child-two';

const count = signal(0);

const App = () => {

  const incrementCount = () => {
    count.value += 1;
  };

  return (
    <div>
      <button onClick={incrementCount}>Increment Count</button>
      <ChildOne count={count} />
      <ChildTwo  />
    </div>
  );
}

export default App;

In this example, the child components are the same as before. The key difference is that we’re using signals instead of hooks. If you notice, when we click the “Increment Count” button, there’s no console log in the browser. This means we’ve successfully avoided unnecessary re-rendering using @preact/signals-react.


Let’s see behind the scenes of the @preact/signals-react.

Image copied from https://preactjs.com/blog/introducing-signals

The below image shows a DevTools Profiler trace for the same app measured twice: once using hooks as the state primitive and a second time using signals

Image copied from https://preactjs.com/blog/introducing-signals

Useful Links

Read More: https://preactjs.com/blog/introducing-signals

Get Started: https://www.npmjs.com/package/@preact/signals-react

Code Sample: https://github.com/chanakaHetti/react-signals-research

Happy coding 🙂

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top