How to use a Maid class on Roblox to manage state

James Onnen (Quenty)
Roblox Development
Published in
7 min readAug 28, 2019

The Maid class on Roblox is a useful class that I use to help manage state, especially in regards to signals or events. Be warned: This is a fairly technical article!

Maids are basically just syntactic sugar on handling connections and other clean up tasks in a way that allows you to store functions and connections in a container. It provides an interface for objects to clean themselves up properly.
~ GollyGreg, developer of QClash and a lot of other neat

Maids are really useful!

I use Maid extensively in Jailbreak. It vastly simplifies the cleaning up of events/objects, and gives me confidence that I will avoid memory leaks.
~ Badcc, developer of Jailbreak

Philosophical programming motivation: Why to use a Maid

You can skip this section if you want…

I use Maid extensively in Jailbreak. It vastly simplifies the cleaning up of events/objects, and gives me confidence that I will avoid memory leaks.
~ Badcc

One of the most common issues with the observable pattern is that an observable can keep an object alive. That is, because the signal creates a closure, and thus, a reference, to an object itself, the object will never be garbage collected. This is what Roact (or React in JavaScript), as well as Angular 2, and a variety of other frameworks are trying to solve. The common solution is to disconnect events upon object deconstruction.

Maids originally were designed to help deconstruct signals. A common pattern is that a signal connection will be created sometime later in an object’s lifecycle, but must be disconnected at the end of an object’s lifecycle if present. This results in objects structured like this:

An example of bad connection code

In general, having this code resulted in a lot of cases where the connection or object being cleaned up wasn’t initially there, but could be in certain cases. In this case, we really want a way to hide this behavior. It’s especially messy in inheritance-like-situations where we need to clean up components in the base class, as well as the child class. In this case, the child class almost always has to call ParentClass.Destroy(self) , before it can do its own deconstruction logic.

You even see this issue in objects in C# or JavaScript. You see it especially in IDisposable C# objects.

The other issue with Dispose() or Destroy() being used at the end of an object’s implementation is it creates multiple places where you must remember a member instance exists, and this must be stored.

Additionally, there’s strong motivation to coupling clean-up routines with the direct site where an object is created. For example, when we create a tooltip, it is useful to know exactly when or where an object is being cleaned up. This really helps make it easy to track what is being cleaned up.

For this reason, we introduce the Maid object.

What is a maid, and why should I care?

A maid is an abstraction in charge of cleaning up a state. Maids can be given a task to clean up, which will then, at a later time, clean up that task. Maids can also be given a named task to clean up. If this task is overwritten, it will clean that task up on the spot.

In short, Maids help manage state in your object. Maids can help augment an existing object in your game, encapsulating the deconstruction logic, or can help maintain closures in a cleaner way.

How to use a maid: An overview of an API

First you will need to import the Maid library. This can be done right now by copying this code into a module script, and then requiring the module script.

You can download the Maid source code here:

You construct a new maid by calling:

local maid = Maid.new()

In a class, I like to construct a maid at the beginning as a private member, and then have the deconstructor call the Maid’s destroy method:

function MyClass.new()
local self = setmetatable({}, MyClass)
self._maid = Maid.new() return self
end
function MyClass:Destroy()
self._maid:Destroy()
end

Once a maid is created, there are several ways to assign a task to a maid for cleanup. The first way is to simple call the method :GiveTask() . Alternatively, the Maid can also be overwritten with a key that is not reserved in a maid. For example, you can do

local maid = Maid.new()
maid.cleanBaseplateTask = workspace.Baseplate

In this case, the maid is now planning to clean up the baseplate when it’s told. The base can take the following types of arguments that can be cleaned up.

  • function: The function will be executed upon cleanup
  • event connection: The event connection will be disconnected upon cleanup
  • maid: The maid’s cleanup function will be invoked upon cleanup
  • Roblox object: The Roblox object will be destroyed upon cleanup
  • nil: Only forces cleanup

Cleanup occurs in two ways. The first way is on overwrite a key. For example, in this case, the Baseplate will be immediately destroyed:

local maid = Maid.new()
maid.cleanBaseplateTask = workspace.Baseplate
maid.cleanBaseplateTask = nil

Additionally, this would also force a cleanup of the baseplate while also adding a new baseplate to track to clean up.

local maid = Maid.new()
maid.cleanBaseplateTask = workspace.Baseplate
local newBaseplate = workspace.Baseplate:Clone()
newBaseplate.Parent = workspace
maid.cleanBaseplateTask = newBaseplat

