action function

Function action(
  1. Function fn
)

Wraps a callback function into a reusable, batched, and untracked action.

An action is a higher-order function that takes a callback and returns a new function with the exact same signature. When the returned function is executed, it runs the original callback inside both a batch and an untracked block.

Why use action instead of batch?

  1. Reusability: batch(fn) executes the callback immediately. In contrast, action(fn) returns a reusable function that you can store, pass around, and invoke multiple times to perform batch transactions on demand.
  2. Untracked Execution: The callback runs inside untracked. If you invoke the action from within an effect or a computed signal, the outer reactive context will not establish subscriptions to any signals read inside the action.

Example: Comparing Normal Updates vs. Action Batching

Without Actions (Standard Sequential Updates)

Every signal write immediately notifies active subscribers. This causes transient states and redundant, intermediate executions:

import 'package:preact_signals/preact_signals.dart';

final a = signal('a');
final b = signal('b');

void main() {
  // Set up a subscriber effect
  effect(() => print('${a.value} ${b.value}'));
  // Prints immediately: "a b"

  a.value = 'aa'; // Prints: "aa b"
  b.value = 'bb'; // Prints: "aa bb"
}

Total prints: 3 (initial execution + 2 updates).

With Actions (Coalesced Transaction)

By wrapping the state-mutating function in action, all updates are postponed and flushed in a single notification block once the function completes:

import 'package:preact_signals/preact_signals.dart';

final a = signal('a');
final b = signal('b');

// Create a reusable action
final updateFields = action((String nextA, String nextB) {
  a.value = nextA;
  b.value = nextB;
});

void main() {
  effect(() => print('${a.value} ${b.value}'));
  // Prints immediately: "a b"

  updateFields('aa', 'bb');
  // The effect is deferred during execution and triggers exactly once at the end.
  // Prints: "aa bb"
}

Total prints: 2 (initial execution + 1 coalesced update).


Type-Safety & Extensions

While action accepts any generic Function, Dart's static analysis benefits greatly from type-safe variants or extensions.

  • Type-safe functions: Use action0 through action10 (e.g. action2(...) for 2 arguments) to preserve type arguments.
  • Extensions: Call .action directly on any Dart function (e.g., myFunction.action).

Implementation

Function action(Function fn) => switch (fn) {
      void Function() _ => () => batch(
            () => untracked(() => (fn as dynamic)()),
          ),
      void Function(Never) _ => (a) => batch(
            () => untracked(() => (fn as dynamic)(a)),
          ),
      void Function(Never, Never) _ => (a, b) => batch(
            () => untracked(() => (fn as dynamic)(a, b)),
          ),
      void Function(Never, Never, Never) _ => (a, b, c) => batch(
            () => untracked(() => (fn as dynamic)(a, b, c)),
          ),
      void Function(Never, Never, Never, Never) _ => (a, b, c, d) => batch(
            () => untracked(() => (fn as dynamic)(a, b, c, d)),
          ),
      void Function(Never, Never, Never, Never, Never) _ => (a, b, c, d, e) =>
          batch(
            () => untracked(() => (fn as dynamic)(a, b, c, d, e)),
          ),
      void Function(Never, Never, Never, Never, Never, Never) _ => (
          a,
          b,
          c,
          d,
          e,
          f,
        ) =>
            batch(
              () => untracked(() => (fn as dynamic)(a, b, c, d, e, f)),
            ),
      void Function(Never, Never, Never, Never, Never, Never, Never) _ => (
          a,
          b,
          c,
          d,
          e,
          f,
          g,
        ) =>
            batch(
              () => untracked(() => (fn as dynamic)(a, b, c, d, e, f, g)),
            ),
      void Function(
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
      ) _ =>
        (
          a,
          b,
          c,
          d,
          e,
          f,
          g,
          h,
        ) =>
            batch(
              () => untracked(() => (fn as dynamic)(a, b, c, d, e, f, g, h)),
            ),
      void Function(
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
      ) _ =>
        (
          a,
          b,
          c,
          d,
          e,
          f,
          g,
          h,
          i,
        ) =>
            batch(
              () => untracked(() => (fn as dynamic)(a, b, c, d, e, f, g, h, i)),
            ),
      void Function(
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
        Never,
      ) _ =>
        (
          a,
          b,
          c,
          d,
          e,
          f,
          g,
          h,
          i,
          j,
        ) =>
            batch(
              () => untracked(
                () => (fn as dynamic)(a, b, c, d, e, f, g, h, i, j),
              ),
            ),
      _ => fn,
    };