If you still use useState, you're late man!

If you still use useState, you're late man!

Preact Signals: Revolutionizing State Management in Web Applications

State management remains one of the most challenging aspects of modern web development. Preact Signals emerges as an elegant and efficient solution, offering a simple API with exceptional performance characteristics.

What are Signals?

Signals are reactive containers that hold values and automatically notify dependent components when those values change. Unlike traditional state management solutions, Signals are incredibly lightweight and require minimal boilerplate code.

Why Choose Signals?

  1. Superior Performance

    • Granular updates that minimize unnecessary re-renders

    • Minimal overhead compared to traditional solutions

    • Reduced need for memo or useMemo hooks

  2. Developer-Friendly API

    • Clear, straightforward syntax

    • Gentle learning curve

    • Seamless integration with Preact components

  3. Minimal Bundle Size

    • Only ~1KB gzipped

    • Perfect for performance-critical applications

Practical Examples

Basic Example: Counter

import { signal } from "@preact/signals";
import { render } from "preact";

const count = signal(0);

function Counter() {
  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick={() => count.value++}>Increment</button>
    </div>
  );
}

Advanced Example: Todo List

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

const todos = signal([]);
const filter = signal('all');

// Computed signal for filtering todos
const filteredTodos = computed(() => {
  switch (filter.value) {
    case 'active':
      return todos.value.filter(todo => !todo.completed);
    case 'completed':
      return todos.value.filter(todo => todo.completed);
    default:
      return todos.value;
  }
});

function TodoList() {
  const addTodo = (text) => {
    todos.value = [...todos.value, { id: Date.now(), text, completed: false }];
  };

  const toggleTodo = (id) => {
    todos.value = todos.value.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
  };

  return (
    <div>
      <input 
        onKeyPress={e => e.key === 'Enter' && addTodo(e.target.value)}
        placeholder="New todo"
      />
      <div>
        {filteredTodos.value.map(todo => (
          <div key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
          </div>
        ))}
      </div>
      <div>
        <button onClick={() => filter.value = 'all'}>All</button>
        <button onClick={() => filter.value = 'active'}>Active</button>
        <button onClick={() => filter.value = 'completed'}>Completed</button>
      </div>
    </div>
  );
}

Best Practices

1 - Leverage computed for Derived Values

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

2 - Avoid Direct Mutations

// ❌ Wrong
todos.value.push(newTodo);

// ✅ Correct
todos.value = [...todos.value, newTodo];

Keep Signals at the Highest Possible Level

  • Define signals outside components for global state

  • Use signals inside components only for local state

Signal Composition

const user = signal({ name: 'John', age: 30 });
const formattedUser = computed(() => 
  `${user.value.name} (${user.value.age})`
);

Performance Tips

  1. Granular Signals

    • Split large state objects into multiple signals for better performance
// Instead of
const userState = signal({ name: '', email: '', preferences: {} });

// Use
const userName = signal('');
const userEmail = signal('');
const userPreferences = signal({});
  1. Batch Updates

    • Use batch functions to update together very fast a lot of signals
function updateUser() {
  batch(() => {
    userName.value = 'Jane';
    userEmail.value = 'jane@example.com';
  });
}

Migrating from useState

jsxCopy// Before
const [count, setCount] = useState(0);

// After
const count = signal(0);
// Use count.value to read
// Use count.value = newValue to write

Conclusion

Preact Signals offers a modern and efficient approach to state management. Its simplicity, performance benefits, and ease of use make it an excellent choice for projects of any scale.

The combination of minimal boilerplate, intuitive API, and exceptional performance positions Signals as a compelling alternative to traditional state management solutions. Whether you're building a small application or a complex system, Preact Signals provides the tools needed for effective state management while maintaining optimal performance.

Next time you start a new project, consider giving Preact Signals a try – you might find it's the state management solution you've been looking for.