Chemical is a comprehensive Luau framework for Roblox game development, emphasizing a reactive programming paradigm. It aims to simplify state management, UI development, and server-client data synchronization by providing a suite of interconnected tools. With Chemical, developers can build dynamic and responsive user interfaces and game systems that automatically react to changes in their underlying data.
Key features include observable values, derived (computed) values, an ECS-backed architecture, declarative UI construction, client-side routing, and a powerful state replication system.
## 2. Core Philosophy
* **Reactivity:** State changes should automatically propagate through the application, updating dependent values and UI elements without manual intervention.
* **Declarative UI:** Describe *what* your UI should look like based on the current state, and let Chemical handle the updates.
* **Centralized State (Optional):** While not strictly enforcing a single global store, `Chemical.Reaction` facilitates managing and syncing shared game state.
* **Performance:** Leveraging an ECS backend and a custom packet system for efficient data handling and networking.
* **Developer Experience:** Providing clear, typed APIs (using Luau type annotations) and utilities to streamline common tasks.
## 3. Installation & Setup
1. Place the `Chemical` root `ModuleScript` (and its descendant file structure from the `.rbxm`) into a suitable location, typically `ReplicatedStorage` to be accessible by both server and client.
2. Ensure the `UseReactions` BoolValue in `Chemical/Configuration` is set to `true` if you intend to use the `Chemical.Reaction` state replication system. If `false`, attempting to use `Reaction` will result in a warning/error.
```lua
-- Accessing the Chemical library
local Chemical = require(game.ReplicatedStorage.Chemical)
```
## 4. Core Reactive Primitives
These are the fundamental building blocks for creating reactive data flows.
### Value`<T>`
The `Value<T>` object is the most basic reactive unit. It encapsulates a single piece of data that can be read and written. When its data changes, any `Computed` values or `Observer`s depending on it are notified.
*`:get(): T`: Retrieves the current value. If called within a `Computed` function or `Watch` target getter, it registers this `Value` as a dependency.
*`:set(newValue: T)`: Sets a new value. If the new value is different from the old, it triggers updates to dependents.
*`:increment(amount: number?)`: For numeric `Value`s. Increments the value by `amount` (defaults to 1).
*`:toggle()`: For boolean `Value`s. Flips the boolean state.
*`:key(key: any, newValue: any)`: For table `Value`s. Sets `tbl[key] = newValue` and triggers updates.
*`:insert(itemValue: any)`: For array-like table `Value`s. Equivalent to `table.insert(tbl, itemValue)`.
*`:remove(itemValue: any)`: For array-like table `Value`s. Removes the first occurrence of `itemValue`.
*`:destroy()`: Destroys the `Value` object, cleaning up its ECS entity and notifying dependent `Computed`s or `Observer`s (which may also destroy themselves).
**Example:**
```lua
local Chemical = require(path.to.Chemical)
local playerScore = Chemical.Value(0)
local isGameOver = Chemical.Value(false)
print(playerScore:get()) -- Output: 0
playerScore:increment(10)
print(playerScore:get()) -- Output: 10
isGameOver:set(true)
```
### Computed`<T>`
A `Computed<T>` object represents a value that is derived from one or more other reactive objects (`Value`s or other `Computed`s). It automatically re-evaluates its derivation function whenever any of its dependencies change, if the new value differs from the old it will appropriately cache the value and respond.
*`derivationFunction`: A function that returns the computed value. Any `Value:get()` or `Computed:get()` calls inside this function establish dependencies.
*`cleanupFunction` (optional): A function called with the *previous* computed value right before the `Computed` is re-cached due to a dependency change, or when the `Computed` object is destroyed. Useful for cleaning up side effects or resources tied to the old value.
*`:get(): T`: Retrieves the current computed value. If called within another `Computed` or `Observer`, it registers this `Computed` as a dependency.
*`:destroy()`: Destroys the `Computed` object, cleaning up its value if `cleanup` was provided as well as its ECS entity and notifying dependent `Computed`s or `Observer`s (which may also destroy themselves).
**Example:**
```lua
local Chemical = require(path.to.Chemical)
local firstName = Chemical.Value("Jane")
local lastName = Chemical.Value("Doe")
local isGameOver = Chemical.Value(true)
local fullName = Chemical.Computed(function()
return firstName:get() .. " " .. lastName:get()
end)
print(fullName:get()) -- Output: Jane Doe
firstName:set("John")
task.wait() --Computeds will run on the next frame after the change.
print(fullName:get()) -- Output: John Doe (automatically updated)
local characterDescription = Chemical.Computed(function()
local name = fullName:get() -- Dependency on another Computed
*`:onChange(callback: (newValue: any?, oldValue: any?) -> ()): () -> ()`: Registers a callback function to be invoked when the observed `target`'s value changes.
* Returns a `disconnectFunction` that, when called, unregisters this specific callback.
*`:destroy()`: Destroys the `Observer` and disconnects all its listeners.
**Example:**
```lua
local Chemical = require(path.to.Chemical)
local health = Chemical.Value(100)
local healthObserver = Chemical.Observer(health)
local disconnectHealthListener = healthObserver:onChange(function(newHealth, oldHealth)
print(string.format("Health changed from %s to %s", tostring(oldHealth), tostring(newHealth)))
end)
health:set(80) -- Output: Health changed from 100 to 80
health:set(80) -- No output (value didn't change)
health:set(95) -- Output: Health changed from 80 to 95
disconnectHealthListener() -- Stop listening for this specific callback
health:set(100) -- No output from the disconnected listener
healthObserver:destroy() -- Destroy the observer entirely
```
### Element
An `Element` is a specialized reactive state value, primarily designed for managing the visibility or active state of UI components, in conjunction with the `Chemical.Router`.
The different between `Element` and `Value` is that `Element`s have reactive parameters which can be retrieved and are set by the `Router`.
**API:**
*`Chemical.Element(): Element`: Constructor. Initializes with a state of `false`.
*`:get(): boolean`: Gets the current boolean state. Registers a dependency if used in a `Computed`.
*`:set(newState: boolean)`: Sets the boolean state.
*`:params(): { from?: string, [any]: any }` : This is the reactive parameter object, which can contain the reserved key `from`.
*`:params(newParams: { from?: string, [any]: any })`: Sets or gets an associated parameters table. The `Router` uses this to pass information like the previous path (`from`) when an element's state changes due to a route transition.
*`:onChange(callback: (newState: boolean, fromPath?: string) -> ()): () -> ()`: Listens for changes to the element's boolean state. The callback receives the new state and the `from` property of the current params. Returns a disconnect function.
*`:destroy()`: Destroys the `Element`.
*`.__persistent: boolean?`: (Internal property set by Router) If true, the Router will not automatically set this Element to `false` when navigating away from its associated path.
{Path = "/settings", Element = settingsPageElement}
})
-- Elsewhere (e.g., Router logic):
-- router:to("/settings", { message = "hello" })
```
### Watch
`Watch` allows you to observe a specific key within a table that is itself held by a `Value` or `Computed` object. The callback triggers only when the value associated with that *specific key* changes.
*`targetGetter`: A function that must return a table and a string:
* The `Value` or `Computed` object that holds the table you want to watch.
* The specific `key` within that table whose value changes you want to monitor.
* Any reactive `:get()` calls within will establish dependencies for re-evaluating which table/key to watch if those dependencies change (though the primary use is for a single reactive table).
*`callback`: A function invoked when the value of `targetTableContainer:get()[key]` changes. It receives the new and old values for that specific key.
* Can be static values: `Size = UDim2.fromScale(0.1, 0.1)`
* Can be a `Value` or `Computed` object: `Visible = myVisibilityValue` (where `myVisibilityValue` is a `Value<boolean>`). The UI property will automatically update when the reactive object changes.
* **`Parent: Instance | Value<Instance> | Computed<Instance>`**: Sets the parent of the created element. Can be static or reactive.
* **`Children: {GuiObject}`**: An array of other `Chemical.Create()` calls or static GuiObject references. The created child elements will be parented to this element.
* **UI Traits** (see below).
**Example (from `LocalScript Examples`):**
```lua
local Chemical = require(path.to.Chemical)
local Create = Chemical.Create
local Value = Chemical.Value
local PlayerGui = game.Players.LocalPlayer.PlayerGui
local frameColor = Value(Color3.fromRGB(200, 200, 200))
local childFrameRef = Value() -- Will hold the reference to the child frame
Applies reactive properties and traits to an *existing*`instance`. The `propertyTable` structure and capabilities are the same as for `Chemical.Create()`. This is useful for hydrating GUIs not created by Chemical or for applying reactive behavior incrementally.
```lua
local existingFrame = script.Parent.SomeFrame -- Assuming a Frame exists
When the UI element is created (by `Create`), or hydrated (by `Give`), `refValue:set(createdInstance)` is called. This allows you to get a reactive reference to the instance itself.
Connects to the specified `eventName` (e.g., "MouseButton1Click", "MouseEnter") of the UI element.
* If `callback` is a function, it's called when the event fires. Arguements of the event *are* passed to the function.
* If `callback` is a `Value<boolean>`, its value is set to `true` when the event fires. (The actual arguments of the event are not passed to the Value's set method).
* If `propertyName` is a string: Listens to `Instance:GetPropertyChangedSignal(propertyName)`.
* If `callback` is a function, it's called with the new property value.
* If `callback` is a `Value`, its `set` method is called with the new property value.
* If `propertyName` is a `Value`, `Computed`, or `Element` (reactive object): Creates an `Observer` for this reactive object.
* If `callback` is a function, it's called when the reactive object changes (receives `newValue, oldValue`).
* (Using a Value as callback for a reactive propertyName is less common and might imply a two-way binding if not careful, however it is permitted).
* You might use this for TextBox.Text properties, such that when the value of the TextBox changes so too does the reactive Value Object's value.
All connections made via these traits are automatically disconnected when the GuiObject they are attached to is destroyed (via `instance:Destroy()` or `Chemical.Destroy()`).
## 6. State Replication (`Chemical.Reaction`)
`Chemical.Reaction` is a singleton service that automates the synchronization of state between the server and connected clients. It supports replicating both static values and reactive `Value`/`Computed` objects.
**Key Characteristics:**
* **Channel & Key Identification:** Reactions are identified by a `(channelName: string, reactionKey: string)` pair.
* **One-Way Server-to-Client:** The primary flow of data is from server to client. Client-side changes to replicated state do not automatically propagate back to the server via this system.
* **Nested Structure:** Supports state tables with up to one level of nesting where the nested values can be `Value` objects.
```lua
-- Example of supported state structure for Reaction
local state = {
staticTopLevel = "hello",
reactiveTopLevel = Chemical.Value(10),
nestedObject = {
staticNested = true,
reactiveNested = Chemical.Value("world")
}
}
```
* **Tokenization:** Internally, channel names, reaction keys, and field keys are tokenized (converted to numbers) for efficient network transmission.
* **Initial Hydration:** When a client connects or is ready, it receives a full snapshot of all existing reactions it's concerned with.
* **Delta Updates (for `Value` objects):** Only changes to `Value` objects are networked after initial hydration. Changes to static parts of the state after creation are not automatically replicated. If a `Value` object itself holds a table, the system is *designed to* (aims to in the future) send only the changed parts of that table.
* Creates a new reaction on the server and broadcasts its construction to all clients.
*`initialState`: The table defining the reaction's state. Values within this table can be static Luau types or `Chemical.Value`/`Chemical.Computed` instances.
* Returns a `ServerReactionAPI` object with two methods:
*`destroy()`: Destroys the reaction on the server and notifies clients to deconstruct it. All reactive `Value` objects within its state are also destroyed.
*`raw()`: Returns a deep, non-reactive snapshot of the current state of the reaction. `Value` objects are replaced with their current values.
### Reaction Client-Side API
Accessible via `local Reaction = Chemical.Reaction()`.
* The resolved `ClientReaction` object mirrors the structure of the server's `initialState`, where server-side `Chemical.Value`/`Chemical.Computed` instances become client-side `Chemical.Value` instances. Static values remain static.
warn("Client: Failed to get PlayerData reaction:", err)
end)
-- Alternatively, listen to all reactions in a channel
-- local disconnectListener = Reaction:onCreate("PlayerData", function(reactionKey, reactionObject)
-- if reactionKey == tostring(localPlayer.UserId) then
-- print("Client: PlayerData (via onCreate) for me!", reactionObject)
-- -- setupPlayerUI(reactionObject)
-- -- if you use onCreate, you might want to manage disconnects or ensure UI is only set up once.
-- else
-- print("Client: PlayerData (via onCreate) for another player:", reactionKey)
-- end
-- end)
```
## 7. Client-Side Routing (`Chemical.Router`)
The `Chemical.Router` is a singleton service for managing client-side application flow by defining paths and associating them with reactive `Chemical.Element`s. When the route changes, corresponding `Element`s are activated or deactivated, typically controlling UI visibility.
*`router:paths(routes: {{ Path: string, Element: Chemical.Element, Persistent?: boolean }})`: Defines a set of routes.
*`Path`: A string like "/shop/items" or "/profile". Leading/trailing slashes are handled.
*`Element`: The `Chemical.Element` instance that will be set to `true` when this path is active.
*`Persistent` (optional, boolean): If true, the `Element` will not be automatically set to `false` when navigating to a sibling or parent. It will still be set to `false` if explicitly exited.
*`router:to(path: string, params?: table)`: Navigates to the specified `path`.
* Deactivates elements associated with the previous path (respecting persistence and shared ancestry).
* Activates the `Element` for the new `path`.
*`params` is an optional table passed to the target `Element`'s `:params()` method. `params.from` is automatically set to the old path.
*`router:is(path: string): boolean`: Returns `true` if the current path exactly matches the given `path`.
*`router:exit(path: string, params?: table)`: Explicitly deactivates the `Element` (and its descendant elements in the route tree) associated with the `path`. Sets `CurrentPath` to `""`.
*`router:onBeforeChange(callback: (newPath: string, oldPath: string) -> ())`: Registers a callback invoked before the current path changes and any `Element`s close.
*`router:onChange(callback: (newPath: string, oldPath: string) -> ())`: Registers a callback invoked when `CurrentPath`'s value changes. This is syntactic sugar for `Chemical.Observer(router.CurrentPath):onChange(...)`.
*`router:onAfterChange(callback: (newPath: string, oldPath: string) -> ())`: Registers a callback invoked after the current path has changed and target elements have been updated.
*`router.CurrentPath: Value<string>`: A reactive `Value` object holding the current active path string.
**Example (from `LocalScript Examples`):**
```lua
local Chemical = require(path.to.Chemical)
local Create = Chemical.Create
local Give = Chemical.Give
local Value = Chemical.Value
local Watch = Chemical.Watch
local Ref = Chemical.Ref
local PlayerGui = game.Players.LocalPlayer.PlayerGui
Yields the current thread until the provided `chemicalObject` (a `Value`, `Computed`, `Element`, or `Router`) changes its value at least once *after*`Await` is called. It resolves with no arguments once the change occurs. Essentially, it's a promise that resolves on the next change.
* **Note:** This uses `Promise.new` internally and `observer:onChange`. The actual return value `T` here is effectively `void` from the promise's perspective (it resolves with no values). Its primary use is to pause execution until a specific reactive data point is known to have received an update.
### Chemical Destroy
* **`Chemical.Destroy(subject: Destroyable)`**
A versatile cleanup function that attempts to properly destroy or disconnect various types of objects:
* Objects with a `:destroy()` (or `:Destroy()`) method (like Chemical primitives).
* Tables: Clears them and sets their metatable to nil. If elements within are `Destroyable`, recursively calls `Chemical.Destroy` on them.
* Roblox `Instance`s: Calls `instance:Destroy()`.
*`RBXScriptConnection`s: Calls `connection:Disconnect()`. - In the future, it will be possible to handle ConnectionLike objects.
* Functions: Calls the function (intended for disconnect functions).
The `Chemical.Reaction` system leverages these `Packet` definitions for its `Construct`, `Deconstruct`, `UpdateRoot`, `UpdateNested`, `Ready`, and `Hydrate` operations.
## 10. Type System
Chemical heavily utilizes Luau's type annotation system for improved code clarity, maintainability, and editor intellisense.
* **Root `Types.lua`:** Defines the public interface types for core Chemical objects like `Value<T>`, `Computed<T>`, `Observer`, `Element`, and `Reaction`.
* **`Types/Gui.lua` &`Types/Gui/Overrides.lua`:** Provide exhaustive type definitions for Roblox GUI object properties (`FrameProperties`, `TextLabelProperties`, etc.) and event names/signatures. These are crucial for the type-safe/intellisense usage of `Chemical.Create` and `Chemical.Give`.
* **Inline Type Annotations:** Throughout the codebase, functions, variables, and table fields are typed.
## 11. Examples
The `Chemical/Examples` (Script) and `Chemical/Examples` (LocalScript) provide practical demonstrations:
* **Server-Side (`Examples` Script):**
* Demonstrates creating a per-player `Reaction` with nested `Value` objects.
* Shows how the server defines the reaction's state and structure.
* The `export type Type` defines the expected structure of the client-side reaction object for better type safety on the client.
* **Client-Side (`Examples` LocalScript):**
* Uses `Chemical.Router` to define simple routes.
* Uses `Chemical.Create` to build UI elements.
* Demonstrates binding UI properties (like `Visible` and `BackgroundColor3`) to `Chemical.Element` and `Chemical.Value` objects, respectively.
* Shows how to use `[Chemical.Ref]` to get a reference to a created UI element.
* Illustrates connecting to UI events using `[Chemical.onEvent]`.
* Shows basic router navigation with `:to()` and `:exit()`.