Skip to main content

Component

Components are classes that can be extended and built upon. Like Roact components, they represent a reusable object that you can create using constructors.

You can create and destroy Components using standard Luau class methods.

local AwesomeComponent = require("AwesomeComponent")
local Object = AwesomeComponent.new()
Object:Destroy()

To declare our first Component class, Helium provides the following API:

local Helium = require(ReplicatedStorage.Helium)
local MyComponent = Helium.Component.Extend("MyComponent")

When a new Component object is created using MyComponent.new(), the Constructor function is called with the same arguments passed through new. Here is a simple printer component:

local Printer = Helium.Component.Extend("Printer")

function Printer:Constructor(Message: string)
	self.Message = Message
end

function Printer:Print()
	print(self.Message)
end

local MyPrinter = Printer.new("Hello, World!")
MyPrinter:Print() -- Hello, World!
MyPrinter:Destroy() -- ( Currently has no effect, but is still a thing we can do )

While this has nothing to do with UI, it is a good example of the object-oriented structure we will be using for the rest of the tutorial.

UI components

Helium gives total control over what a component does when it is constructed. You can create as many Gui objects as you like, and update them however you like. The information we actually display to the user can be controlled using the Component class' :Redraw() method.

warning

Never ever call :Redraw() directly. This method is automatically called next RenderStepped, Heartbeat or Stepped event depending on what was set as the RedrawBinding.

To queue a redraw on the next frame, use self.QueueRedraw() instead. This is an anonymous, idempotent function that tells Helium to call :Redraw() automatically on the next RedrawBinding step. It should be noted that when a component is constructed, Helium automatically calls self.QueueRedraw() once.

We can control whether :Redraw() is called on by using the static RedrawBinding property of components. This is an Enum which you can access by doing Helium.RedrawBinding..

Let's say we wanted to create a CoinsDisplay component, which draws some representation of how many coins a player has.

local Helium = require(ReplicatedStorage.Helium)
local CoinsDisplay = Helium.Component.Extend("CoinsDisplay")

function CoinsDisplay:Constructor()
	self.Coins = 0

	self.Gui = Instance.new("ScreenGui")
	self.CoinsLabel = Instance.new("TextLabel")
	self.CoinsLabel.Size = UDim2.fromOffset(100, 100)
	self.CoinsLabel.Parent = self.Gui

	self.Gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")
end

function CoinsDisplay:AddCoin()
	self.Coins += 1
	self.QueueRedraw()
end

CoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat
function CoinsDisplay:Redraw()
	self.CoinsLabel.Text = self.Coins
end

-- Add 1 coin every second
local MyCoinsDisplay = CoinsDisplay.new()
while true do
	task.wait(1)
	MyCoinsDisplay:AddCoin()
end

ComponentExample

As you can see this component functions as intended. However, there is one small problem: What would happen if we were to destroy the CoinDisplay component?

local MyCoinsDisplay = CoinsDisplay.new()
MyCoinsDisplay:AddCoin()
MyCoinsDisplay:Destroy()

Bad

Now, wait a minute... why is the Gui still appearing? Furthermore, why are we seeing the text "Label" instead of the number 1 or 0? While it's true that the state of self.Coins should have been set to 1 after calling :AddCoin(), the MyCoinsDisplay object was destroyed before the next Heartbeat frame started.

Thus, even though self.QueueRedraw() was called, this line of code never ran, as Helium automatically unbinds queued redraws once a component is destroyed:

function CoinsDisplay:Redraw()
	self.CoinsLabel.Text = self.Coins
end

Since the Text property was never set, it was left with the default value of all TextLabel objects: "Label".

We also have one other problem: the Gui and coinsLabel objects are still parented to PlayerGui when CoinsDisplay:Destroy() is called. While we could define a destructor and remove them there:

function CoinsDisplay:Destroy() -- Note: Do not do this
	self.Gui:Destroy()
end
warning

Never overwrite the :Destroy method, doing so all but guarantees you'll have a major problem down the line.

The problem is that keeping track of every every object that is created can become unmanageable, especially after creating a large number of components

function MyApp:Constructor()
	self.MyComponentA = ComponentA.new(...)
	self.MyComponentB = ComponentB.new(...)
	self.MyComponentC = ComponentC.new(...)
	self.MyFrame = Instance.new("Frame")
end

function MyApp:Destroy() -- Note: Do not do this
	self.MyComponentA:Destroy()
	self.MyComponentB:Destroy()
	self.MyComponentC:Destroy()
	self.MyFrame:Destroy()
end

Seems like a lot of work, right? Now, if you want to add or remove elements from your UI Component, you have to also add or remove it from the Destructor. If you forget to do this, bad things can happen. Furthermore, what if components/Gui Objects are created during MyApp:Redraw() rather than MyComponent:Constructor()? Now you have to use an if statement to conditionally check if the object even exists, and if it does, destroy it in the destructor.

Helium utilizes the Janitor object for Component destructors. You can read more about it on the Janitor documentation site.

Going back to the CoinsDisplay example, our Janitor object can be utilized in the constructor as follows:

