# Chemical - Reactive State & UI Framework for Roblox Luau **Version:** 0.1.0b ALPHA (as of 6/3/2025, per comments) **Author:** Sovereignty (Discord: sov_dev) ## Table of Contents 1. [Introduction](#1-introduction) 2. [Core Philosophy](#2-core-philosophy) 3. [Installation & Setup](#3-installation--setup) 4. [Core Reactive Primitives](#4-core-reactive-primitives) * [Value``](#valuet) * [Computed``](#computedt) * [Observer](#observer) * [Element](#element) * [Watch](#watch) 5. [UI Creation & Management](#5-ui-creation--management) * [Create: `Chemical.Create()`](#chemicalcreate) * [Give: `Chemical.Give()`](#chemicalgive) * [UI Traits: `Ref`, `onEvent`, `onChange`](#ui-traits) 6. [State Replication (`Chemical.Reaction`)](#6-state-replication) * [Server-Side API](#reaction-server-side-api) * [Client-Side API](#reaction-client-side-api) * [Example Usage](#reaction-example-usage) 7. [Client-Side Routing (`Chemical.Router`)](#7-client-side-routing) 8. [Utility Functions](#8-utility-functions) * [Await: `Chemical.Await()`](#chemical-await) * [Destroy: `Chemical.Destroy()`](#chemical-destroy) * [Nothing: `Chemical.Nothing()`](#chemical-nothing) 9. [Under The Hood (Advanced)](#9-under-the-hood-advanced) * [Networking (`Suphi Packet`)](#networking) 10. [Type System](#10-type-system) 11. [Examples](#11-examples) --- ## 1. Introduction 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`` The `Value` 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. **API:** * `Chemical.Value(initialValue: T): Value`: Constructor. * `: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`` A `Computed` 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. **API:** * `Chemical.Computed(derivationFunction: () -> T, cleanupFunction?: (oldDerivedValue: T) -> ()): Computed`: Constructor. * `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 local status = isGameOver:get() return string.format("%s (Game Over: %s)", name, tostring(status)) end, function(oldDescription) --Optional Cleanup method print("CharacterDescription cleanup. Old value:", oldDescription) end) print(characterDescription:get()) -- Output: John Doe (Game Over: true) isGameOver:set(false) -- Triggers re-computation of characterDescription task.wait() print(characterDescription:get()) -- Output: John Doe (Game Over: false) isGameOver:set(false) -- Because isGameOver is already == false, it will not triggers re-computation of characterDescription -- Nor will it cause any observational changes/events to be triggered as the value of isGameOver did not change. -- This applies to computeds as well. ``` ### Observer An `Observer` allows you to react to changes in a `Value` or `Computed` object by executing a callback function. **API:** * `Chemical.Observer(target: Value | Computed): Observer`: Constructor. * `: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. **Example (Conceptual, often used with Router):** ```lua local Chemical = require(path.to.Chemical) local router = Chemical.Router local settingsPageElement = Chemical.Element() -- In UI Creation: -- Visible = settingsPageElement, router:paths({ {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. **API:** * `Chemical.Watch(targetGetter: () -> ({ targetTableContainer: Value<{[any]:any}> | Computed<{[any]:any}>, key: any }), callback: (newValueForKey: any?, oldValueForKey: any?) -> ()): () -> ()`: Constructor. * `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. * Returns a `disconnectFunction`. **Example:** ```lua local Chemical = require(path.to.Chemical) local userProfile = Chemical.Value({ username = "User123", score = 100, inventory = {"sword", "shield"} }) local disconnectUsernameWatch = Chemical.Watch( function() return userProfile:get(), "username" end, function(newUsername, oldUsername) print(string.format("Username changed from '%s' to '%s'", oldUsername, newUsername)) end ) local disconnectScoreWatch = Chemical.Watch( function() return userProfile:get(), "score" end, function(newScore, oldScore) print(string.format("Score changed from %s to %s", oldScore, newScore)) end ) -- Update the whole profile table userProfile:set({ username = "PlayerOne", score = 150, inventory = {"sword", "shield", "potion"} }) -- Output: -- Username changed from 'User123' to 'PlayerOne' -- Score changed from 100 to 150 -- Update using :key (also triggers Watch if the specified key is watched) userProfile:key("score", 200) -- Output: -- Score changed from 150 to 200 ``` `Watch` has a very specific use case in regards to `Element`s. ```lua local Chemical = require(path.to.Chemical) local router = Chemical.Router local someElement = Chemical.Element() router:paths({ { Path = "/settings", Element = someElement } }) local disconnect = Chemical.Watch( function() return someElement:params(), "message" end, function(new, old) print("New message: ", new) end ) router:to("/settings", { message = "hello" }) --Prints "New message: hello" --Element:params() can also be accessed directly, usually inside of an Observer after the Element's state has changed. ``` ## 5. UI Creation & Management Chemical provides a declarative API for creating and managing Roblox GUI elements, making it easy to bind UI properties to reactive state. ### Chemical.Create() `Chemical.Create(className: string): (propertyTable: dictionary): GuiObject` Creates a new instance of the specified `className` (e.g., "Frame", "TextLabel") and applies properties defined in `propertyTable`. This supports intellisense for each className of GuiObjects! **`propertyTable` Keys and Values:** * **Standard Properties:** (e.g., `Size`, `Position`, `BackgroundColor3`, `Text`, `Visible`) * 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`). The UI property will automatically update when the reactive object changes. * **`Parent: Instance | Value | Computed`**: 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 local mainFrame = Create("Frame"){ Size = UDim2.fromScale(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), AnchorPoint = Vector2.new(0.5, 0.5), BackgroundColor3 = frameColor, -- Reactive property Visible = true, Children = { Create("Frame"){ Name = "ChildInnerFrame", Size = UDim2.fromScale(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), AnchorPoint = Vector2.new(0.5, 0.5), BackgroundColor3 = Color3.fromRGB(100, 100, 100), Visible = true, [Chemical.Ref] = childFrameRef -- Assign instance to childFrameRef Value }, Create("TextButton"){ Name = "ColorChangeButton", Size = UDim2.fromOffset(100, 30), Position = UDim2.fromScale(0.5, 0.8), AnchorPoint = Vector2.new(0.5, 0.5), Text = "Change Color", [Chemical.onEvent("MouseButton1Click")] = function() frameColor:set(Color3.fromRGB(math.random(0, 255), math.random(0, 255), math.random(0, 255))) end } }, Parent = Create("ScreenGui"){ Name = "MyChemicalScreenGui", Parent = PlayerGui } } task.wait(2) print("Child frame reference:", childFrameRef:get().Name) -- Output: ChildInnerFrame ``` ### Chemical.Give() `Chemical.Give(instance: GuiObject): (propertyTable: dictionary): GuiObject` 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 local frameVisible = Chemical.Value(true) Chemical.Give(existingFrame) { Visible = frameVisible, --Reactive object value [Chemical.onChange("AbsoluteSize")] = function(newSize) print("Frame AbsoluteSize changed to:", newSize) end } task.wait(3) frameVisible:set(false) -- existingFrame will become invisible ``` ### UI Traits Special keys used within the `propertyTable` of `Create` and `Give` to add specific behaviors: * **`[Chemical.Ref] = refValue: Value`**: 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. * **`[Chemical.onEvent(eventName: string)] = callback: (() -> ()) | Value`**: 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`, 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). * **`[Chemical.onChange(propertyName: string | Value | Computed)] = callback: ((newValue: any) -> ()) | Value`**: * 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. * 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. * 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). 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` 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. ### Reaction Server-Side API Accessible via `local Reaction = Chemical.Reaction`. * `Reaction:create(channelName: string, reactionKeyName: string, initialState: table): ServerReactionAPI`: * 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()`. * `Reaction:await(channelName: string, reactionKeyName: string): Promise`: * Returns with the client-side reaction object once it has been constructed (either through initial hydration or a `ConstructReaction` packet). * 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. * `Reaction:onCreate(channelName: string, callback: (reactionKeyName: string, reactionObject: ClientReaction) -> ()): () -> ()`: * Subscribes to creations of new reactions within the specified `channelName`. * The `callback` is invoked immediately for any already existing reactions in that channel, and then for any new ones as they are constructed. * Returns a `disconnectFunction` to stop listening. ### Reaction Example Usage *(See `Chemical/Examples` script for a practical implementation which creates a `PlayerData` reaction per player.)* **Server (`ServerScriptService`):** ```lua local ReplicatedStorage = game:GetService("ReplicatedStorage") local Players = game:GetService("Players") local Chemical = require(ReplicatedStorage.Chemical) local Reaction = Chemical.Reaction -- Get the singleton instance Players.PlayerAdded:Connect(function(player) -- Define initial state, some parts reactive, some static local healthValue = Chemical.Value(100) local manaValue = Chemical.Value(50) local playerStatsState = { PlayerName = player.Name, -- Static UserId = player.UserId, -- Static Health = healthValue, -- Reactive Mana = manaValue, -- Reactive Inventory = { Gold = Chemical.Value(10), -- Nested reactive Items = {"Sword", "Shield"} -- Nested static } } -- Create the reaction for this player -- The "PlayerData" channel could hold reactions for all players local myReaction = Reaction:create("PlayerData", tostring(player.UserId), playerStatsState) print("Created reaction for player:", player.Name) -- Example of updating a reactive value after creation task.delay(5, function() if player and myReaction then -- Ensure player and reaction still exist print("Server: Setting health for", player.Name, "to 75") healthValue:set(75) -- This change will be replicated to clients end end) -- When player leaves, destroy their reaction player.Removing:Connect(function() if myReaction then print("Server: Destroying reaction for player:", player.Name) myReaction:destroy() end end) end) ``` **Client (`LocalScript` in `StarterPlayerScripts`):** ```lua local ReplicatedStorage = game:GetService("ReplicatedStorage") local Players = game:GetService("Players") local Chemical = require(ReplicatedStorage.Chemical) local Reaction = Chemical.Reaction -- Get the singleton instance local Create = Chemical.Create local localPlayer = Players.LocalPlayer local function setupPlayerUI(playerData) print("Client: Received PlayerData for", playerData.PlayerName, playerData) local screenGui = Create("ScreenGui"){ Parent = localPlayer.PlayerGui } Create("TextLabel"){ Name = "HealthDisplay", Size = UDim2.fromOffset(200, 30), Position = UDim2.fromScale(0.5, 0.1), AnchorPoint = Vector2.new(0.5, 0.5), Text = Chemical.Computed(function() -- Text reactively updates return string.format("Name: %s | Health: %d", playerData.PlayerName, playerData.Health:get()) end), Parent = screenGui, } Create("TextLabel"){ Name = "ManaDisplay", Size = UDim2.fromOffset(200, 30), Position = UDim2.fromScale(0.5, 0.15), AnchorPoint = Vector2.new(0.5, 0.5), Text = Chemical.Computed(function() return "Mana: " .. playerData.Mana:get() end), Parent = screenGui, } Create("TextLabel"){ Name = "GoldDisplay", Size = UDim2.fromOffset(200, 30), Position = UDim2.fromScale(0.5, 0.2), AnchorPoint = Vector2.new(0.5, 0.5), Text = Chemical.Computed(function() return "Gold: " .. playerData.Inventory.Gold:get() end), Parent = screenGui, } -- Observe changes locally if needed Chemical.Observer(playerData.Health):onChange(function(newHealth) print("Client: Health is now", newHealth) end) end -- Await this specific player's data Reaction:await("PlayerData", tostring(localPlayer.UserId)) :andThen(setupPlayerUI) :catch(function(err) 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 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. **API:** * `Chemical.Router: RouterInstance`: Gets the singleton router instance. * `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`: 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 local Router = Chemical.Router -- Get the singleton instance -- Define Elements for different pages/views local tutorialPageElement = Chemical.Element() local homePageElement = Chemical.Element() local tutorialFrame = Value() -- Define routes Router:paths({ { Path = "/tutorial", Element = tutorialPageElement }, { Path = "/home", Element = homePageElement, Persistent = true } }) -- Create UI that reacts to these elements Create("ScreenGui"){ Parent = PlayerGui, Children = { Create("Frame"){ -- Tutorial Page Name = "TutorialFrame", Size = UDim2.fromScale(0.8, 0.8), Position = UDim2.fromScale(0.5, 0.5), AnchorPoint = Vector2.new(0.5, 0.5), BackgroundColor3 = Color3.fromRGB(50, 50, 150), Visible = tutorialPageElement, -- Reactive visibility [Ref] = tutorialFrame, --Set the reference to this Frame Children = { Create("TextLabel"){ Text = "Tutorial Page", Size = UDim2.fromScale(1,0.1)} } }, Create("Frame"){ -- Home Page Name = "HomeFrame", Size = UDim2.fromScale(0.7, 0.7), Position = UDim2.fromScale(0.5, 0.5), AnchorPoint = Vector2.new(0.5, 0.5), BackgroundColor3 = Color3.fromRGB(50, 150, 50), Visible = homePageElement, -- Reactive visibility Children = { Create("TextLabel"){ Text = "Home Page", Size = UDim2.fromScale(1,0.1)} } } } } --When the tutorialPageElement params are changed by the Router, we'll apply the specific key `customParam`'s value to the tutorialFrame. Watch( function() return tutorialPageElement:params(), "customParam" end, function(new) Give(tutorialFrame:get()) { Name = new } end ) -- Listen to route changes Router:onChange(function(newPath, oldPath) print(string.format("Route changed from '%s' to '%s'", oldPath, newPath)) end) -- Navigate task.wait(1) print("Navigating to /tutorial") Router:to("/tutorial", { customParam = "hello from tutorial start" }) task.wait(3) print("Navigating to /home") Router:to("/home") -- tutorialPageElement becomes false, homePageElement becomes true task.wait(3) print("Navigating to /tutorial again") Router:to("/tutorial") -- homePageElement remains true (persistent), tutorialPageElement becomes true task.wait(3) print("Exiting /tutorial (not /home because it's persistent from this level)") Router:exit("/tutorial") -- tutorialPageElement becomes false task.wait(3) print("Exiting /home") Router:exit("/home") -- homePageElement becomes false ``` ## 8. Utility Functions ### Chemical Await * **`Chemical.Await(chemicalObject: Value | Computed): T`** 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). * Threads: Calls `task.cancel(thread)`. * Tables: Recurscively calls `Chemical.Destroy(Table)`. ### Chemical Nothing * **`Chemical.Nothing()`** As the name implies, it does nothing. This can be useful when you want to specify that the cleanup of a Computed or Iterator should do nothing. ## 9. Under The Hood (Advanced) ### Networking 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`, `Computed`, `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()`.