BasicState - Simple State Management

:warning: This project is now archived. No more updates will be made; however, both the repo and downloads will remain available.

BasicState

CI GitHub release (latest by date) Wally release (latest)

BasicState is a really simple key-value based state management solution. It makes use of BindableEvents to allow your project to watch for changes in state, and provides a simple but comprehensive API for communication with your state objects. Think Rodux, but easier!

Installation

Rojo

You can use git submodules to clone this repo into your project’s packages directory:

$ git submodule add https://github.com/csqrl/BasicState packages/BasicState

Once added, simply sync into Studio using the Rojo plugin.

0.5.x

Download/clone this repo on to your device, and copy the /src directory into your packages directory.

Wally

Add BasicState to your wally.toml and run wally install

[package]
name = "user/repo"
description = "My awesome Roblox project"
version = "1.0.0"
license = "MIT"
authors = ["You (https://github.com/you)"]
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"

[dependencies]
BasicState = "csqrl/BasicState@^0.2.6"
$ wally install

Roblox-TS (unofficial)

While this package doesn’t officially support TypeScript, bindings are available under the @rbxts/basicstate package, which can be installed using npm or yarn.

$ npm i @rbxts/basicstate
$ yarn add @rbxts/basicstate
$ pnpm add @rbxts/basicstate

TypeScript bindings are provided by @tech0tron. Please file any issues for the npm package over on their repo.

Manual Installation

Grab a copy from the Roblox Library (Toolbox), or download the latest .rbxm/.rbxmx file from the releases page and drop it into Studio.

Usage

Here’s a quick example of how BasicState can be used:

local BasicState = require(path.to.BasicState)

local State = BasicState.new({
    Hello = "World"
})

State:GetChangedSignal("Hello"):Connect(function(NewValue, OldValue)
    print(string.format("Hello, %s; goodbye %s!", NewValue, OldValue))
end)

State:SetState({
    Hello = "Roblox"
})

--[[
    Triggers the RBXScriptConnection above and prints
    "Hello, Roblox; goodbye World!"
--]]

Usage with Roact:

-- Store.lua
local BasicState = require(path.to.BasicState)

local Store = BasicState.new({
    Hello = "World"
})

return Store
-- MyComponent.lua
local Roact = require(path.to.Roact)
local MyComponent = Roact.Component:extend("MyComponent")

local Store = require(script.Parent.Parent.Store)

function MyComponent:render()
    return Roact.createElement("TextButton", {
        Text = string.format("Hello, %s!", self.state.Hello),
        --> Displays "Hello, World!"

        [Roact.Event.MouseButton1Click] = function()
            Store:SetState({ Hello = "Roblox" })
            --> Will re-render and display "Hello, Roblox!"
        end
    })
end

-- Wrap the component with the BasicState store
return Store:Roact(MyComponent)

Documentation

Please refer to the documentation site for a full overview of the exported API and further examples on how to use this module.

73 Likes

Is there any benefits to using this rather than something like Roact or Rodux?

TL;DR: It’s a lot less complicated!

BasicState is just a simpler way of handling state rather than learning Rodux! Rodux can be really complicated for a beginner to learn, and a little daunting. I would encourage people to make up their own mind on which state management solution to use, but obviously there are benefits to using both!

Rodux is based on Redux, a popular state management solution, typically used in conjunction with Roact/React. By learning to use Rodux, you’ll find that learning Redux is easier (and vice-versa), and have the benefit of being able to use a similar solution on the web and on Roblox.

BasicState provides a really simple solution to state management. It’s a lot easier to learn for a beginner, and is, on the whole, pretty self-explanatory. BasicState was actually designed to be used on my server, and I have a range of different uses for it. It handles caching of players’ save data before I upload it to a DataStore. It controls UI. It keeps ephemeral data on the server side, such as a player’s current location within the game. It’s wholly possible to complete these actions using Rodux too, but BasicState eliminates the need for lots of actions and reducers, and simplifies my project structure overall.

In addition, it follows more closely Roblox’s own style. It uses PascalCase for its events and methods, much like Roblox services, so you’ll find it’s much more intuitive to use. For example; Rodux’s change event vs. BasicState’s:

-- Rodux
Store.changed:connect(...)

-- BasicState
State.Changed:Connect(...)

I’m not entirely sure why Rodux even uses a lower-case convention, as this form is usually deprecated across the Roblox APIs.

BasicState also supports listening to individual keys within your state, just like you can with Instances, using the :GetChangedSignal method.

But as I said, it’s entirely up to you which solution you use. There’s no reason to use one over the other, other than preference and simplicity.

6 Likes

Sorry if what I am about to say doesn’t make sense, as I don’t use Rodux at all. What’s the benefit of using this or even Rodux over just changing a variable and calling a function when a button is clicked? I could just write a function ViewCategory(Name) or whatever you want it to be, and it can accomplish the same thing.

When it comes down to it, there’s really not much difference. However, it unlocks the potential for your projects to become much more powerful and declarative. It gives a clear structure to your projects (better organisation), and can make things much easier to work with. Additionally, using modules, you can share the state between scripts.

I found an article about state management here (specifically, in conjunction with React):

You don’t necessarily need state management for every project. Sometimes it’s easier without it, and sometimes you just don’t need it; it’s entirely your preference.

1 Like

Updated: 17th May 2020

I’ve added three new convenience methods: :Toggle() for toggling stored booleans, :Increment() for increasing the value of stored numeric values, and :Decrement() for decreasing the value of stored numeric values.

See more information in the Documentation section of the OP.

The update is live in the Library/Toolbox. Haven’t updated the Gist yet.

2 Likes

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