function CoinsDisplay:Constructor()
	self.Coins = 0

	self.Gui = self.Janitor:Add(Instance.new("ScreenGui"), "Destroy")
	self.CoinsLabel = self.Janitor:Add(Instance.new("TextLabel"), "Destroy")
	self.CoinsLabel.Size = UDim2.fromOffset(100, 100)
	self.CoinsLabel.Parent = self.Gui

	self.Gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")
end

:Add() is a special function that takes in an object that can be called. If the Janitor is given an Instance, then that instance will automatically be destroyed when the Component is destroyed. The first argument is meant for the object you are passing. The second argument is the either true for functions or a string for the name of the function to call. You can see the API for Janitor:Add for more information. When the Component is destroyed, the :Destroy() method of the Janitor will be called which in turn cleans up everything in the Janitor.


Improving our Component class

Now, I will attempt to explain some improvements that can be made to our CoinDisplay code. First of all, we don't actually need to create our gui objects until :Redraw() is called. For the sake of separation of concerns, it would be better to move that into the :Redraw() function.

function CoinsDisplay:Constructor()
	self.Coins = 0

	-- Note: self.QueueRedraw() is automatically called after the CoinsDisplay object is created
end

CoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat
function CoinsDisplay:Redraw()
	-- This will run once on the first frame that our CoinsDisplay element is rendered (if it is rendered)
	if not self.Gui then
		self.Gui = self.Janitor:Add(Instance.new("ScreenGui"), "Destroy")
		self.CoinsLabel = self.Janitor:Add(Instance.new("TextLabel"), "Destroy")
		self.CoinsLabel.Size = UDim2.fromOffset(100, 100)
		self.CoinsLabel.Parent = self.Gui

		self.Gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")
	end

	self.CoinsLabel.Text = self.Coins
end

