Signals Vs Hooks: Understanding Reactivity Models

Explore how fine-grained reactivity with signals compares to component-level re-renders in React hooks, with practical code examples and performance guidance for modern web applications.

Understanding Reactivity Models in Web Development

Modern frontend development has evolved significantly in how we manage state and reactivity. React introduced hooks in 2018, revolutionizing how developers write stateful logic. Meanwhile, signals have emerged as an alternative reactivity model, promising fine-grained updates without full component re-renders.

The fundamental difference between hooks and signals lies in their granularity of updates. While hooks trigger component-level re-renders when state changes, signals enable precise, surgical updates to only the DOM elements that actually depend on changed data.

Key topics covered:

  • How React hooks handle state updates and re-renders
  • Fine-grained reactivity with signals
  • Performance implications and optimization strategies
  • Code examples comparing both approaches
  • When to choose each approach in modern web development

The Evolution From Class Components to Hooks

React's class component model required developers to manage state through this.setState() and lifecycle methods like componentDidMount and componentDidUpdate. The introduction of hooks in React 16.8 addressed these limitations by allowing state and other React features in functional components.

The Component Re-Render Cycle

When you call a state setter in React, whether through useState or useReducer, React schedules a re-render of that component. During this re-render, React executes the component function again with the new state values. The function returns a new JSX tree, which React then compares against the previous tree to determine minimal DOM updates.

This reconciliation process, while sophisticated, involves work that might be unnecessary. Consider a component with multiple pieces of state rendered across different parts of the UI. Updating one piece of state causes the entire component to re-execute, even if most of the rendered output hasn't changed.

Hook Rules and Best Practices

React hooks operate under specific rules that ensure consistent behavior:

  • Hooks must be called at the top level of a component, not inside loops, conditions, or nested functions
  • Dependency arrays in useEffect, useMemo, and useCallback require careful consideration
  • Incorrect dependency arrays can cause stale closures and unexpected behavior

As documented in the Preact Signals Guide, these rules ensure predictable state management across renders.

