Skip to main content

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 (A calls B which calls A) — 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

SituationUse
B needs A's current value onceDirect access: A.signal()
B needs to react when A changes:watch(A.signal, fn)
Multiple components share ownershipPulse.Store
A needs to trigger an action in BDirect method: B:Method()
A needs to send data to B persistentlyPulse.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.