How to detect changes in a table

This tutorial will go over how to detect whenever a value inside a table changes. Pretty straight forward.

Before we continue any further. I recommend you are somewhat familar with MetaTables.

Also be aware that this tutorial will only work with dictionaries.


Now. To start. I guess you already have a table set up. For this tutorials sake i’ll call my table “Players”.

After you’ve created your main table. Create another one called “Proxy”

local Players = {}
local Proxy = {}

For demonstration. I’ll already set up some values. Make sure to write your values to the Proxy table. Not the main one!


Now we’re gettin’ into some deep waters.

To detect changes we need to add 2 metamethods. The methods we’ll be using are:
“__index”
“__newindex”

We’ll be giving our main table a metatable that will contain all these methods.

local Players = {}
local Proxy = {}



Proxy.Herobrinekid1 = 10 -- // For demonstration.



setmetatable(Players,{
	__index = Proxy,
	
	__newindex = function(Table, Key: string, Value: any)
		
	end,
})

Now you might be askin’ yourself. Why are we using __index and __newindex. We’ll. I’ll try to explain it to you as best as i can:

__index

Were usin’ __index to link/connect our main table to the proxy one. This will copy all our values n’ methods inside the proxy table to the main table.

Whenever we try to write to our main table. Luau will check if the key(Herobrinekid1 in my case) exist in our main table which it doesnt. It’ll will then proceed to look if our main table has a metatable attached to it which it has. It’ll then look if we’re returnin’ a function or a table. In this case we’re returnin’ a table(Proxy) and return that one which contains our key. If none of those we’re found it’ll just return nil.

__newindex

Were usin’ __newindex to detect changes.

Normally __newindex fires when you try to set a value that’s nil. We can actually take advantage of this.

Since we’ve linked our main table to the proxy one. Whenever we try to change our keys value inside the main table(Which doesnt exist but does in the proxy one).

__newindex will fire because the key “Herobrinekid1” doesnt exist in our main table.

local MyTable = {}



setmetatable(MyTable,{
	__newindex = function(...)
		print(...)
	end,
})
MyTable.Player = 1

Im terribly sorry if this explanation was really confusin’ or wasnt good. I hope you atleast understood some parts haha. Explainin’ how __index/__newindex actually works in this case really took a toll on my neurons.

Now we can begin’ to compare both the values. Old/New to check if a change happened. If a our value isnt the same as the new one. We can overwrite the old one.

local Players = {}
local Proxy = {}



Proxy.Herobrinekid1 = 10 -- // For demonstration.



setmetatable(Players,{
	__index = Proxy,
	
	__newindex = function(Table, Key: string, NewValue: any)
		local OldValue = Table[Key]
		
		if OldValue ~= NewValue then
			Proxy[Key] = NewValue
		end
	end,
})


Now you’re basically done. This will register when a change happens in the main table.

Though if you also want to have the ability to add events such as Connect() follow this step. We’ll be usin’ a bindable event to make this easy.

local Players = {}
local Proxy = {}
local Event = Instance.new("BindableEvent")

Players.Changed = Event.Event
Proxy.Herobrinekid1 = 10 -- // For demonstration.



setmetatable(Players,{
	__index = Proxy,

	__newindex = function(Table, Key, NewValue)
		local OldValue = Table[Key]

		if OldValue ~= NewValue then
			Proxy[Key] = NewValue
			Event:Fire(OldValue,NewValue)
		end
	end,
})




Now you’re all set :smiley: now you can connect a listener to your table to catch changes. Example:

Players.Changed:Connect(function(...)
	print(...)
end)


Players.Herobrinekid1 = 5
task.wait(2)
Players.Herobrinekid1 = 10

Hope this was somewhat useful :smiley: this was my first ever tutorial ive written here.

If you have any feedback make sure to tell me. All feedback positive/negative is very useful.

17 Likes

This is called a Getter / Setter.

These don’t happen on Roblox often because most people using them don’t use them correctly and inefficient use of this can cause drawbacks.

Sorry didn’t realize this was a necro-post! For some reason it showed this as recently updated and in the recent feed so I didn’t bother checking the date. Probably someone deleted a message.

Hello there, what drawbacks could this cause? I wanna know if it causes any issues such as performance or anything else. ty

In my opinion, if you know what is changing data in a table, you can simply add your code after changing a tables data. I feel like there’s overhead for creating a metatable and metamethods if they’re not truly needed.

Now if you have a use case for random scripts adding random things to random tables (hard to think of a real life example for this) then I can see this being important. The only drawback I can see is if someone decides to do this for many tables.

As @debugMage said, it’s pretty unnecessary overhead to do it this way but is always an option. It’s way more practical to just know which changes you’re making, firing relevant events (like a BindableEvent), or causing other, already built-in events to fire (like “AttributeChanged”). A sort of simple idea is like this, how do we keep track of when a Player gains XP?

local playerStats = {
    TrippyGhostOG = 75
}

playerStats.TrippyGhostOG = 100

This doesn’t make it obvious that it changed as Roblox doesn’t have built-in support for Getter Setters, so here’s some workarounds to this issue (that aren’t Getter / Setters.)

  1. Using Attributes

Using attributes is sometimes a good idea when working with Players as you can connect the attributes directly to the Player instance. Of course, it’s also worth noting that these attributes will be visible to the client, so don’t show anything sensitive that a Player / other Players shouldn’t know. A nice thing about this is that when you do it this way, you can then use Roblox’s built-in AttributeChanged event. This isn’t the best idea, but for some specific use cases might just be the ideal choice.

local Players = game:GetService("Players")

Players.TrippyGhostOG.AttributeChanged:Connect(function(name)
    if name == "XP" then
        print("Your XP is now " .. Players.TrippyGhostOG:GetAttribute("XP")
    end
end)

Players.TrippyGhostOG:SetAttribute("XP", 100)
  1. Using Bindables

By far the best option is to instead create a function that calls a BindableEvent if the functionality needs to leave the current script. Of course I would like to mention again that these are just examples, not the best implementation. You’d want this to scale and also for this use-case save even after Player’s leave the session. I’m just giving a short run-down on how these behave. Here’s how this might look.

local statsUpdated = Instance.new("BindableEvent")
statsUpdated.Name = "StatsUpdated"
statsUpdated.Parent = script

local playerStats = {
    TrippyGhostOG = 75
}

local function updateStats(player: string, xp: number)
    playerStats[player] = xp
    statusUpdated:Fire(player, xp)
end

-- in another script
local statsUpdated = (location):WaitForChild("StatsUpdated")

statsUpdated.Event:Connect(function(player: string, xp: number)
    print(player .. "'s XP is now " .. xp)
end

Using ModuleScripts, by far the most common for modularity sake.

-- ModuleScript
local module = {}

module.stats = {
    TrippyGhostOG = 75
}

function module.update(player: string, xp: number)
    module.stats[player] = xp
    print(player .. "'s new XP is " .. xp)
    -- any other functionality you want, maybe return that it was successful?
end

return module

-- in another script
local playerStats = require(path.to.module)

playerStats.update("TrippyGhostOG", 100) -- "TrippyGhostOG's new XP is 100"

These are your main options, both of them having their own use-cases, but it’s typically way more common to see modularity. Making a system like the one I’ve defined modular is very useful especially when combined with a DataStore. Of course, how you implement your own systems is completely up to you, these are just some of the more common practices I see.