How to make a table with a change handler

I see a lot of posts asking how to create a system similar to Roblox’s “Change” event mechanism.

I’ve decided to make this post to answer the question once and for all.

The Basic Idea

To Luau, when you use an Instance, you’re referring to a userdata proxy that points to the actual Instance. These userdatas have no data written to them directly, but instead use the __index metamethod to retrieve data from the Instance, and __newindex to set it.

A userdata is a Lua construct for exposing a C object to Lua in a more safe manner. Almost all Roblox datatypes are userdatas, the notable exception is Vector3, which uses the native vector type instead.

Most properties set through __newindex will invoke the Change handler system, which is eventually sent downstream to your change handlers.

You can emulate this behaviour simply with your own metatable and some BindableEvents

Code outline

The basic example uses two tables and a BindableEvent, first of all, lets create the two tables, the one holding the raw data, and the one with a metatable to the raw data:

local changeSignal = Instance.new("BindableEvent") -- change event (you dont have to use a bindable, but it makes this easier)

local raw = {
  apples = 10
} -- the raw data table, we dont access this directly unless to bypass the change signal handler

local proxy = setmetatable({}, {
  __index = nil,
  __newindex = nil
} -- the change handler proxy, each method will be outlined shortly

So, from here, we should explain what to put in each of the metamethod keys.

__index

For index, it should be safe to just point it directly to the raw table, we dont actually need any special behaviour here, unless you want to add access modifiers.

__newindex

This is where the magic happens, in __newindex, we need to define a function to send a change downstream to the event, unless the value didn’t change. Its worth noting that the existence of __newindex does not automatically write the key-value pair to the table, unless you tell it to (through rawset).

My approach to this is as follows, this sends the property name, new and old value to the event signal.

__newindex = function(self, key, value)
  local oldValue = raw[key]

  if oldValue == value then return end -- we dont want changes that didn't change anything firing the event
  raw[key] = value -- write the new key to the raw data table

  changeSignal:Fire(key, value, oldValue) -- send the change through the event
end

As you can see, this is an extremely simple system once you get to grips with how it works. We’re simply changing the key value then sending that to an event, because we’re not writing to the proxy table itself, this should work.

Writing a key to the proxy table directly, either within setmetatable or rawset will disable __newindex for that key, as this metamethod only invokes for undefined/nil keys.

Showcase

If we now use this proxy and bindable to listen for changes, we’ll notice that any changes to apples is sent to the event handler:

changeSignal.Event:Connect(print)
proxy.apples = 15 --> apples 15 10
proxy.apples = 10 --> apples 10 15
proxy.apples = 10 --> nothing (expected)

Full Code

For ease of access, here’s the full code outline:

local changeSignal = Instance.new("BindableEvent")
local raw = {
  apples = 10
}

local proxy = setmetatable({}, {
  __index = raw,
  __newindex = function(self, key, value)
    local oldValue = raw[key]

    if oldValue == value then return end
    raw[key] = value

    changeSignal:Fire(key, value, oldValue)
  end,

  __metatable = "The metatable is locked" -- you dont have to lock the metatable if you dont want to, but more just preference for me
})

The ChangeEvent invokes if you write keys to the proxy, not raw.

Summary

As you can see, this is a really simple mechanism to set up on any table, I believe the existence of metatables makes this appear scary, but its really not once you figure out how it works.

5 Likes

FAQ: How do I fit this into the __index system

This is a fun question, a common design pattern for OOP within Roblox is to use a metamethod indexing itself to define methods.
We can change some stuff around to make the change pattern work.

Simply return a secondary table that refers back to the main property table (it also gives you the ability to add private properties if you want that :D)

local Account = {}
Account.__index = Account

function Account.new(balance)
  local account = {}
	account.balance = balance
	setmetatable(account, Account)

	local proxy = setmetatable({}, {
		__index = account
		__newindex = function(self, k, v)
			-- change handler
		end
	})

	return proxy
end

This creates the side effect that methods within Account refer to the proxy table instead of the account table. This should be fine, but may create some unusual behaviour. You might want to create some mechanism for refering back to the original table the proxy is proxying.

I’d personally do that using a weak table that the proxy table refers to:

local Account = {}
local accountProxyRefs = setmetatable({}, {mode = "k"})
Account.__index = Account

function Account.new(balance)
	local account = {}
	account.balance = balance
	setmetatable(account, Account)
	
	local proxy = setmetatable({}, {
		__index = account
		__newindex = function(self, k, v)
			-- change handler
		end
	})

	accountProxyRefs[proxy] = account
	return account
end

In a method, you can then refer back to the owner of a proxy by indexing accountProxyRefs

function Account:widthdraw(balance)
	local raw = accountProxyRefs[self]
	if not raw then return end -- you could alternatively add a bad call error here

	raw.balance -= balance
end
1 Like