See how much cleaner the constructor is? Now, when we want to locate the portion of code that draws what is displayed to the user, we need only look at the :Redraw() function. Secondly, we do not need to keep track of our CoinsLabel frame, as it is already parented to our Gui (we also do not need to give it to the Component's Janitor for that matter).

function CoinsDisplay:Redraw()
	if not self.Gui then
		self.Gui = self.Janitor:Add(Instance.new("ScreenGui"), "Destroy") -- Only the gui needs to be given to the janitor.

		local CoinsLabel = Instance.new("TextLabel")
		CoinsLabel.Name = "CoinsLabel"
		CoinsLabel.Size = UDim2.fromOffset(100, 100)
		CoinsLabel.Parent = self.Gui

		self.Gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")
	end

	self.Gui.CoinsLabel.Text = self.Coins -- Here we index gui instead of coinsLabel, I don't personally recommend this because it's extra indexing for no reason.
end

We deferred creation of our self.Gui object until :Redraw() is called by Helium. However, there is one small problem with our code:

self.Gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")

This :WaitForChild() is a yielding function. Yielding on a Redraw means our code could be subject to race conditions. In general, you should avoid yielding within :Redraw() wherever possible. Furthermore, it is not ideal to hardcode the parent in which our component's UI is placed. What if, for example, we wanted to nest a CoinsDisplay object inside of another menu? Let's define the parent in which we want to place the component as a parameter of the CoinsDisplay constructor:

function CoinsDisplay:Constructor(Parent: Instance)
	self.Coins = 0
	self.Parent = Parent
end

function CoinsDisplay:Redraw()
	if not self.Gui then
		...
		self.Gui.Parent = self.Parent
	end

	self.Gui.CoinsLabel.Text = self.Coins
end

Now, when we create our component, we should provide it with a parent argument:

local PlayerGui = Players.LocalPlayer:WaitForChild("PlayerGui")

-- Add 1 coin every second
local MyCoinsDisplay = CoinsDisplay.new(PlayerGui)
while true do
	task.wait(1)
	MyCoinsDisplay:AddCoin()
end

There is one other thing that will make the code simpler: UI Templates. Because Helium gives us full control over how our GUI elements are created, we can place a template inside of our component's module:

example

Final Code

Here is the final code for the CoinsDisplay module:

local Helium = require(ReplicatedStorage.Helium)
local CoinsDisplay = Helium.Component.Extend("CoinsDisplay")

function CoinsDisplay:Constructor(Parent: Instance)
	self.Coins = 0
	self.Parent = Parent
end

function CoinsDisplay:AddCoin()
	self.Coins += 1
	self.QueueRedraw()
end

CoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat
function CoinsDisplay:Redraw()
	if not self.Gui then
		self.Gui = self.Janitor:Add(script.CoinsDisplayTemplate:Clone(), "Destroy")
		self.Gui.Parent = self.Parent
	end

	self.Gui.CoinsLabel.Text = "Coins: " .. self.Coins
end

return CoinsDisplay

And here is a LocalScript that utilizes the module:

local CoinsDisplay = require(ReplicatedStorage.CoinsDisplay)
local PlayerGui = Players.LocalPlayer:WaitForChild("PlayerGui")

-- Add 1 coin every second
local MyCoinsDisplay = CoinsDisplay.new(PlayerGui)
while true do
	task.wait(1)
	MyCoinsDisplay:AddCoin()
end

example


1.2 Component State Reduction

The Observer Pattern

In order to understand how we can re-draw our components based on store updates, we must first look at the way in which the Helium Store propogates updates

As we saw in the last tutorial, reducers are given a special function SetState(), which mutates a value in the store.

Technically, for the root reducer, the actual function GetState() passed to the reducer is Store:GetState(), and the actual function SetState() is Store:SetState().

What Store:GetState(...KeyPath) does is parse a provided series of string keys until a value is found in the store. If a key does not exist at a given path, the store will return nil. For the sake of mutation safety, the store will NOT directly return tables in the store when calling Store:GetState(...); instead, tables will be deeply cloned, then returned.

If you want to keep a table in the store that is directly mutable when it is retrieved using get(), you can create a pointer to it by wrapping it in a function:

local function CreateFenv(Value: any)
	return function()
		return Value
	end
end

local Table = {}
Store:SetState("Table", CreateFenv(Table))

---...

local PointsToTable = Store:GetState("Table")
print(Table == PointsToTable()) -- true
RbxInstance CreateFenv(void) : (RbxInstance Value)
{
	return Value;
}

GenericDictionary Table = table();
Store::SetState(L"Table", CreateFenv:(Table));

// ...

void PointsToTable = Store::GetState @ Table;
print(Table == PointsToTable()); // true

Whereas GetState() does more than directly returning the value in the store, the function SetState() also does more than just mutating the value in the store. When store:SetState() is called, it keeps track of each key that was changed, and notifies any observers of the change.

We can observe store changes using the Store:Connect("Path.To.Key", Function) function. Unlike GetState and SetState, the key path is denoted using the dot notation. Subscribing to the empty string "" will observe all store changes.

Example:

local Store = Helium.Store.new(function()
end, {PlayerStats = {Coins = 0}})

local Disconnect = Store:Connect("PlayerStats.Coins", function()
	local Coins = Store:GetState("PlayerStats.Coins")
	print("You have", coins, "Coins")
end)

Store:SetState("PlayerStats", "Coins", 10) -- You have 10 Coins
Disconnect()
Store:SetState("PlayerStats", "Coins", 20) -- No output.

Observing with Components

Our Components can listen to changes in a store and automatically queue updates when a value in the store has changed. In order to do this, some preconditions need to be set:

  1. The component needs to know what store to observe changes from
  2. The component needs to know what key paths to subscribe to, and how to display them.

The first precondition is simple: We can simply pass the store in as an argument in the Component's constructor. In fact, Helium Components must receive a store as the first argument in their constructor in order to observe changes from that store.

While passing the same first argument through every single component down the tree of components may seem verbose, this actually makes it easy to differentiate "Container Components" (which are generally coupled with a particular segment of the application) from "Presentational Components" (which can generally be re-used throughout the application). More on that in a later tutorial.

function CoinsDisplay:Constructor(Store, Parent: Instance)
	self.Parent = Parent
	self.Store = Store
end

In this instance, we set self.Store = Store so that we can keep track of the store in case we need to give it to a nested component in our redraw function (similar to how we keep track of Parent in order to know where we should inevitably place the copy of our component's template). Now what we want is to subscribe to a value in the store (say, 'Coins'), and automatically call self.QueueRedraw() whenever this state changes. Helium provides an easy way of doing this for Components using a property called Reduction:

CoinsDisplay.Reduction = {Coins = "Store.Path.To.Coins"}

This will automatically subscribe new CoinsDisplay components from the keypath on the right-hand side ("Store.Path.To.Coins"), and map it to the value on the left-hand side ("Coins"). The reduced state will then be passed in as a table, as the first argument to CoinsDisplay:Redraw()

CoinsDisplay.Reduction = {Coins = "Store.Path.To.Coins"}
CoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat
function CoinsDisplay:Redraw(ReducedState)
	local Gui = self.Gui
	if not Gui then
		Gui = self.Janitor:Add(script.CoinsDisplayTemplate:Clone(), "Destroy")
		Gui.Parent = self.Parent
		self.Gui = Gui
	end

	-- Now we can display from ReducedState.Coins instead of self.Coins.
	-- In fact, we can get rid of self.Coins now that all our data is coming from the store.
	Gui.CoinsLabel.Text = "Coins: " .. ReducedState.Coins
end

We can now get rid of the self.coins property initialized in the constructor. In fact, we can also get rid of the CoinsDisplay:AddCoin() method we defined earlier, and replace it with actions such as ADD_COINS that we created in the last tutorial. Putting it all together:

Final Code

ReplicatedStorage.CoinsDisplay ModuleScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Helium = require(ReplicatedStorage.Helium)
local CoinsDisplay = Helium.Component.Extend("CoinsDisplay")

function CoinsDisplay:Constructor(Store, Parent: Instance)
	self.Store = Store
	self.Parent = Parent
end

type ReducedState = {Coins: number}

CoinsDisplay.Reduction = {Coins = ""} -- In this example, our store state is equivalent to coins
CoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat
function CoinsDisplay:Redraw(ReducedState: ReducedState)
	local Gui = self.Gui
	if not Gui then
		Gui = self.Janitor:Add(script.CoinsDisplayTemplate:Clone(), "Destroy")
		Gui.Parent = self.Parent
		self.Gui = Gui
	end

	Gui.CoinsLabel.Text = "Coins: " .. ReducedState.Coins
end

return CoinsDisplay

A LocalScript:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local CoinsDisplay = require(ReplicatedStorage.CoinsDisplay)
local Helium = require(ReplicatedStorage.Helium)

local PlayerGui = Players.LocalPlayer:WaitForChild("PlayerGui")

local AddCoins = Helium.MakeActionCreator("AddCoins", function(Amount: number)
	return {Amount = Amount}
end)

type BaseAction = {Type: string}

local function Reducer(Action: BaseAction, GetState, SetState)
	if Action.Type == AddCoins.ActionName then
		SetState(GetState() + Action.Amount)
	end
end

local InitialState = 0

-- Create the store
local CoinsStore = Helium.Store.new(Reducer, InitialState)

-- Mount the root component; notice how CoinsStore is given as the first argument
CoinsDisplay.new(CoinsStore, PlayerGui)

-- Add 1 coin every second (hopefully)
while true do
	local DeltaTime = task.wait(1)
	CoinsStore:Fire(AddCoins(math.floor(DeltaTime)))
end

This should function exactly the same as before, but this time our coins are pulling directly from the store, and listening to action dispatches. We also don't need to store our CoinsDisplay instance as a variable in this case, nor do we need to directly tell the CoinsDisplay component to increment the state of 'coins'.


info

All of this documentation is from the original Rocrastinate docs and was written by DataBrain, so 100% of the credit should go to him. All I did was modify it to fit Helium's API.

Types#

PossibleLifecycleEvents#

interface PossibleLifecycleEvents {
Destroyed: boolean?--

Whether or not you want to create the Destroyed event.

Destroying: boolean?--

Whether or not you want to create the Destroying event.

DidRedraw: boolean?--

Whether or not you want to create the DidRedraw event.

WillRedraw: boolean?--

Whether or not you want to create the WillRedraw event.

}

Functions#

Extend#

Component.Extend(
ClassName: string,--

The ClassName of the component. This is used for __tostring debug stuff.

LifecycleEventsToCreate: PossibleLifecycleEvents?--

The lifecycle events you want to create.

) → BaseComponent

Creates a new Component object.

Show raw api
{
    "functions": [
        {
            "name": "Constructor",
            "desc": "The base constructor function.",
            "params": [],
            "returns": [],
            "function_type": "method",
            "ignore": true,
            "source": {
                "line": 608,
                "path": "src/Component/init.lua"
            }
        },
        {
            "name": "Extend",
            "desc": "Creates a new Component object.",
            "params": [
                {
                    "name": "ClassName",
                    "desc": "The ClassName of the component. This is used for `__tostring` debug stuff.",
                    "lua_type": "string"
                },
                {
                    "name": "LifecycleEventsToCreate",
                    "desc": "The lifecycle events you want to create.",
                    "lua_type": "PossibleLifecycleEvents?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "BaseComponent"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 781,
                "path": "src/Component/init.lua"
            }
        }
    ],
    "properties": [],
    "types": [
        {
            "name": "PossibleLifecycleEvents",
            "desc": "",
            "fields": [
                {
                    "name": "Destroyed",
                    "lua_type": "boolean?",
                    "desc": "Whether or not you want to create the `Destroyed` event."
                },
                {
                    "name": "Destroying",
                    "lua_type": "boolean?",
                    "desc": "Whether or not you want to create the `Destroying` event."
                },
                {
                    "name": "DidRedraw",
                    "lua_type": "boolean?",
                    "desc": "Whether or not you want to create the `DidRedraw` event."
                },
                {
                    "name": "WillRedraw",
                    "lua_type": "boolean?",
                    "desc": "Whether or not you want to create the `WillRedraw` event."
                }
            ],
            "source": {
                "line": 764,
                "path": "src/Component/init.lua"
            }
        }
    ],
    "name": "Component",
    "desc": "Components are classes that can be extended and built upon. Like [Roact](https://github.com/Roblox/roact/ \"Roact by Roblox\") components, they represent a reusable object that you can create using constructors.\n\nYou can create and destroy Components using standard Luau class methods.\n\n```lua\nlocal AwesomeComponent = require(\"AwesomeComponent\")\nlocal Object = AwesomeComponent.new()\nObject:Destroy()\n```\n\nTo declare our first Component class, Helium provides the following API:\n\n```lua\nlocal Helium = require(ReplicatedStorage.Helium)\nlocal MyComponent = Helium.Component.Extend(\"MyComponent\")\n```\n\nWhen a new Component object is created using `MyComponent.new()`, the `Constructor` function is called with the same arguments passed through `new`. Here is a simple printer component:\n\n```lua\nlocal Printer = Helium.Component.Extend(\"Printer\")\n\nfunction Printer:Constructor(Message: string)\n\tself.Message = Message\nend\n\nfunction Printer:Print()\n\tprint(self.Message)\nend\n\nlocal MyPrinter = Printer.new(\"Hello, World!\")\nMyPrinter:Print() -- Hello, World!\nMyPrinter:Destroy() -- ( Currently has no effect, but is still a thing we can do )\n```\n\nWhile this has nothing to do with UI, it is a good example of the object-oriented structure we will be using for the rest of the tutorial.\n\n### UI components\n\nHelium gives total control over what a component does when it is constructed. You can create as many Gui objects as you like, and update them however you like.\nThe information we actually display to the user can be controlled using the Component class' `:Redraw()` method.\n\n:::warning\nNever ever call `:Redraw()` directly. This method is automatically called next `RenderStepped`, `Heartbeat` or `Stepped` event depending on what was set as the `RedrawBinding`.\n:::\n\nTo queue a redraw on the next frame, use `self.QueueRedraw()` instead. This is an anonymous, idempotent function that tells Helium to call `:Redraw()` automatically on the next `RedrawBinding` step.\nIt should be noted that when a component is constructed, Helium automatically calls `self.QueueRedraw()` once.\n\nWe can control whether `:Redraw()` is called on by using the static `RedrawBinding` property of components. This is an Enum which you can access by doing `Helium.RedrawBinding.`.\n\nLet's say we wanted to create a `CoinsDisplay` component, which draws some representation of how many coins a player has.\n\n```lua\nlocal Helium = require(ReplicatedStorage.Helium)\nlocal CoinsDisplay = Helium.Component.Extend(\"CoinsDisplay\")\n\nfunction CoinsDisplay:Constructor()\n\tself.Coins = 0\n\n\tself.Gui = Instance.new(\"ScreenGui\")\n\tself.CoinsLabel = Instance.new(\"TextLabel\")\n\tself.CoinsLabel.Size = UDim2.fromOffset(100, 100)\n\tself.CoinsLabel.Parent = self.Gui\n\n\tself.Gui.Parent = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\nend\n\nfunction CoinsDisplay:AddCoin()\n\tself.Coins += 1\n\tself.QueueRedraw()\nend\n\nCoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat\nfunction CoinsDisplay:Redraw()\n\tself.CoinsLabel.Text = self.Coins\nend\n\n-- Add 1 coin every second\nlocal MyCoinsDisplay = CoinsDisplay.new()\nwhile true do\n\ttask.wait(1)\n\tMyCoinsDisplay:AddCoin()\nend\n```\n\n![ComponentExample](https://i.imgur.com/QqGKiJs.gif)\n\nAs you can see this component functions as intended. However, there is one small problem: What would happen if we were to destroy the CoinDisplay component?\n\n```lua\nlocal MyCoinsDisplay = CoinsDisplay.new()\nMyCoinsDisplay:AddCoin()\nMyCoinsDisplay:Destroy()\n```\n\n![Bad](https://github.com/headjoe3/Rocrastinate/blob/master/docs/introduction_coins_example2.png?raw=true)\n\nNow, wait a minute... why is the Gui still appearing? Furthermore, why are we seeing the text \"Label\" instead of the number 1 or 0?\nWhile it's true that the state of `self.Coins` should have been set to 1 after calling `:AddCoin()`, the `MyCoinsDisplay` object was destroyed before the next `Heartbeat` frame started.\n\nThus, even though `self.QueueRedraw()` was called, this line of code never ran, as Helium automatically unbinds queued redraws once a component is destroyed:\n\n```lua\nfunction CoinsDisplay:Redraw()\n\tself.CoinsLabel.Text = self.Coins\nend\n```\n\nSince the Text property was never set, it was left with the default value of all TextLabel objects: \"Label\".\n\nWe also have one other problem: the `Gui` and `coinsLabel` objects are still parented to PlayerGui when `CoinsDisplay:Destroy()` is called. While we could define a destructor and remove them there:\n\n```lua\nfunction CoinsDisplay:Destroy() -- Note: Do not do this\n\tself.Gui:Destroy()\nend\n```\n\n:::warning\n**Never** overwrite the `:Destroy` method, doing so all but guarantees you'll have a major problem down the line.\n:::\n\nThe problem is that keeping track of every every object that is created can become unmanageable, especially after creating a large number of components\n\n```lua\nfunction MyApp:Constructor()\n\tself.MyComponentA = ComponentA.new(...)\n\tself.MyComponentB = ComponentB.new(...)\n\tself.MyComponentC = ComponentC.new(...)\n\tself.MyFrame = Instance.new(\"Frame\")\nend\n\nfunction MyApp:Destroy() -- Note: Do not do this\n\tself.MyComponentA:Destroy()\n\tself.MyComponentB:Destroy()\n\tself.MyComponentC:Destroy()\n\tself.MyFrame:Destroy()\nend\n```\n\nSeems like a lot of work, right? Now, if you want to add or remove elements from your UI Component, you have to also add or remove it from the Destructor. If you forget to do this, bad things can happen.\nFurthermore, what if components/Gui Objects are created during `MyApp:Redraw()` rather than `MyComponent:Constructor()`? Now you have to use an if statement to conditionally check if the object even\nexists, and if it does, destroy it in the destructor.\n\nHelium utilizes the Janitor object for Component destructors. You can read more about it on the [Janitor documentation site](https://howmanysmall.github.io/Janitor/).\n\nGoing back to the CoinsDisplay example, our `Janitor` object can be utilized in the constructor as follows:\n\n```lua\nfunction CoinsDisplay:Constructor()\n\tself.Coins = 0\n\n\tself.Gui = self.Janitor:Add(Instance.new(\"ScreenGui\"), \"Destroy\")\n\tself.CoinsLabel = self.Janitor:Add(Instance.new(\"TextLabel\"), \"Destroy\")\n\tself.CoinsLabel.Size = UDim2.fromOffset(100, 100)\n\tself.CoinsLabel.Parent = self.Gui\n\n\tself.Gui.Parent = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\nend\n```\n\n`:Add()` is a special function that takes in an object that can be called. If the Janitor is given an Instance, then that instance will automatically be destroyed when the Component is destroyed.\nThe first argument is meant for the object you are passing. The second argument is the either `true` for functions or a string for the name of the function to call. You can see the API for [Janitor:Add](https://howmanysmall.github.io/Janitor/api/Janitor#Add) for more information.\nWhen the Component is destroyed, the `:Destroy()` method of the Janitor will be called which in turn cleans up everything in the Janitor.\n\n----\n\n### Improving our Component class\n\nNow, I will attempt to explain some improvements that can be made to our `CoinDisplay` code.\nFirst of all, we don't actually need to create our gui objects until `:Redraw()` is called. For the sake of separation of concerns, it would be better to move that into the `:Redraw()` function.\n\n```lua\nfunction CoinsDisplay:Constructor()\n\tself.Coins = 0\n\n\t-- Note: self.QueueRedraw() is automatically called after the CoinsDisplay object is created\nend\n\nCoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat\nfunction CoinsDisplay:Redraw()\n\t-- This will run once on the first frame that our CoinsDisplay element is rendered (if it is rendered)\n\tif not self.Gui then\n\t\tself.Gui = self.Janitor:Add(Instance.new(\"ScreenGui\"), \"Destroy\")\n\t\tself.CoinsLabel = self.Janitor:Add(Instance.new(\"TextLabel\"), \"Destroy\")\n\t\tself.CoinsLabel.Size = UDim2.fromOffset(100, 100)\n\t\tself.CoinsLabel.Parent = self.Gui\n\n\t\tself.Gui.Parent = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\n\tend\n\n\tself.CoinsLabel.Text = self.Coins\nend\n```\n\nSee how much cleaner the constructor is? Now, when we want to locate the portion of code that draws what is displayed to the user, we need only look at the `:Redraw()` function.\nSecondly, we do not need to keep track of our CoinsLabel frame, as it is already parented to our Gui (we also do not need to give it to the Component's Janitor for that matter).\n\n```lua\nfunction CoinsDisplay:Redraw()\n\tif not self.Gui then\n\t\tself.Gui = self.Janitor:Add(Instance.new(\"ScreenGui\"), \"Destroy\") -- Only the gui needs to be given to the janitor.\n\n\t\tlocal CoinsLabel = Instance.new(\"TextLabel\")\n\t\tCoinsLabel.Name = \"CoinsLabel\"\n\t\tCoinsLabel.Size = UDim2.fromOffset(100, 100)\n\t\tCoinsLabel.Parent = self.Gui\n\n\t\tself.Gui.Parent = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\n\tend\n\n\tself.Gui.CoinsLabel.Text = self.Coins -- Here we index gui instead of coinsLabel, I don't personally recommend this because it's extra indexing for no reason.\nend\n```\n\n---\n\nWe deferred creation of our `self.Gui` object until `:Redraw()` is called by Helium. However, there is one small problem with our code:\n\n```lua\nself.Gui.Parent = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\n```\n\nThis `:WaitForChild()` is a yielding function. Yielding on a Redraw means our code could be subject to race conditions. In general, you should avoid yielding within `:Redraw()` wherever possible.\nFurthermore, it is not ideal to hardcode the parent in which our component's UI is placed. What if, for example, we wanted to nest a `CoinsDisplay` object inside of another menu? Let's define the\nparent in which we want to place the component as a parameter of the `CoinsDisplay` constructor:\n\n```lua\nfunction CoinsDisplay:Constructor(Parent: Instance)\n\tself.Coins = 0\n\tself.Parent = Parent\nend\n\nfunction CoinsDisplay:Redraw()\n\tif not self.Gui then\n\t\t...\n\t\tself.Gui.Parent = self.Parent\n\tend\n\n\tself.Gui.CoinsLabel.Text = self.Coins\nend\n```\n\nNow, when we create our component, we should provide it with a parent argument:\n\n```lua\nlocal PlayerGui = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\n\n-- Add 1 coin every second\nlocal MyCoinsDisplay = CoinsDisplay.new(PlayerGui)\nwhile true do\n\ttask.wait(1)\n\tMyCoinsDisplay:AddCoin()\nend\n```\n\nThere is one other thing that will make the code simpler: UI Templates. Because Helium gives us full control over how our GUI elements are created, we can place a template inside of our component's module:\n\n![example](https://github.com/headjoe3/Rocrastinate/blob/master/docs/introduction_coins_example3.png?raw=true)\n\n## Final Code\n\nHere is the final code for the CoinsDisplay module:\n\n```lua\nlocal Helium = require(ReplicatedStorage.Helium)\nlocal CoinsDisplay = Helium.Component.Extend(\"CoinsDisplay\")\n\nfunction CoinsDisplay:Constructor(Parent: Instance)\n\tself.Coins = 0\n\tself.Parent = Parent\nend\n\nfunction CoinsDisplay:AddCoin()\n\tself.Coins += 1\n\tself.QueueRedraw()\nend\n\nCoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat\nfunction CoinsDisplay:Redraw()\n\tif not self.Gui then\n\t\tself.Gui = self.Janitor:Add(script.CoinsDisplayTemplate:Clone(), \"Destroy\")\n\t\tself.Gui.Parent = self.Parent\n\tend\n\n\tself.Gui.CoinsLabel.Text = \"Coins: \" .. self.Coins\nend\n\nreturn CoinsDisplay\n```\n\nAnd here is a LocalScript that utilizes the module:\n\n```lua\nlocal CoinsDisplay = require(ReplicatedStorage.CoinsDisplay)\nlocal PlayerGui = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\n\n-- Add 1 coin every second\nlocal MyCoinsDisplay = CoinsDisplay.new(PlayerGui)\nwhile true do\n\ttask.wait(1)\n\tMyCoinsDisplay:AddCoin()\nend\n```\n\n![example](https://github.com/headjoe3/Rocrastinate/blob/master/docs/introduction_coins_example4.gif?raw=true)\n\n------\n\n# 1.2 Component State Reduction\n\n## The Observer Pattern\n\nIn order to understand how we can re-draw our components based on store updates, we must first look at the way in which the Helium Store propogates updates\n\nAs we saw in the last tutorial, reducers are given a special function `SetState()`, which mutates a value in the store.\n\nTechnically, for the root reducer, the actual function `GetState()` passed to the reducer is `Store:GetState()`, and the actual function `SetState()` is `Store:SetState()`.\n\nWhat `Store:GetState(...KeyPath)` does is parse a provided series of string keys until a value is found in the store. If a key does not exist at a given path, the store will return `nil`. For the sake of mutation safety, the store will NOT directly return tables in the store when calling `Store:GetState(...)`; instead, tables will be deeply cloned, then returned.\n\nIf you want to keep a table in the store that is directly mutable when it is retrieved using `get()`, you can create a pointer to it by wrapping it in a function:\n\n```lua\nlocal function CreateFenv(Value: any)\n\treturn function()\n\t\treturn Value\n\tend\nend\n\nlocal Table = {}\nStore:SetState(\"Table\", CreateFenv(Table))\n\n---...\n\nlocal PointsToTable = Store:GetState(\"Table\")\nprint(Table == PointsToTable()) -- true\n```\n\n```cpp\nRbxInstance CreateFenv(void) : (RbxInstance Value)\n{\n\treturn Value;\n}\n\nGenericDictionary Table = table();\nStore::SetState(L\"Table\", CreateFenv:(Table));\n\n// ...\n\nvoid PointsToTable = Store::GetState @ Table;\nprint(Table == PointsToTable()); // true\n```\n\nWhereas `GetState()` does more than directly returning the value in the store, the function `SetState()` also does more than just mutating the value in the store. When `store:SetState()` is called, it keeps track of each key that was changed, and notifies any observers of the change.\n\nWe can observe store changes using the `Store:Connect(\"Path.To.Key\", Function)` function. Unlike `GetState` and `SetState`, the key path is denoted using the dot notation. Subscribing to the empty string `\"\"` will observe all store changes.\n\nExample:\n\n```lua\nlocal Store = Helium.Store.new(function()\nend, {PlayerStats = {Coins = 0}})\n\nlocal Disconnect = Store:Connect(\"PlayerStats.Coins\", function()\n\tlocal Coins = Store:GetState(\"PlayerStats.Coins\")\n\tprint(\"You have\", coins, \"Coins\")\nend)\n\nStore:SetState(\"PlayerStats\", \"Coins\", 10) -- You have 10 Coins\nDisconnect()\nStore:SetState(\"PlayerStats\", \"Coins\", 20) -- No output.\n```\n\n## Observing with Components\n\nOur Components can listen to changes in a store and automatically queue updates when a value in the store has changed. In order to do this, some preconditions need to be set:\n1. The component needs to know what store to observe changes from\n2. The component needs to know what key paths to subscribe to, and how to display them.\n\nThe first precondition is simple: We can simply pass the store in as an argument in the Component's constructor. **In fact, Helium Components must receive a store as the first argument in their constructor in order to observe changes from that store**.\n\nWhile passing the same first argument through every single component down the tree of components may seem verbose, this actually makes it easy to differentiate \"Container Components\" (which are generally coupled with a particular segment of the application) from \"Presentational Components\" (which can generally be re-used throughout the application). More on that in a later tutorial.\n\n```lua\nfunction CoinsDisplay:Constructor(Store, Parent: Instance)\n\tself.Parent = Parent\n\tself.Store = Store\nend\n```\n\nIn this instance, we set `self.Store = Store` so that we can keep track of the store in case we need to give it to a nested component in our redraw function (similar to how we keep track of `Parent` in order to know where we should inevitably place the copy of our component's template).\nNow what we want is to subscribe to a value in the store (say, 'Coins'), and automatically call `self.QueueRedraw()` whenever this state changes. Helium provides an easy way of doing this for Components using a property called `Reduction`:\n\n```lua\nCoinsDisplay.Reduction = {Coins = \"Store.Path.To.Coins\"}\n```\n\nThis will automatically subscribe new CoinsDisplay components from the keypath on the right-hand side (`\"Store.Path.To.Coins\"`), and map it to the value on the left-hand side (`\"Coins\"`). The reduced state will then be passed in as a table, as the first argument to `CoinsDisplay:Redraw()`\n\n```lua\nCoinsDisplay.Reduction = {Coins = \"Store.Path.To.Coins\"}\nCoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat\nfunction CoinsDisplay:Redraw(ReducedState)\n\tlocal Gui = self.Gui\n\tif not Gui then\n\t\tGui = self.Janitor:Add(script.CoinsDisplayTemplate:Clone(), \"Destroy\")\n\t\tGui.Parent = self.Parent\n\t\tself.Gui = Gui\n\tend\n\n\t-- Now we can display from ReducedState.Coins instead of self.Coins.\n\t-- In fact, we can get rid of self.Coins now that all our data is coming from the store.\n\tGui.CoinsLabel.Text = \"Coins: \" .. ReducedState.Coins\nend\n```\n\nWe can now get rid of the `self.coins` property initialized in the constructor. In fact, we can also get rid of the `CoinsDisplay:AddCoin()` method we defined earlier, and replace it with actions such as `ADD_COINS` that we created in the last tutorial. Putting it all together:\n\n## Final Code\n\n### ReplicatedStorage.CoinsDisplay ModuleScript\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Helium = require(ReplicatedStorage.Helium)\nlocal CoinsDisplay = Helium.Component.Extend(\"CoinsDisplay\")\n\nfunction CoinsDisplay:Constructor(Store, Parent: Instance)\n\tself.Store = Store\n\tself.Parent = Parent\nend\n\ntype ReducedState = {Coins: number}\n\nCoinsDisplay.Reduction = {Coins = \"\"} -- In this example, our store state is equivalent to coins\nCoinsDisplay.RedrawBinding = Helium.RedrawBinding.Heartbeat\nfunction CoinsDisplay:Redraw(ReducedState: ReducedState)\n\tlocal Gui = self.Gui\n\tif not Gui then\n\t\tGui = self.Janitor:Add(script.CoinsDisplayTemplate:Clone(), \"Destroy\")\n\t\tGui.Parent = self.Parent\n\t\tself.Gui = Gui\n\tend\n\n\tGui.CoinsLabel.Text = \"Coins: \" .. ReducedState.Coins\nend\n\nreturn CoinsDisplay\n```\n\n### A LocalScript:\n```lua\nlocal Players = game:GetService(\"Players\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal CoinsDisplay = require(ReplicatedStorage.CoinsDisplay)\nlocal Helium = require(ReplicatedStorage.Helium)\n\nlocal PlayerGui = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\n\nlocal AddCoins = Helium.MakeActionCreator(\"AddCoins\", function(Amount: number)\n\treturn {Amount = Amount}\nend)\n\ntype BaseAction = {Type: string}\n\nlocal function Reducer(Action: BaseAction, GetState, SetState)\n\tif Action.Type == AddCoins.ActionName then\n\t\tSetState(GetState() + Action.Amount)\n\tend\nend\n\nlocal InitialState = 0\n\n-- Create the store\nlocal CoinsStore = Helium.Store.new(Reducer, InitialState)\n\n-- Mount the root component; notice how CoinsStore is given as the first argument\nCoinsDisplay.new(CoinsStore, PlayerGui)\n\n-- Add 1 coin every second (hopefully)\nwhile true do\n\tlocal DeltaTime = task.wait(1)\n\tCoinsStore:Fire(AddCoins(math.floor(DeltaTime)))\nend\n```\n\nThis should function exactly the same as before, but this time our coins are pulling directly from the store, and listening to action dispatches. We also don't need to store our `CoinsDisplay` instance as a variable in this case, nor do we need to directly tell the CoinsDisplay component to increment the state of 'coins'.\n\n---\n\n:::info\nAll of this documentation is from the original [Rocrastinate docs](https://github.com/headjoe3/Rocrastinate/tree/master/docs) and was written by DataBrain, so 100% of the credit should go to him.\nAll I did was modify it to fit Helium's API.\n:::",
    "source": {
        "line": 602,
        "path": "src/Component/init.lua"
    }
}