Cross-Component Communication
Components are self-contained, but sometimes they need to talk to each other. Pulse gives you three ways to do this. Choosing the right one depends on what you're sharing and why.
Method 1 — Direct global access
Every component is a global. Read signals and call methods directly:
// From any component:
Aimbot.enabled.set(true) // write a signal
const r = Aimbot.radius() // read a signal
When to use it:
- One component needs a current value from another
- One component needs to trigger a method on another
- The relationship is clear and intentional
When not to use it:
- You find yourself calling five different components to coordinate one action — that's a sign you need a store
- Circular dependencies (
AcallsBwhich callsA) — refactor
Method 2 — Watch a signal from another component
React to a signal change in another component using on.signal or .watch:
// In another component's defineComponent:
Aimbot.enabled.watch((v) => {
if (v) showIndicator()
else hideIndicator()
})
Subscriptions registered via .watch() are cleaned up automatically when the script is re-run.
When to use it:
- Component B needs to react to changes in component A's signal
- You don't want B to poll — you want it to react immediately
Method 3 — Pulse.Store (shared reactive KV)
For state that multiple components legitimately own together:
-- Define the key once (usually in globals.lua or the component that owns it)
Pulse.Store.define("GlobalTarget", nil)
-- Any component can write it
Pulse.Store.set("GlobalTarget", someModel)
-- Any component can read it
local target = Pulse.Store.get("GlobalTarget")
-- Any component can watch it
Pulse.Store.watch("GlobalTarget", function(newValue)
-- react to change
end)
Define keys in component setup:
// In defineComponent — define once, use anywhere
Pulse.Store.define('GlobalTarget', null)
When to use it:
- State is genuinely shared and has no single owner
- Multiple components need to react to the same state change
- The state doesn't belong cleanly inside any one component
When not to use it:
- State that clearly belongs to one component — just make it a signal there
- As a lazy way to avoid passing values — that leads to spaghetti
Choosing between them
| Situation | Use |
|---|---|
| B needs A's current value once | Direct access: A.signal() |
| B needs to react when A changes | :watch(A.signal, fn) |
| Multiple components share ownership | Pulse.Store |
| A needs to trigger an action in B | Direct method: B:Method() |
| A needs to send data to B persistently | Pulse.Store.set(key, value) |
A real example — aimbot + nape size
The NapeSize component reads whether the Aimbot has a locked target, then applies a larger hitbox size to that specific target:
-- inside NapeSize's loop:
local isTargeted = Components.Aimbot ~= nil
and Aimbot:GetLockedTarget() == shifter
if isTargeted then
nape.Size = Vector3.new(orig.X * 15, orig.Y * 20, orig.Z * 20)
else
-- normal size
end
NapeSize calls Aimbot:GetLockedTarget() — a direct method call. Aimbot doesn't know NapeSize exists. The dependency is one-directional and explicit.
Anti-patterns to avoid
Global state tables (_G.State = {}): These work but have no reactivity, no cleanup, and no type safety. Use Pulse.Store instead.
Polling another component in Heartbeat: on.heartbeat(() => { if (Aimbot.enabled()) ... }) — if you need to react when Aimbot.enabled changes, use .watch(), not a polling loop.
Writing to another component's signal to communicate: Aimbot.radius(600) from NapeSize is bad coupling. If NapeSize needs to influence Aimbot's radius, there should be a shared store key for it.