TableTrackers and Detecting Table Changes

The TableTracker class is a utility class that allows you to track changes to a table. You can bind a listener function to a specific path within the table, and the listener will be called whenever the value at that path is changed.

Here is an example of how to use the TableTracker class:

-- Create a new TableTracker instance and set its initial value to a table with some initial data.
local tracker = TableTracker.new({
    foo = {
        bar = "hello"
    }
})

-- Bind a listener to the "foo" key in the table.
local fooUnlistener = tracker:Bind({"foo"}, function(newValue)
    print("The 'foo' key has changed to: " .. tostring(newValue))
end)

-- Bind a listener to the "bar" key within the "foo" key.
local unlistener = tracker:Bind({"foo", "bar"}, function(newValue)
    print("The 'foo.bar' key has changed to: " .. tostring(newValue))
end)

-- Set the value at the "foo" key to a new table. This will cause the "foo" listener to be called.
tracker:Set({"foo"}, {
    bar = "hello"
})

-- Set the value at the "foo.bar" key to "goodbye". This will cause the "foo.bar" listener to be called.
tracker:Set({"foo", "bar"}, "goodbye")

-- Unbind the "foo.bar" listener.
unlistener()

-- Set the value at the "foo.bar" key to "hi". This will not cause the "foo.bar" listener to be called, since it was unbound earlier.
tracker:Set({"foo", "bar"}, "hi")

The TableTracker class has the following methods:

TableTracker.new(value)

This method creates a new TableTracker instance. The value parameter is optional and specifies the initial value of the tracked table. If no value is specified, the tracked table will be initialized to an empty table.

TableTracker:Bind(path, listenerFunc)

This method binds a listener function to the specified path in the tracked table. The path parameter is a table of keys representing the path to the value that the listener should be bound to. The listenerFunc parameter is a function that will be called whenever the value at the specified path changes. The function will be passed the new value as its only argument.

This method returns a TrackerListener instance that can be used to unbind the listener from the TableTracker instance.

TableTracker:Get(path)

This method gets the value at the specified path in the tracked table. The path parameter is a table of keys representing the path to the value that should be retrieved.

TableTracker:Set(path, value)

This method sets the value at the specified path in the tracked table. The path parameter is a table of keys representing the path to the value that should be set. The value parameter is the new value that should be set at the specified path.

This method will cause any listener functions bound to the specified path or any of its parent paths to be called with the new value.

Here are examples of how to use the Get and Set methods of the TableTracker class:

-- Create a new TableTracker instance and set its initial value to a table with some initial data.
local tracker = TableTracker.new({
    foo = {
        bar = "hello"
    }
})

-- Get the value at the "foo" key.
local fooValue = tracker:Get({"foo"})

-- The value of fooValue will be the table { bar = "hello" }.

-- Set the value at the "foo.bar" key to "goodbye".
tracker:Set({"foo", "bar"}, "goodbye")

-- Get the value at the "foo.bar" key.
local barValue = tracker:Get({"foo", "bar"})

-- The value of barValue will be "goodbye".

A data structure like the TableTracker class can be extremely powerful because it allows you to track changes to a table in a very flexible and efficient way. This can be useful in a variety of situations, such as:

  • When you want to be notified whenever a specific value in a table changes, so that you can take some action in response to the change.
  • When you want to update other parts of your code whenever a specific value in a table changes, without having to manually check for changes yourself.
  • When you want to track changes to a table that is shared by multiple parts of your code, and you want to ensure that all parts of your code are notified of any changes that happen to the table.

Overall, a data structure like the TableTracker class can help you write cleaner, more maintainable code by providing a simple and efficient way to track changes to a table.

Download Link

Roblox Library: Table Tracker - Roblox
Roblox Model: TableTracker.rbxm (3.4 KB)

6 Likes

Your model isn’t for sale however fantastic work! Also, I recommend deleting the other post you made about this as it is duplicated.

Thank you for letting me know it’s not on sale. It’s actually another resource but for variable changes detections instead.

1 Like

Why do you have to set and get through the module? You should be able to do that directly with the table.

thing.foo.bar = "Yes"
--> The 'foo.bar' key has changed to: Yes

Rbxconnectsignal???

The point of doing that is to have a signal when a value changes. Whole purpose of the module is this.

@FuzzyEq also could you upload the source code to github?

You don’t need this type of interface to create a signal. You can use metatables for this.

As far as I know, you can’t detect value changes with metatables and you are questioning the purpose of the module. With your code, you can’t detect changes, which he made a wrapper for it.

This can detect changes of tables. Achieved by using metatables like I said. The only thing is that it doesn’t detect tables inside of other tables, but you should just make another detector for that.

--\\ Detector Module:

local Detector = {}
local Connection = {}
Detector.__index = Detector
Connection.__index = Connection

function Detector.new(table)
    local self = {}
    self.Binds = {}
    self.VirtualProperties = {}

    setmetatable(table, {
        __index = function(_, i)
            return self.VirtualProperties[i]
        end,
        __newindex = function(_, i, v)
            local old = self.VirtualProperties[i]
            if old == v then return end
            self:_CallBinds(i, old, v)
            self.VirtualProperties[i] = v
        end,
    })
    
    return setmetatable(self, Detector)
end

function Detector:Bind(callback) 
    local connection = {}
    connection.Callback = callback
    connection.Property = nil
    connection.Detector = self
    setmetatable(connection, Connection)
    table.insert(self.Binds, connection)
    return connection
end

function Detector:BindProperty(property, callback) 
    local connection = {}
    connection.Callback = callback
    connection.Property = property
    connection.Detector = self
    setmetatable(connection, Connection)
    table.insert(self.Binds, connection)
    return connection
end

function Detector:_CallBinds(property, old, new)
    for _, bind in pairs(self.Binds) do 
        if bind.Property == property then
            task.spawn(bind.Callback, old, new)
        elseif not bind.Property then
            task.spawn(bind.Callback, property, old, new)
        end
    end
end

Detector.__tostring = function() return "ChangeDetector" end

--\\ Connection Methods

function Connection:Disconnect()
    table.remove(self.Detector.Binds, table.find(self.Detector.Binds, self))
    setmetatable(self, {})
end

Try testing this code with that module:

local thing = {}
local detect = Detector.new(thing)

-- Bind to any property change:

local connection = detect:Bind(function(property, old, new)
    print(property .. " was changed to " .. tostring(new))
end)

thing.Success1 = 1
thing.Success2 = 2
connection:Disconnect()
thing.Success3 = 3 -- won't call anything

-- Bind to specific properties:

local connection = detect:BindProperty("Success3", function(old, new)
    print("Success3 was changed to", new)
end)

thing.Success1 = -1
thing.Success2 = -2
thing.Success3 = -3
thing.Success3 = -3 -- wont 'change' value twice
connection:Disconnect()
thing.Success3 = 1 -- won't call anything
2 Likes

I created a class that does not utilize metatables due to a particular reason. I was able to devise an alternate solution that achieved the desired outcome without them.

1 Like

I wish Roblox were to add a __changed metamethod for metatables, but they literally said they will not be adding it because of “impact on performance” and it cannot be implemented for “in various cases where metamethods are already ignored”. :disappointed_relieved:

Link: Better Way to Detect Changes In Tables

I believe a feature like this is essential and of utmost importance. While having to rely on suboptimal methods currently is not ideal, they are suitable for the moment.