Vide Reactive Luau UI Library

repository·main·Indexed 43 days ago

https://github.com/centau/vide

A 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().

Tokens
14.4K
Snippets
49
Records
60
Agent score
53%

What's inside Vide

  1. Overview of Vide reactive UI library

    main
    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.
  2. Introduction to Vide

    main

    Vide 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.

  3. Use Effects to respond to data changes

    main

    An effect is a reactive operation that runs in response to a source update. You create an effect using the effect() 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)
  4. Create a Derived Source

    main

    A 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)
  5. How stable and reactive scopes work in Vide

    main

    Vide code operates within two types of scopes that determine how reactivity is handled:

    • Reactive scopes: These rerun automatically if any source read within the scope is updated. They are created using effect() or derive(). 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.

  6. How components work in Vide

    main

    In 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, use vide.create to 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
            }
        }
    end
  7. Choosing between indexes() and values()

    main

    Both 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.

  8. How effects work in vide

    main

    An 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" printed
  9. Use `derive()` to create efficient derived sources

    main

    When 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 the derive block 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 time
  10. Managing Roblox instances with cleanup()

    main

    In 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.

  11. Manage Scope Cleanup

    main

    Vide 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.
  12. Understand Scopes in Vide

    main

    Vide 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:

    1. Stable Scopes: Created via root(). They never rerun. They serve as the parent container for other scopes.
    2. 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