BasicState - Simple State Management

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.

3 Likes

That worked great! I really appreciate your help.

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.

1 Like

Updated: 06th June 2020

  • The .Changed (and consequently :GetChangedSignal) event will no longer fire if the value being set is the same as the already stored value.
  • Added :RawSet() method for adding to state without firing a .Changed event.
  • A link to this topic is included in the module for easier access to documentation.

See more information in Documentation in this topic’s original post.

The update is now live in the Library.

2 Likes

Updated: 03rd August 2020

  • 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.

For more information, visit the Documentation site.

The update is now live in the Library and available to download via GitHub.

Updated: 25th August 2020

:sparkles: Roact Wrapping :sparkles:

  • New experimental Roact method which allows BasicState to inject state into Roact components.
  • No dependency on Roact – you can continue to use BasicState as normal, no need to have Roact present in your game hierarchy.
  • Specify whether to use the entire state or selected keys.
  • Automatically handles mounting and unmounting of state connections.
  • Hook your existing BasicState up to your Roact project.
  • Control and modify state within Roact projects and outside of Roact simultaneously.
  • Continue to use as normal within Roact components – modify state as you would without Roact.

For more information, visit the Documentation site.

An example on how to use the new Roact method is available on GitHub to download or sync in via Rojo.

The update is now live in the Library and available to download via GitHub.

2 Likes

Updated: 27th August 2020

:sparkles: Deep Merging :sparkles:

  • Tables can now be set in the store–this was due to the table’s memory address not changing. Tables are now copied before being stored.
  • Updating a nested table (or any sub-tables) will cause change events to fire.
  • Tables are now stored using deep merging techniques.

For more information, visit the Documentation site.

Deep Merging Example
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.
--]]

The update is now live in the Library and available to download via GitHub.

1 Like

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.

3 Likes

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.

1 Like

Happy to help! Thank you for adding sections for thanking contributors, feeling loved rn :smiling_face_with_three_hearts:

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.

1 Like

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. :smiley:

1 Like

Updated: 03rd September 2020

:sparkles: Type Safety ft. @boatbomber :sparkles:

Thanks to the amazing @boatbomber, strict type-checking is now available (in addition to some optimisations regarding :GetChangedSignal().

  • Setting .ProtectType to true will enable strict type checking for your state object(s).
  • Attempting to change the type of a stored value will throw an error with ProtectType = true.
  • :GetChangedSignal() now returns the RBXScriptSignal for existing events, rather than creating a new one each time it’s called.

For more information, visit the Documentation site.

Strict Type Checking Example
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
})

The update is now live in the Library and available to download via GitHub.

6 Likes

Honestly, it would be nice if there was a state library like this for React, because Redux is way to complicated.

1 Like

Actually, the idea for this came from a nice little state management library I found and started using in React!

1 Like

Oh my god. That is the easiest state management plugin I’ve seen.

1 Like

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).

1 Like

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 :slight_smile:

1 Like

Updated: 13th November 2020

:sparkles: Delete Everything! (+Patches) :sparkles:

  • 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.

For more information, visit the Documentation site.

The update is now live in the Library and available to download via GitHub.

1 Like

Whats the pro’s of using this over _G table?

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.

1 Like