RemoteComponent
RemoteComponent is a component extension that allows you to easily give networking capabilities to your components.
You can access the server-side component from the client by using the .Server index
on the component. You can access the client-side component from the server by using
the .Client index on the component.
-- MyComponent.server.lua
local MyComponent = Component.new {
Tag = "MyComponent",
Ancestors = {workspace},
Extensions = {RemoteComponent},
}
MyComponent.Client = {
TestProperty = RemoteComponent.createProperty(0),
TestSignal = RemoteComponent.createEvent(),
}
function MyComponent.Client:TestMethod(player: Player)
return ("Hello from the server!")
end
-- MyComponent.client.lua
local MyComponent = Component.new {
Tag = "MyComponent",
Ancestors = {workspace},
Extensions = {RemoteComponent},
}
function MyComponent:Start()
self.Server:TestMethod():andThen(print)
end
Fast tagging and untagging
You can encounter issues if you untag and then retag again quickly or unparent and reparent to the same location on the server. This is because the server will rebuild the component, but the client will not recognize that there was a change as collectionservice wont think anything is different and their remotes can become desynced.
RemoteComponent Usage Limitations
Accessing .Server or .Client is only safe to do so once the client has completed its
extension 'Starting' cycle and began its :Start() method
Yielding accidents
When using RemoteComponent, you must have both a client and server component. If you do not, then the client will yield until the server component is created. If only one side extends RemoteComponent, then you may encounter infinite yields.
Extension remotes
Other component extensions can contribute their own remotes to a component
without owning its Client table. Declare RemoteComponent as a dependency
and register in your Constructing hook (dependency ordering guarantees this
runs after RemoteComponent's Constructing and before its Starting). Access
the built remotes from your Starting hook onward via RemoteComponent.getRemotes.
The extension module is shared across both realms. addRemotes only does work
on the server; on the client it is a no-op, because the client recovers these
remotes from their replicated names rather than from registration.
local RunService = game:GetService("RunService")
local WeaponExtension = {}
WeaponExtension.Extensions = {RemoteComponent}
function WeaponExtension.Constructing(component)
-- Runs on both realms; the client call is a no-op.
RemoteComponent.addRemotes(component, "Weapon", {
Ammo = RemoteComponent.createProperty(30), -- RP: server -> client state
Hit = RemoteComponent.createEvent(), -- RE: server -> client event
Shoot = function(self, player, target) -- RF: client -> server method
-- `self` is the server sub-object (carries `.Server`); `player` is injected.
self.Ammo:Set(player, self.Ammo:Get(player) - 1)
return true
end,
})
end
Server usage
function WeaponExtension.Starting(component)
if not RunService:IsServer() then return end
local remotes = RemoteComponent.getRemotes(component, "Weapon")
remotes.Ammo:SetTop(30)
remotes.Hit:FireAll(Vector3.zero)
-- remotes.Server == component (server-only back-reference)
end
Client usage
On the client the sub-object holds the same keys, but as client-side objects:
methods return Promises and must be called with : (Comm's method wrapper
consumes the first self argument, so a dot-call would drop the real
arguments — RemoteComponent guards against this and raises a clear error rather
than silently sending the wrong request), signals expose :Connect, and
properties expose :Get / :Observe. There is no .Server back-reference in
the client sub-object.
function WeaponExtension.Starting(component)
if RunService:IsServer() then return end
local remotes = RemoteComponent.getRemotes(component, "Weapon")
print("ammo:", remotes.Ammo:Get()) -- property: read
remotes.Ammo:Observe(function(ammo) --[[ ... ]] end) -- property: observe
remotes.Hit:Connect(function(hitPosition) --[[ ... ]] end) -- event: connect
remotes:Shoot(someTarget) -- method: colon call, returns a Promise
:andThen(function(didFire) --[[ ... ]] end)
end
Internal vs exposed
Registered remotes are internal by default: their names are prefixed with the
namespace under the hood, and they are reachable only through getRemotes, so
two extensions can never collide. Pass {exposed = true} to instead merge the
remotes flat onto the component's Client/Server surface (like the fork's
Methods feature) — collisions with the author's remotes or another extension's
exposed remotes raise a hard error at registration time. Exposed remotes are
not returned by getRemotes; on the client they live flat on component.Server
(same colon-call/Promise rules):
-- server: RemoteComponent.addRemotes(component, "Weapon", { Reload = fn }, { exposed = true })
-- client:
component.Server:Reload():andThen(print) -- flat on .Server, not in getRemotes
Registration is only valid before the component's remotes are built: the window
opens in Constructing and closes when RemoteComponent's Starting runs.
Because a component with registered remotes no longer needs its own Client
table, RemoteComponent will build a namespace for it even when Client is nil.
Types
RemoteComponent
interface RemoteComponent {Client: table?--
Only available on the server. Set this to a table to expose it to the client.
Server: table?--
Only available on the client. The indices of this are inferred from the server.
}Functions
createEvent () -> MARKER
RemoteComponent.createEvent () -> MARKER() → ()Redirects to NetWire.createEvent
createUnreliableEvent () -> MARKER
RemoteComponent.createUnreliableEvent () -> MARKER() → ()Redirects to NetWire.createUnreliableEvent
createProperty (initialValue: any) -> MARKER
RemoteComponent.createProperty (initialValue: any) -> MARKER() → ()Redirects to NetWire.createProperty
addRemotes
This item only works when running on the server. ServerRemoteComponent.addRemotes() → ()
Registers a set of remote definitions under an extension namespace, to be
built alongside the component's own Client remotes. Call this from a
dependent extension's Constructing hook (declare .Extensions = {RemoteComponent}
so ordering is guaranteed). defs has the same shape as a component's Client
table (functions become server methods; createEvent/createProperty markers
become signals/properties).
By default the remotes are internal: they live under namespace and are
retrieved with RemoteComponent.getRemotes. Pass {exposed = true} to instead
merge them flat onto the component's Client/Server surface (errors on any
name collision). No-op on the client — the client derives everything from the
replicated remote names.
getRemotes
RemoteComponent.getRemotes() → ()
Returns the isolated sub-object for an extension's internal remotes (the ones
registered without exposed = true). Valid from the registering extension's
Starting hook onward, on both the server and the client. On the server the
sub-object carries a .Server back-reference to the component, mirroring
Client.Server.