Skip to main content

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 {
Clienttable?--

Only available on the server. Set this to a table to expose it to the client.

Servertable?--

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. Server
RemoteComponent.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.

Show raw api
{
    "functions": [
        {
            "name": "createEvent () -> MARKER",
            "desc": "Redirects to NetWire.createEvent",
            "params": [],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 216,
                "path": "lib/remotecomponent/src/init.luau"
            }
        },
        {
            "name": "createUnreliableEvent () -> MARKER",
            "desc": "Redirects to NetWire.createUnreliableEvent",
            "params": [],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 223,
                "path": "lib/remotecomponent/src/init.luau"
            }
        },
        {
            "name": "createProperty (initialValue: any) -> MARKER",
            "desc": "Redirects to NetWire.createProperty",
            "params": [],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 230,
                "path": "lib/remotecomponent/src/init.luau"
            }
        },
        {
            "name": "addRemotes",
            "desc": "Registers a set of remote definitions under an extension `namespace`, to be\nbuilt alongside the component's own `Client` remotes. Call this from a\ndependent extension's `Constructing` hook (declare `.Extensions = {RemoteComponent}`\nso ordering is guaranteed). `defs` has the same shape as a component's `Client`\ntable (functions become server methods; `createEvent`/`createProperty` markers\nbecome signals/properties).\n\nBy default the remotes are *internal*: they live under `namespace` and are\nretrieved with [RemoteComponent.getRemotes]. Pass `{exposed = true}` to instead\nmerge them flat onto the component's `Client`/`Server` surface (errors on any\nname collision). No-op on the client — the client derives everything from the\nreplicated remote names.",
            "params": [],
            "returns": [],
            "function_type": "static",
            "realm": [
                "Server"
            ],
            "source": {
                "line": 250,
                "path": "lib/remotecomponent/src/init.luau"
            }
        },
        {
            "name": "getRemotes",
            "desc": "Returns the isolated sub-object for an extension's internal remotes (the ones\nregistered without `exposed = true`). Valid from the registering extension's\n`Starting` hook onward, on both the server and the client. On the server the\nsub-object carries a `.Server` back-reference to the component, mirroring\n`Client.Server`.",
            "params": [],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 330,
                "path": "lib/remotecomponent/src/init.luau"
            }
        }
    ],
    "properties": [],
    "types": [
        {
            "name": "RemoteComponent",
            "desc": "",
            "fields": [
                {
                    "name": "Client",
                    "lua_type": "table?",
                    "desc": "Only available on the server. Set this to a table to expose it to the client."
                },
                {
                    "name": "Server",
                    "lua_type": "table?",
                    "desc": "Only available on the client. The indices of this are inferred from the server."
                }
            ],
            "source": {
                "line": 157,
                "path": "lib/remotecomponent/src/init.luau"
            }
        }
    ],
    "name": "RemoteComponent",
    "desc": "RemoteComponent is a component extension that allows you to easily give\nnetworking capabilities to your components.\n\nYou can access the server-side component from the client by using the `.Server` index\non the component. You can access the client-side component from the server by using\nthe `.Client` index on the component.\n\n```lua\n-- MyComponent.server.lua\nlocal MyComponent = Component.new {\n\tTag = \"MyComponent\",\n\tAncestors = {workspace},\n\tExtensions = {RemoteComponent},\n}\n\nMyComponent.Client = {\n\tTestProperty = RemoteComponent.createProperty(0),\n\tTestSignal = RemoteComponent.createEvent(),\n}\n\nfunction MyComponent.Client:TestMethod(player: Player)\n\treturn (\"Hello from the server!\")\nend\n```\n\n```lua\n-- MyComponent.client.lua\nlocal MyComponent = Component.new {\n\tTag = \"MyComponent\",\n\tAncestors = {workspace},\n\tExtensions = {RemoteComponent},\n}\n\nfunction MyComponent:Start()\n\tself.Server:TestMethod():andThen(print)\nend\n```\n\n:::caution Fast tagging and untagging\nYou can encounter issues if you untag and then retag again quickly or unparent and\nreparent to the same location on the server. This is because the server will rebuild the\ncomponent, but the client will not recognize that there was a change as collectionservice\nwont think anything is different and their remotes can become desynced.\n:::\n\n:::caution RemoteComponent Usage Limitations\nAccessing `.Server` or `.Client` is only safe to do so once the client has completed its \nextension 'Starting' cycle and began its `:Start()` method\n:::\n\n:::caution Yielding accidents\nWhen using RemoteComponent, you *must* have both a client and server component. If you do not,\nthen the client will yield until the server component is created. If only one side extends RemoteComponent,\nthen you may encounter infinite yields.\n:::\n\n## Extension remotes\n\nOther component extensions can contribute their own remotes to a component\nwithout owning its `Client` table. Declare `RemoteComponent` as a dependency\nand register in your `Constructing` hook (dependency ordering guarantees this\nruns after RemoteComponent's `Constructing` and before its `Starting`). Access\nthe built remotes from your `Starting` hook onward via [RemoteComponent.getRemotes].\n\nThe extension module is shared across both realms. `addRemotes` only does work\non the server; on the client it is a no-op, because the client recovers these\nremotes from their replicated names rather than from registration.\n\n```lua\nlocal RunService = game:GetService(\"RunService\")\n\nlocal WeaponExtension = {}\nWeaponExtension.Extensions = {RemoteComponent}\n\nfunction WeaponExtension.Constructing(component)\n\t-- Runs on both realms; the client call is a no-op.\n\tRemoteComponent.addRemotes(component, \"Weapon\", {\n\t\tAmmo = RemoteComponent.createProperty(30),   -- RP: server -> client state\n\t\tHit = RemoteComponent.createEvent(),          -- RE: server -> client event\n\t\tShoot = function(self, player, target)         -- RF: client -> server method\n\t\t\t-- `self` is the server sub-object (carries `.Server`); `player` is injected.\n\t\t\tself.Ammo:Set(player, self.Ammo:Get(player) - 1)\n\t\t\treturn true\n\t\tend,\n\t})\nend\n```\n\n### Server usage\n\n```lua\nfunction WeaponExtension.Starting(component)\n\tif not RunService:IsServer() then return end\n\tlocal remotes = RemoteComponent.getRemotes(component, \"Weapon\")\n\tremotes.Ammo:SetTop(30)\n\tremotes.Hit:FireAll(Vector3.zero)\n\t-- remotes.Server == component  (server-only back-reference)\nend\n```\n\n### Client usage\n\nOn the client the sub-object holds the same keys, but as *client-side* objects:\nmethods return Promises and **must be called with `:`** (Comm's method wrapper\nconsumes the first `self` argument, so a dot-call would drop the real\narguments — RemoteComponent guards against this and raises a clear error rather\nthan silently sending the wrong request), signals expose `:Connect`, and\nproperties expose `:Get` / `:Observe`. There is no `.Server` back-reference in\nthe client sub-object.\n\n```lua\nfunction WeaponExtension.Starting(component)\n\tif RunService:IsServer() then return end\n\tlocal remotes = RemoteComponent.getRemotes(component, \"Weapon\")\n\n\tprint(\"ammo:\", remotes.Ammo:Get())               -- property: read\n\tremotes.Ammo:Observe(function(ammo) --[[ ... ]] end) -- property: observe\n\tremotes.Hit:Connect(function(hitPosition) --[[ ... ]] end) -- event: connect\n\n\tremotes:Shoot(someTarget)                          -- method: colon call, returns a Promise\n\t\t:andThen(function(didFire) --[[ ... ]] end)\nend\n```\n\n### Internal vs exposed\n\nRegistered remotes are *internal* by default: their names are prefixed with the\nnamespace under the hood, and they are reachable only through `getRemotes`, so\ntwo extensions can never collide. Pass `{exposed = true}` to instead merge the\nremotes flat onto the component's `Client`/`Server` surface (like the fork's\n`Methods` feature) — collisions with the author's remotes or another extension's\nexposed remotes raise a hard error at registration time. Exposed remotes are\n*not* returned by `getRemotes`; on the client they live flat on `component.Server`\n(same colon-call/Promise rules):\n\n```lua\n-- server: RemoteComponent.addRemotes(component, \"Weapon\", { Reload = fn }, { exposed = true })\n-- client:\ncomponent.Server:Reload():andThen(print)   -- flat on .Server, not in getRemotes\n```\n\nRegistration is only valid before the component's remotes are built: the window\nopens in `Constructing` and closes when RemoteComponent's `Starting` runs.\nBecause a component with registered remotes no longer needs its own `Client`\ntable, RemoteComponent will build a namespace for it even when `Client` is nil.",
    "source": {
        "line": 150,
        "path": "lib/remotecomponent/src/init.luau"
    }
}