Vide Reactive Luau UI Library
repository·main·Indexed 43 days ago
https://github.com/centau/videA reactive Luau UI library inspired by SolidJS for building declarative, type-safe user interfaces. Vide features a reactive model using source(), derive(), and effect() for state management, along with utilities like create() for UI instantiation, spring() for physics-based animations, and dynamic rendering tools such as show(), switch(), indexes(), and values().
What's inside Vide
- Vide is a reactive Luau UI library inspired by SolidJS. It provides a declarative and concise syntax for building user interfaces that are reactively driven. The library is designed to be fully Luau typecheckable, making it suitable for robust development in Luau environments.
Introduction to Vide
mainVide is a reactive and declarative UI library heavily inspired by Solid. It focuses on a data-oriented approach to programming UIs, emphasizing how data flows through an application.
Key design principles include:
- Minimal syntax: Designed to be lightweight.
- Data oriented: Focuses on the data driving the UI.
- Typechecking compatible: Works well with type systems.
- Instance independent: Decoupled from specific instances.
Note on Reactivity: Vide uses the concept of scopes to manage reactivity. While this introduces a learning curve, it is the mechanism that enables Vide's minimal syntax.
Use Effects to respond to data changes
mainAn
effectis a reactive operation that runs in response to a source update. You create an effect using theeffect()function. Effects are part of the reactive graph and will rerun whenever the sources they depend on change.local count = source(0) effect(function() print("The count is: " .. count()) end)Create a Derived Source
mainA derived source is a new source composed of other sources. It automatically updates when its dependencies change. You can create one using a plain function or explicitly with the
derive()function.local count = source(0) local text = derive(function() return "count: " .. count() end)How stable and reactive scopes work in Vide
mainVide code operates within two types of scopes that determine how reactivity is handled:
- Reactive scopes: These rerun automatically if any
sourceread within the scope is updated. They are created usingeffect()orderive(). Reactive scopes cannot be created directly inside another reactive scope. - Stable scopes: These never rerun. They are created using
root().
Lifecycle and Constraints:
- When a scope is destroyed, all child scopes created within it are also destroyed.
- Yielding is strictly prohibited in both stable and reactive scopes. Strict mode will check for this.
Use stable scopes for initialization or one-off logic, and reactive scopes for logic that must respond to state changes.
- Reactive scopes: These rerun automatically if any
How components work in Vide
mainIn Vide, a component is a function that creates and returns a piece of UI. Components allow you to break your interface into small, reusable chunks.
To define a component, create a function that accepts a single parameter,
props, which is used to pass properties into the component. Inside the function, usevide.createto construct the UI elements. You can then compose these components by calling them within other components or UI trees.local create = vide.create -- A component definition local function Button(props: { Position: UDim2, Text: string, Activated: () -> () }) return create "TextButton" { Position = props.Position, Text = props.Text, Activated = props.Activated, -- ... other properties } end -- Using a component inside another local function Menu() return create "ScreenGui" { Button { Position = UDim2.fromOffset(200, 200), Text = "back", Activated = function() print("back") end } } endChoosing between indexes() and values()
mainBoth functions allow you to map table entries to UI elements, but they differ in performance and behavior based on how the source table changes.
Use
values()when:- You have a fixed set of objects that can be re-arranged (e.g., Player lists, Inventories, Chat history, Toast notifications).
- You want to animate re-ordering of instances (the source index can be used to animate position changes).
- You map a value to a UI element.
Use
indexes()when:- Your source table contains primitive values.
- You map an index to a UI element (e.g., character or weapon stats).
Performance Note: Choosing the correct function can reduce the number of property updates and re-renders.
How effects work in vide
mainAn effect is a function that runs in response to source updates. It is analogous to a connection in a signal-connection model. When you create an effect using
effect(), the function runs its callback immediately once to track which sources are being read. Any source (or derived source) accessed within the effect is automatically tracked; the effect will re-run whenever those sources are updated with a new value.Key behaviors:
- Automatic Tracking: Any source read inside the effect is tracked, regardless of how deeply nested it is within function calls.
- Value Equality Check: If a source is updated with a value identical to its current value, dependent effects will not re-run.
- Derived Sources: Effects track derived sources just like direct sources.
local source = vide.source local effect = vide.effect local count = source(0) effect(function() print("count: " .. count()) end) -- "count: 0" printed count(1) -- "count: 1" printedUse `derive()` to create efficient derived sources
mainWhen a source's value needs to be transformed (e.g., converting a number to a string) and that transformed value is consumed by multiple effects, using a standard function wrapper causes the transformation to rerun for every single effect.
To optimize this, use
vide.derive(). This creates a new source that runs its transformation function within a reactive scope. The result is cached, so multiple effects reading the derived source will receive the cached value without triggering redundant computations. The transformation only reruns when one of the underlying sources used inside thederiveblock updates.local source = vide.source local effect = vide.effect local derive = vide.derive local count = source(0) -- This transformation runs only once per update, even with multiple effects local text = derive(function() print "ran" return "count: " .. tostring(count()) end) effect(function() text() end) effect(function() text() end) count(1) -- prints "ran" only 1 timeManaging Roblox instances with cleanup()
mainIn Roblox, instances are freed when parented to
nil, so explicit destruction is often unnecessary for memory management.Warning: If you connect a function that references an instance to an event on that same instance, the instance will create a circular reference to itself and will never be freed. In these cases, you must use
cleanup()to disconnect the connection or explicitly destroy the instance.Manage Scope Cleanup
mainVide handles automatic memory and lifecycle management through scope cleanup:
- Automatic Destruction: When a scope is rerun or destroyed, all scopes created within it are automatically destroyed.
- Manual Cleanup: You can queue specific functions to run during destruction using the
cleanup()function.
Understand Scopes in Vide
mainVide uses Scopes to manage the lifecycle of connections and effects. Instead of manually disconnecting individual signals, you can group them within a scope. When a scope is destroyed, all scopes and effects created within it are also destroyed, preventing memory leaks (e.g., when an effect holds a reference to a UI instance).
There are two types of scopes:
- Stable Scopes: Created via
root(). They never rerun. They serve as the parent container for other scopes. - Reactive Scopes: Created via
effect(). They can rerun when their dependencies change.
Crucial Rules for Scopes:
- A scope must be created within another scope (except for the initial
root()). - A reactive scope (
effect) cannot be created inside another reactive scope; it must be created within a stable scope. - Destroying a parent scope automatically destroys all child scopes.
local root = vide.root local source = vide.source local effect = vide.effect local count = source(0) local function setup() effect(function() print(count()) end) end -- setup() -- This would error if called directly because effect() needs a stable scope local destroy = root(setup) -- OK: effect() is called inside the stable scope provided by root() count(1) -- prints "1" count(2) -- prints "2" destroy() -- Destroys the root scope and the effect inside it count(3) -- The effect is gone; nothing is printed- Stable Scopes: Created via