action function
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?
- 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. - Untracked Execution: The callback runs inside
untracked. If you invoke the action from within aneffector acomputedsignal, 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
action0throughaction10(e.g.action2(...)for 2 arguments) to preserve type arguments. - Extensions: Call
.actiondirectly 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,
};