React Hooks State Update Example
1import { useState, useEffect, useMemo } from 'react';2 3function Counter() {4 const [count, setCount] = useState(0);5 const [multiplier, setMultiplier] = useState(1);6 7 // Computed value with manual dependency management8 const doubled = useMemo(() => count * 2, [count]);9 const total = useMemo(() => doubled * multiplier, [doubled, multiplier]);10 11 // Effect with dependency array12 useEffect(() => {13 console.log(`Count changed to: ${count}`);14 }, [count]);15 16 return (17 <div>18 <p>Count: {count}</p>19 <p>Doubled: {doubled}</p>20 <p>Total: {total}</p>21 <button onClick={() => setCount(c => c + 1)}>Increment</button>22 <button onClick={() => setMultiplier(m => m + 1)}>Increase Multiplier</button>23 </div>24 );25}

Performance Considerations With Hooks

Performance optimization with hooks involves several strategies:

useMemo - Memoizes expensive computations, recomputing only when dependencies change. Useful for transforming large datasets or performing calculations that would otherwise run on every render.

useCallback - Stabilizes function references, preventing child components that are memoized with React.memo from receiving new function props on every render.

useRef - Provides a way to maintain mutable values without triggering re-renders. Perfect for accessing DOM elements or storing values that shouldn't trigger renders.

Despite these tools, the fundamental model of component-level re-renders can create performance bottlenecks in applications with frequently changing state or deeply nested component trees. For teams building modern JavaScript applications, understanding these trade-offs becomes essential for choosing the right architecture.

Fine-Grained Reactivity With Signals

Signals represent a paradigm shift in how state changes propagate through an application. A signal is an object with a .value property that holds state, but unlike React's state, updating a signal doesn't trigger component re-renders.

How Signals Track Dependencies

When you read a signal's value, the signal registers the current execution context as a dependent. This registration happens transparently during the execution of effects, computations, or component renders, as explained in the Preact Signals Documentation.

The core signal API includes:

  • signal(initialValue) - Creates a reactive value
  • computed(fn) - Creates derived state that auto-updates
  • effect(fn) - Runs side effects when dependencies change

Signal Integration With Component Frameworks

Signals integrate with component frameworks through various mechanisms. Preact Signals provides seamless integration where passing a signal directly to JSX automatically creates subscriptions. When the signal changes, only the text node or attribute binding updates, without re-rendering the component function.

Preact Signals Example
1import { signal, computed, effect } from '@preact/signals';2import { render } from 'preact';3 4// Create signals5const count = signal(0);6const multiplier = signal(1);7 8// Create computed signals for derived state9const doubled = computed(() => count.value * 2);10const total = computed(() => doubled.value * multiplier.value);11 12// Create an effect that runs when dependencies change13effect(() => {14 console.log(`Count changed to: ${count.value}`);15});16 17// Components can use signals directly18function Counter() {19 return (20 <div>21 <p>Count: {count}</p> {/* Auto-subscribes */}22 <p>Doubled: {doubled}</p> {/* Auto-subscribes */}23 <p>Total: {total}</p> {/* Auto-subscribes */}24 <button onClick={() => count.value++}>Increment</button>25 <button onClick={() => multiplier.value++}>Increase Multiplier</button>26 </div>27 );28}29 30// Updating count triggers only affected bindings31count.value = 5; // Updates count, doubled, total without component re-render

Computed Signals and Derived State

Computed signals provide an elegant solution for derived state. A computed signal defines its value as a function of other signals, and the framework handles caching, dependency tracking, and updates automatically.

Key differences from useMemo:

  • Computed signals automatically update downstream consumers without explicit dependency management
  • Computed signals are cached and only re-evaluate when actually read
  • useMemo re-evaluates whenever dependencies change regardless of whether the result is used
const firstName = signal('John');
const lastName = signal('Doe');

const fullName = computed(() => {
 return `${firstName.value} ${lastName.value}`;
});

// Reads from computed - auto-updates when dependencies change
console.log(fullName.value);

Comparing Performance Characteristics

Render Optimization Strategies

In applications with deeply nested component trees, signals can significantly reduce update work. With hooks, a state change at the top level triggers re-renders of the entire subtree; with signals, only specific bindings update.

Signals excel in:

  • Real-time data feeds with frequent updates
  • Dashboard applications with multiple widgets
  • Interactive applications (drag-and-drop, collaborative editors)
  • Large applications with complex component trees

According to analysis from LogRocket's comparison of hooks and signals, the performance benefits become most apparent in scenarios with frequent state updates affecting small portions of the UI.

For teams deploying serverless applications on Vercel, understanding these performance characteristics helps in choosing the right state management strategy for your specific use case.

When Hooks Remain the Better Choice

React hooks remain excellent for:

  • Simpler applications with infrequent state updates
  • Teams with strong React expertise
  • Projects where the React ecosystem is essential
  • Applications already built with hooks that don't have performance issues

Hybrid Approaches

Many applications benefit from using both approaches strategically:

  • Global state as signals for efficient cross-component sharing
  • Local component state in hooks for simplicity
  • Derived state as computed signals to avoid manual memoization

Implementation Patterns and Best Practices

Structuring Signal-Based Applications

// store/counter.js
import { signal } from '@preact/signals';

export const count = signal(0);
export const increment = () => count.value++;

// Components can import and use signals directly
import { count, increment } from '../store/counter';

Migration Strategies

  1. Start by identifying isolated components or state that would benefit from signals
  2. Replace local state with signals while keeping component structure
  3. Test thoroughly as behavioral differences in update patterns can affect subtle aspects
  4. For global state, consider gradual rollout where some components use signals while others continue using props

Common Pitfalls to Avoid

  • Creating unnecessary signal overhead for truly static data
  • Accessing signal values inside closures without proper subscription
  • Very complex derived state without chaining simpler computed signals
  • Mixing signals and hooks without understanding the interaction

The Future of Reactivity in Web Development

Emerging Trends

The frontend development landscape continues evolving:

React Compiler (experimental) aims to automatically optimize components without manual memoization, potentially closing some performance gaps with fine-grained reactivity.

Angular Signals show that major frameworks recognize the value of fine-grained reactivity, providing an ergonomic API integrated with Angular's change detection system.

SolidJS demonstrates what's possible when signals are fundamental to the framework architecture--components render once and reactive bindings update directly.

Making Informed Decisions

The choice between signals and hooks should be driven by:

  • Your application's update patterns and performance requirements
  • Team expertise and familiarity with each approach
  • Specific project needs rather than hype or trends
  • Future growth and maintainability considerations

Our web development team has experience implementing both React hooks and signal-based architectures. Whether you're building a new application or optimizing an existing one, we can help you choose the right reactivity model for your specific needs.

Key Differences at a Glance

Understanding the fundamental contrasts between hooks and signals

Update Granularity

Hooks trigger component-level re-renders; signals update only affected bindings

Dependency Tracking

Hooks require manual dependency arrays; signals track dependencies automatically

Performance Model

Hooks use virtual DOM diffing; signals use direct DOM updates without VDOM

Bundle Size

React core includes hooks; signals require additional runtime library

Learning Curve

Hooks are familiar to React developers; signals require new mental model

Ecosystem Support

React ecosystem is hooks-centric; signals have growing library support

Frequently Asked Questions

Ready to Optimize Your Web Application?

Our team specializes in modern frontend architectures and can help you choose the right reactivity model for your project.

Sources

  1. Preact Signals Documentation - Core concepts, API reference, and best practices
  2. LogRocket Blog: Hooks vs Signals - Reactivity model comparison and performance analysis
  3. Prishusoft: Angular Signals vs React useState - Practical code examples comparing state management approaches
  4. Preact Signals GitHub Repository - Implementation details and core package documentation