Trigger
trigger re-runs the effects that depend on the signals read inside its function — without writing any value. It is the "refresh" primitive: force observers to re-execute with the current, unchanged state.
local signals = require(ReplicatedStorage["luau-signals"])
local signal, effect, trigger = signals.signal, signals.effect, signals.trigger
local count = signal(1)
local runs = 0
effect(function()
count()
runs += 1
end) -- runs == 1
trigger(function()
count()
end) -- runs == 2 — the effect re-ran, the value is still 1What it does, step by step
- A throwaway subscriber node is installed as active.
- The function runs; every signal read inside links to the throwaway node.
- The links are unlinked, and each dep is propagated manually — subscribers of those signals are marked dirty/queued as if the signals had been written.
- The queue flushes (inside an implicit batch), running the effects that observe those signals.
The value never changes, so:
- computed cutoff still applies — a computed whose getter returns the same value stops propagation beyond itself, and its effect does not re-run,
- but effects that read a signal directly always re-run: signals have no cutoff, because nothing recomputed.
local raw = signal(1)
local parity = computed(function() return raw() % 2 end)
local signalRuns, computedRuns = 0, 0
effect(function() raw(); signalRuns += 1 end)
effect(function() parity(); computedRuns += 1 end)
trigger(function()
raw()
end)
-- signalRuns == 2 — direct signal read, re-runs
-- computedRuns == 2 — parity's getter re-ran and returned the same value...WARNING
Careful with the second case: parity's getter does re-run (it is marked dirty by the propagation), so if the value is unchanged the cutoff stops the effect from restarting but the getter still executed. trigger is "make them look again", not "make them cheap".
Only what is read inside
trigger affects exactly the signals read during its function — the same dynamic-tracking rules as effects. A signal not read is untouched:
local a, b = signal(1), signal(1)
local runsA, runsB = 0, 0
effect(function() a(); runsA += 1 end)
effect(function() b(); runsB += 1 end)
trigger(function()
a()
end)
-- runsA == 2, runsB == 1 — b's effect never ranReading without the refresh
Because trigger's function runs with a throwaway subscriber, reads inside it do not subscribe anything else — there is no enclosing effect to pollute. But if you call trigger inside a running effect (rare), the inner reads are tracked by the throwaway node, not the effect — so an effect can force a refresh of its own dependencies without double-subscribing.
Typical uses
Invalidating external state. A computed that caches a value derived from a non-reactive source (an instance property, a DataStore snapshot, os.clock):
local frame = workspace.Checkpoint
local lastCFrame = frame.CFrame
local positionText = computed(function()
return `pos: {lastCFrame.Position}` -- reactive part
.. ` | real: {frame.CFrame.Position}` -- non-reactive part, stale!
end)
-- when the non-reactive world changes:
trigger(function()
lastCFrame -- read it, so its observers refresh
end)Manual invalidation hooks. Wrap a non-reactive event (.Changed, a RemoteEvent) into reactivity:
local scoreRemote = ReplicatedStorage.ScoreRemote
local scoreVersion = signal(0) -- a pure "version" signal
local score = computed(function()
scoreVersion() -- depends on the version counter
return readScoreFromSomewhere() -- non-reactive source
end)
scoreRemote.OnClientEvent:Connect(function()
trigger(function()
scoreVersion() -- bump observers of `score`
end)
end)TIP
A plain write scoreVersion(scoreVersion() + 1) does the same job here and is often clearer. Reach for trigger when there is no natural value to write, or when writing would cascade somewhere you do not want.
Re-rendering on demand. UI that must re-run an effect on a non-value event (viewport size, theme change) where the effect reads signals only for the data part.
Inside a batch
trigger wraps its work in an implicit batch (it increments batchDepth around the function and flushes at the end), so multiple signals read inside one trigger produce a single restart of an effect that observes them all:
local a, b = signal(1), signal(1)
local runs = 0
effect(function() a(); b(); runs += 1 end)
trigger(function()
a()
b()
end) -- runs == 2 — once more, not twiceError behavior
If the function passed to trigger throws, the throwaway subscriber's links are still unlinked and propagated (the cleanup runs before the error is re-thrown) — the reactive state stays consistent, and the error surfaces to the caller.
Next steps
- Batches — the batching
triggerleans on - How It Works — propagation and the flush queue
- API: trigger