Thank you for this.
I’m wondering how I would set things up to be able to change the same State from several other modules. Something like this:
Module 1
local State = BasicState.new({
Hello = "World"
})
State:Set("Hello", "Roblox")
Module 2
State:Set("Hello", "there")
Module 3
State:Set("Hello", "again")
I would like that Module 2 and 3 change the State defined in module 1 and I’m not sure how I would set everything up to make this happen. Any help would be appreciated.
So you would basically need a separate module that contains the state.
State Module
local State = BasicState.new({
Hello = "World"
})
return State
Module 1
local State = require(StateModule)
State:Set("Hello", "Roblox")
… and so on. Once you initialise a module with the state object, any other scripts or modules which require it will receive access to the same state object.
This will let you do things such as allowing multiple scripts or modules to listen to changes or set the value of the same state object.
It’s more or less the same as how Rodux works, but less faffing about.
I’ve looked at Rodux before but it felt overly complicated for my needs and I had a hard time understanding it all. Your solution works great with a much simpler approach.
Added a new React-style SetState method which allows multiple values to be changed at once. This will still fire changed signals for each value which was changed.
The Set method now internally uses RawSet for setting values in the store.
local State = BasicState.new({
Greeting = "Hello",
Locale = {
French = "Salut",
Spanish = "Hola",
Polish = "Dzień dobry"
}
})
State:SetState({
Greeting = "Hi",
Locale = {
Polish = "Cześć"
}
})
--[[
The new state object will look like this:
{
Greeting = "Hi",
Locale = {
French = "Salut",
Spanish = "Hola",
Polish = "Cześć"
}
}
Original data is not lost; only applicable values are updated.
--]]
I heard about this module from @Alvin_Blox and thought it was super neat. I’ve made something extremely similar for my own work and would love to move over to this module, but I don’t feel this is ready for me to switch just yet.
One feature disparity that prevents me from switching to this is the lack of runtime type protection.
I’ve created a pull request that implements this with full backwards compatibility. The PR has an explanation of what it is and why it does it, but I’ll copy it here ease of reading for others.
PR Explanation
State.ProtectType
What is it?
State.ProtectType is a boolean propety that (when enabled, of course) prevents accidentally changing the type of a value that a key is storing. When enabled, you cannot overwrite a key that holds a string with a number value, etc. Using :RawSet() will ignore State.ProtectType in situations where you want to override this protection.
It is disabled by default in order to ensure backwards compatibility.
Why?
I like to be certain that state values will be a specific type without needing to do checks whenever I get them- it saves a lot of worrying because you know it can’t have accidentally been set to NaN or something like that. It gives your top level code more clarity and keeps it concise, while also giving you better peace of mind.
Usage:
local State = BasicState.new({
NestedTable = {
Key = "string type value"
}
})
State.ProtectType = true
State:Set("NestedTable",{Key = 123}) -- Errors due to nested type mismatch
Explanations for whoever code reviews this:
I added a small snippet into :Set() that does a type check on your values to protect you from changing types. It avoids checking when the original value is nil, because there you are allowed to initialize that key to any type, and then subsequent calls to that key will be checked against that newly set value’s type.
I moved JoinDictionary() into State.__joinDictionary() so that the function can have access to self so that it can check the value of self.ProtectType. I then altered all calls to this new function location. Lastly, I added a small snippet into the function that handles this type protection for the nested values in your State.
I made another PR that greatly improves the memory usage and performance of State:GetChangedSignal().
PR Explanation
Let’s take a look at a simple example- you have a few connections to one key change in various places of your codebase, and one connection to a different key.
State:GetChangedSignal(Key):Connect(function()
-- code
end)
State:GetChangedSignal(Key):Connect(function()
-- more code
end)
State:GetChangedSignal(Key):Connect(function()
-- even more code
end)
State:GetChangedSignal(Key):Connect(function()
-- yup, more code
end)
State:GetChangedSignal(DifferentKey):Connect(function()
-- code
end)
The current module will result in 5 BindableEvents and 5 Connections, since each call creates a new BindableEvent and Connection inside the module, plus the additional 5 Connections in our example code to bring the total to 10 Connections.
My changed module results in 2 BindableEvents and 1 Connection. The module has 1 Connection for all ChangedSignals, regardless of key, and 1 BindableEvent per requested Key with all subsequent calls to that Key sharing that single Instance. With our 5 in the top layer, that brings our Connection total to just 6.
This heavily saves on memory, and even helps performance noticeably.
Something I noticed and wanted to mention, but didn’t PR:
function State:Destroy()
... --irrelevant code
self = nil
end
Doing self = nil does nothing, because self is just a local scope variable. You’re clearing that reference to your table, but not actually clearing any table. Here’s how to prove that to yourself:
local BasicState = require(script.BasicState)
local State = BasicState.new({Key = true})
print(State.Changed)
State:Destroy()
print(State.Changed)
-- Output:
--> Signal Event
--> Signal Event
Hopefully you’ll find some ways for me to improve these during your review! I want to make sure we implement this as best we can. Feel free to DM me about this if you don’t want to bloat this thread or the PR comment threads.
Oh that’s awesome! I’ve just tested and merged these into the default branch. Thanks so much! It’s a great idea to implement type safety, and I must have completely overlooked the issue with :GetChangedSignal.
As for setting self to nil during :Destroy(), I realised this not long after I did it. I just forgot to remove it (and still have!). In another module I created, loosely based around the same concept, I have omitted this. I’ll have to remember to get rid of that in the next update.
These changes are now live in the Library and Releases, in addition to the docs. Will write an update post soon.
Happy to help! Thank you for adding sections for thanking contributors, feeling loved rn
Do you have a roadmap with planned updates, or are you just making it up as the needs arise? If there’s a feature list/discussion board I would be more than happy to get involved.
No problem! Well, you contributed, so you deserve a shout-out!
And nope! I’m making this up as I go along. I don’t receive much feedback on this, so I just add to it when I find there’s something I need or ideas come to me. You’re more than welcome to contact me or continue making PRs if you want anything else added/fixed.
local State = BasicState.new({
Hello = "World"
})
State.ProtectType = true
State:SetState({
Hello = 12345678 --> Will throw an error; a string was expected, but it received a number
})
It really is! I sort of borrowed the “wrapping” method they use for BasicState. You’re basically replacing export default view(<React.Component>) with return State:Roact(<Roact.Component>) (didn’t want to name the method anything else as this module can be used both with and without Roact).
Been looking at the documentation and wow this is so simple! I like how the methods follow the stuff from Roblox too! I will probably make my own version instead in the future, but for now this will serve my needs amaizingly
Introduced State.None, which allows for keys to be removed from the state table (similarly to Cryo). This is due to Lua unable to differentiate between nil and undefined, and therefore omits nil values from tables.
Added a new State:Delete method for removing keys from the state table. This is the same as doing State:Set("Key", State.None).
Patched State:Roact to prevent BasicState from updating a component’s state while unmounting. This should stop any warnings related to this issue you may have experienced.
I would personally recommend not using _G at all–if you had to share a table between scripts, contain it in a ModuleScript.
Here’s what BasicState offers that _G doesn’t as-is:
Type safety
Signals when content is updated
Deep merging (you can update tables within tables, etc)
Roact integration–Plug your data straight into Roact apps
Convenience methods for toggling booleans true/false and incrementing/decrementing numeric values
I have personally used it to:
Keep track of global state within Roact apps
Retrieve and store plugin settings (with the benefit of being able to add new setting options in an update without screwing up the user’s existing config)
Track users in my game when they enter/leave areas
Animate door models to open/close as players approach.
Also being a module, you can guarantee that your initial data is there to begin with; you don’t need to wait for it to be set by another script before you can use it, like you would have to with _G.