Secondarily, maids can be cleaned up by invoking the :DoCleaning() method, which is the same as a Maid’s :Destroy() method. Maids can be asked to clean multiple times. This looks like this:

local maid = Maid.new()
maid.cleanBaseplateTask = workspace.Baseplate
maid:DoCleaning()

In this case, a maid will clean up all of its tasks.

Note that :GiveTask() actually is also generating a tracking key for that task. For example, you can use :GiveTask() to get a new unique ID for a task that can later be cleaned up. For example

local maid = Maid.new()
local cleanupBaseplateTaskId = maid:GiveTask(workspace.Baseplate)
maid[cleanupBaseplateTaskId] = nil

will actually clean up the baseplate.

Note that maids accept other types of arguments for cleaning besides Roblox parts. The most used one is connections, which generally must be cleaned up when an object is being used. For example, you might write this:

local function onTouchStarted(inputObject)
local maid = Maid.new()
remoteEvent:FireServer("TouchStarted") maid:GiveTask(function()
remoteEvent:FireServer("TouchStopped")
end)
maid:GiveTask(UserInputService.InputEnded:connect(
function(_inputObject)
if _inputObject == inputObject then
print("Touch ended")
maid:Destroy()
end
end))
end

In this case, you’re easily able to capture cleaning up both the touch started event, and also the event listening for an input end. In this case, maids are able to create a lot of extra value here in something that might otherwise be painful to write.

Case study: Animating with RenderStepped and Maids

It may be hard to see why maids are inherently useful. One example of when I like to use maids is when I bind to RunService.RenderStepped in Roblox. I like to have a method setup like this:

local function startUpdate()
if maid._updateConnection and maid._updateConnection.Connected then
return -- already animating
end
maid.updateConnection = RunService.RenderStepped:Connect(function()
if animationIsDone() then
maid.updateConnection = nil
end
doAnimationUpdate()
end)endlocal function setAnimationTarget(target)
-- set target here

startUpdate() -- you can call this freely since maid is tracking state
end

This sort of pattern means that an animation can be stopped because it’s done, thus, stopping the render stepped update, or it can be stopped because the maid is cleaned up, common when you’ve associated an animation with a maid.

Implementation details: Maid cleaning order gotchas

One specific detail about the Maid implementation I linked is that it’s intentionally designed to work such that a maid in cleaning can still add more tasks to be cleaned, and they will be cleaned. This means you can give a maid a task that will clean other tasks before (such as disconnecting signals) additional tasks get executed. Additionally, maids disconnect all events before executing other cleaning components to make sure that maids don’t invoke themselves into an infinite loop.

If a maid’s cleaning task errors (for example, a function), then other tasks may not clean. This is designed to help maids execute permanently, and also report stack traces properly. However, beaware that maids are not guaranteed to execute in an isolated environment, so don’t use wait() or other async operations in a maid, such as invoking a RemoteFunction in a deconstructor.

Maids and promises

Maids are also designed to take in promises. Giving a maid a promise will result in a new promise that will be rejected in the event of the maid being cleaned up. This is especially useful to help manage state whereas promises can be invoked after the lifetime of an object. For example, in another case you might find yourself checking if self.Destroy is defined before letting a promise continue. Giving a promise to a maid is quite valuable in this regard. This API is specifically for promises, and looks like this:

local maid = Maid.new()
local promise = ... -- some promise
maid:GivePromise(promise):Then(...) -- do operation here

In this case, promises and maids can be very valuable together.

Summary

Overall, maids are a really useful tool that can be used to handle events. There are hundreds of points where maids are used across my code.

Maids are everywhere in WFYB’s codebase

I know other top developers are also using maids in their code to help manage state. I highly recommend looking into Maids as a tool to clean up your code further.

self._tooltip = tooltip
self._connection = tooltip.Hidden:Connect(function()
self._connection:Disconnect()
self._connection = nil
self._tooltip:Destroy()
self._tooltip = nil
end)
end
function MyClass:Destroy()
if self._tooltip then
self._connection:Disconnect()
self._connection = nil
self._tooltip:Destroy()
self._tooltip = nil
end
end
return MyClass

--

--

James Onnen (Quenty)
Roblox Development

I’m a maker. Designing and creating new things with people is my hobby. Former intern @Microsoft, @Roblox, @Garmin. Roblox Toy. Raikes 2019.