update docs

This commit is contained in:
Joe 2026-02-09 21:47:49 -06:00
parent 0b1e8c89dc
commit 7b73720143
586 changed files with 5616 additions and 1325 deletions

View file

@ -0,0 +1,83 @@
//import { defineConfig } from "vitepress"
import { withMermaid } from "vitepress-plugin-mermaid";
// https://vitepress.dev/reference/site-config
export default withMermaid({
title: "Vide",
titleTemplate: ":title - A reactive UI library for Luau",
description: "A reactive UI library for Luau.",
base: "/vide/",
head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
themeConfig: {
logo: "/logo.svg",
search: {
provider: "local"
},
footer: {
message: 'Released under the MIT License.',
},
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Home", link: "/" },
{ text: "Tutorials", link: "/tut/crash-course/1-introduction" },
{ text: "API", link: "/api/reactivity-core"},
],
sidebar: {
"/api/": [
{
text: "API",
items: [
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
{ text: "Reactivity: Dynamic Scoping", link: "/api/reactivity-dynamic" },
{ text: "Element Creation", link: "/api/creation" },
{ text: "Animation", link: "/api/animation" },
{ text: "Strict Mode", link: "/api/strict-mode" },
]
}
],
"/tut/": [
{
text: "Crash Course",
items: [
{ text: "Introduction", link: "/tut/crash-course/1-introduction" },
{ text: "Element Creation", link: "/tut/crash-course/2-creation" },
{ text: "Components", link: "/tut/crash-course/3-components" },
{ text: "Sources", link: "/tut/crash-course/4-source" },
{ text: "Effects", link: "/tut/crash-course/5-effect" },
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
{ text: "Reactive Components", link: "/tut/crash-course/7-reactive-component" },
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
{ text: "Dynamic Scopes", link: "/tut/crash-course/11-dynamic-scopes" },
{ text: "Actions", link: "/tut/crash-course/12-actions" },
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
]
},
{
text: "Advanced",
items: [
{ text: "Dynamic Scopes", link: "/tut/advanced/dynamic-scopes"}
]
},
{
text: "Design Patterns",
items: [
]
}
],
},
socialLinks: [
{ icon: "github", link: "https://github.com/centau/vide" }
]
}
})

View file

@ -0,0 +1,25 @@
.home > * > .VPNavBar:not(.top) {
backdrop-filter: blur(0.5rem);
background-color: transparent !important;
}
.home > * > .VPNavBar > .divider {
opacity: 0;
}
.home > .VPContent {
display: flex;
justify-content: center;
flex-direction: column;
}
.VPHome {
margin-top: auto !important;
margin-bottom: auto !important;
}
.VPBadge a {
text-decoration: none;
color: inherit
}

View file

@ -0,0 +1,8 @@
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './vars.css'
import './index.css'
export default {
extends: DefaultTheme,
}

View file

@ -0,0 +1,52 @@
/* Colors */
:root {
--vp-c-brand-1: #3086ff;
--vp-c-brand-2: #75aeff;
--vp-button-brand-bg: #3661a2;
--vp-button-brand-hover-bg: #24447f;
--vp-button-brand-press-bg: #4896f3;
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#3661a2,
#4896f3
);
--vp-home-hero-logo-background: -webkit-linear-gradient(
120deg,
#3661a2,
#4896f3
);
--vp-home-hero-image-filter: blur(96px);
--vp-c-bg: #f2f5f8;
--vp-c-bg-alt: #dfe8f5;
--vp-c-bg-elv: #dde7f4;
--vp-c-bg-soft: #e8f1fe;
--vp-c-border: #c0c3c6;
--vp-c-divider: #dfe2e6;
--vp-c-gutter: #dfe2e6;
--vp-plugin-tabs-tab-bg: var(--vp-c-bg);
--vp-badge-info-bg: #122d26;
--vp-badge-info-text: #6bdbbd;
--vp-badge-tip-bg: #132741;
--vp-badge-tip-text: #70abfa;
}
.dark {
--vp-c-brand-2: #234782;
--vp-c-bg: #0d131b;
--vp-c-bg-alt: #111720;
--vp-c-bg-elv: #182231;
--vp-c-bg-soft: #182231;
--vp-c-border: #111720;
--vp-c-divider: #1d273c;
--vp-c-gutter: #181d27;
}

View file

@ -0,0 +1,50 @@
# Animation
## spring() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Returns a new source with a value always moving torwards the input source value.
- **Type**
```luau
function spring<T>(
source: () -> T & Animatable,
period: number = 1,
damping_ratio: number = 1
): (() -> T, SpringControl<T>)
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
type SpringControl<T> = ({
position: T?,
velocity: T?,
impulse: T?
}) -> ()
```
- **Details**
Creates a reactive scope internally to detect source updates.
The movement is physically simulated according to a
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
`period` is the amount of time in seconds it takes for the spring to
complete one full cycle if undamped.
`damping_ratio` is the amount of resistance applied to the spring.
- \>1 = Overdamped - slowly reaches target without any overshoot.
- 1 = Critically damped - reaches target without any overshoot.
- <1 = Underdamped - reaches target with some overshoot.
- 0 = Undamped - never stabilizes, oscillates forever.
By default, the spring solver is ran at 120 Hz in
[`Heartbeat`](https://create.roblox.com/docs/reference/engine/classes/RunService#Heartbeat).
You can change when the solver runs by calling `vide.step(dt)`, which will
advance the simulation time by `dt` seconds and automatically stop the
solver running in heartbeat.
::: warning
Large periods or damping ratios can break the spring.
:::

View file

@ -0,0 +1,152 @@
# Element Creation
## create()
Creates a new UI element, applying any given properties.
- **Type**
```luau
function create(class: string): (Properties) -> Instance
function create(instance: Instance): (Properties) -> Instance
type Properties = Map<string|number, unknown>
```
- **Details**
The function can take either a `string` or an `Instance` as its first argument.
- If given a `string`, a new instance with the same class name will be created.
- If given an `Instance`, a new instance that is a clone of the given instance
will be created.
This returns another function that is used to apply any properties to the new
instance.
- **Property setting rules**
- **index is string:**
- **value is function:**
- **property is event:** connect function as callback
- **property is not event:** create effect to update property
- **value is not function:** set property to value
- **index is number:**
- **value is action:** run action
- **value is table:** recurse table
- **value is function:** create effect to update children
- **value is instance:** set instance as child
- **Example**
Basic element creation.
```luau
local frame = create "TextButton" {
Name = "Button",
Size = UDim2.fromOffset(200, 160),
Activated = function()
print "clicked"
end,
create "UICorner" {}
}
```
## action()
Creates a special object that can be passed to `create()` to invoke custom
actions on instances.
- **Type**
```luau
function action((Instance) -> (), priority: number = 1): Action
```
- **Details**
When passed to `create()`, the function is called with the instance being
created as the only argument. Actions take precedence over property and
child assignments.
A priority can be optionally specified to ensure certain actions run after
other actions. Lower priority values are ran first.
- **Example**
An action to listen to changed properties:
```luau
local function changed(property: string, fn: (new) -> ())
return action(function(instance)
local cn = instance:GetPropertyChangedSignal(property):Connect(function()
fn(instance[property])
end)
-- disconnect on scope destruction to allow gc of instance
cleanup(function()
cn:Disconnect()
end)
end)
end
local output = source ""
create "TextBox" {
-- will update the output source anytime the text property is changed
changed("Text", output)
}
```
## changed()
A wrapper for `action()` to listen for property changes.
- **Type**
```luau
function changed(property: string, fn: (unknown) -> ()): Action
```
- **Details**
Will run the given function immediately and whenever the property updates.
The function is called with the updated property value.
Runs with an action priority of 1.
## mount() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
Runs a function in a new stable scope and optionally applies its result to a
target instance.
- **Type**
```luau
function mount<T>(component: () -> T, target: Instance?): () -> ()
```
- **Details**
This is a utility for `root()` when parenting a component to an existing
instance.
The result of the function is applied to a target in the same way
properties are using `create()`.
Returns a function that when called will destroy the stable scope.
- **Example**
```luau
local function App()
return create "ScreenGui" {
create "TextLabel" { Text = "Vide" }
}
end
local destroy = mount(App, game.StarterGui)
```

View file

@ -0,0 +1,155 @@
# Reactivity: Core
## Scopes
Vide code can run in one of two scopes: <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge> or <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>.
- Reactive scopes rerun if a source read within updates.
- Stable scopes never rerun.
- Reactive scopes cannot be created directly within another reactive scope.
- When a scope is destroyed, all scopes created within are also destroyed.
Different functions in Vide's API will run code in different scopes.
:::warning
Yielding is not allowed in any stable or reactive scope. Strict mode will check
for this.
:::
## root() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
Runs a function in a new stable scope.
- **Type**
```luau
function root<T...>(fn: (Destructor) -> T...): (Destructor, T...)
type Destructor = () -> ()
```
- **Details**
Returns a destructor and any values returned by the callback.
## source()
Creates a new source.
- **Type**
```luau
function source<T>(value: T): Source<T>
type Source<T> =
() -> T -- get
& (T) -> () -- set
```
- **Details**
Call the returned source with no argument to read its value.
Call the returned source with an argument to set its value.
- **Example**
```luau
local count = source(0)
print(count())-- 0
count(count() + 1)
print(count()) -- 1
```
## effect() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Runs a function in a new reactive scope.
- **Type**
```luau
function effect(fn: () -> ())
```
- **Details**
The function is ran once immediately.
- **Example**
```luau
local count = source(1)
effect(function()
print(count())
end)
-- prints 1
count(2)
-- prints 2
```
## derive() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Runs a function in a new reactive scope to compute a value for new source.
- **Type**
```luau
function derive<T>(fn: () -> T): () -> T
```
- **Details**
Anytime the reactive scope reruns, the output source value is set to what is
returned.
The function is ran once immediately.
- **Example**
```luau
local count = source(0)
local text = derive(function() return `count: {count()}` end)
print(text()) -- "count: 0"
count(1)
print(text()) -- "count: 1"
```
A `derive()` should be used instead of a pure function when you expect it to
be read multiple times between updates, because `derive()` will cache the
result to prevent recomputing it on every read.
::: code-group
```luau [Pure Function]
local count = source(0)
local text = function()
print "ran"
return `count: {count()}`
end
count(1)
print(text()) -- prints "ran" followed by "count: 1"
print(text()) -- prints "ran" followed by "count: 1"
```
```luau [Derived Source]
local count = source(0)
local text = derive(function() -- [!code highlight]
print "ran"
return `count: {count()}`
end) -- [!code highlight]
count(1) -- prints "ran"
print(text()) -- prints "count: 1"
print(text()) -- prints "count: 1"
```
:::

View file

@ -0,0 +1,229 @@
# Reactivity: Dynamic Scopes
Dynamic scopes are scopes that are created or destroyed in response to
source updates. Vide provides functions for some common use-cases for dynamic scopes.
## show() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Shows a component if the source is truthy. Optionally shows a fallback component
if the source is falsey.
- **Type**
```luau
function show<T>(source: () -> unknown, component: Constructor<T>): () -> T?
function show<T, U>(source: () -> unknown, component: Constructor<T>, fallback: () -> U): () -> T | U
type Constructor<T> = () -> (T, number?)
```
- **Details**
Creates a reactive scope internally to detect source updates.
The component is run in a stable scope when truthy, otherwise the stable
scope is destroyed.
Returns a source holding an instance of the currently shown component or
`nil` if no component is currently shown.
Destruction of the scope can be delayed by returning the number of seconds
to delay by, after the component.
## switch() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Shows one of a set of components depending on a source and a mapping table.
- **Type**
```luau
function switch<K, V>(source: () -> K): (map: Map<K, Constructor<V>>): () -> V?
type Constructor<T> = () -> (T, number?)
```
- **Details**
Creates a reactive scope internally to detect source updates.
When the source updates, its value is inputted into a map to get a component
constructor. This component is then run in a stable scope. The previous
stable scope is destroyed.
Returns a source holding an instance of the currently shown component or
`nil` if no component is currently shown.
Destruction of the scope can be delayed by returning the number of seconds
to delay by, after the component.
- **Example**
```luau
local logged = source(false)
local button = switch(logged) {
[true] = function()
return Button { Text = "Log out", Toggle = logged }
end,
[false] = function()
return Button { Text = "Log in", Toggle = logged }
end
}
```
## indexes() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Shows a component for each index in a table.
- **Type**
```luau
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
constructor: (value: () -> VI, index: KI) -> (VO, number?)
): Array<VO>
```
- **Details**
Creates a reactive scope internally to detect source updates.
When the source table updates, a component is generated for each index in
the table.
- For any added index, the `constructor` function is run in a new stable
scope to produce an instance that is cached.
- For any removed index, the stable scope for that index is destroyed.
The `constructor` function is called with:
1. A *source containing the index's value*.
2. The *index itself*.
Anytime an existing index's value changes, the `constructor` function is not
rerun, instead, that index's corresponding source is updated with the new
value.
Returns a source holding an array of instances currently shown.
Destruction of the scope can be delayed by returning the number of seconds
to delay by, after the component.
- **Example**
```luau
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return i .. ": " .. item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
}
end)
```
## values() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Shows a component for each value in a table.
- **Type**
```luau
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
constructor: (value: VI, index: () -> KI) -> (VO, number?)
): Array<VO>
- **Details**
Operates with the same idea as `indexes()`, but applied to values instead of
indexes.
Creates a reactive scope internally to detect source updates.
When the source table updates, a component is generated for each value in
the table.
- For any added value, the `constructor` function is run in a new stable scope
to produce an instance that is cached.
- For any removed value, the stable scope for that value is destroyed.
The `constructor` function is called with:
1. The *value itself*.
2. A *source containing the value's index*.
Anytime an existing value's index changes, the `constructor` function is not
rerun, instead, that value's corresponding source is updated with the new
index.
Returns a source holding an array of instances currently shown.
Destruction of the scope can be delayed by returning the number of seconds
to delay by, after the component.
::: warning
Having the same values appear multiple times in the input source table can
cause unexpected behavior. Strict mode has checks for this.
:::
- **Example**
```luau
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = function()
return i() .. ": " .. item.Name
end
Image = "rbxassetid://" .. item.icon,
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive values. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
In most cases, both functions will produce the same observed result.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders. One case to note is
that `values()` works nicely when animating re-ordering of instances, since
the source index can be used to animate a change in position for the UI
element.
--------------------------------------------------------------------------------

View file

@ -0,0 +1,158 @@
# Reactivity: Utility
## cleanup()
Queues a callback to run when a scope is reran or destroyed.
- **Type**
```luau
function cleanup(v: Function | Disconnectable | Destroyable | thread)
type Function = () -> ()
type Destroyable = { destroy: () -> () }
type Disconnectable = { disconnect: () -> () }
```
- **Example**
```luau
local count = source(0)
local destroy = root(function()
effect(function()
count()
cleanup(function()
print "cleaned"
end)
end)
end
-- nothing printed yet
count(1) -- prints "cleaned"
count(2) -- prints "cleaned"
destroy() -- prints "cleaned"
```
## untrack() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
Runs a function in a new stable scope.
- **Type**
```luau
function untrack<T>(source: () -> T): T
```
- **Details**
Can be used inside a reactive scope to read from sources you do not want
tracked by the reactive scope.
- **Example**
```luau
local a = source(0)
local b = source(0)
local sum = derive(function()
return a() + untrack(b)
end)
print(sum()) -- 0
b(1) -- untracked so reactive scope created by derive() does not rerun
print(sum()) -- 0
a(1) -- reactive scope created by derive() reruns
print(sum()) -- 2
```
## read()
Utility used to read a value that is either a primitive or a source.
- **Type**
```luau
function read<T>(value: T | () -> T): T
```
## batch()
Runs a function where any source updates made within the function do not
trigger effects until after the function ends.
- **Type**
```luau
function batch(fn: () -> ())
```
- **Details**
Improves performance when an effect depends on multiple sources, and those
sources need to be updated.
- **Example**
```luau
local a = source(0)
local b = source(0)
effect(function()
print(a() + b())
end)
-- prints "0"
batch(function()
a(1) -- no print
b(2) -- no print
end)
-- prints "3"
```
## context() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
Creates a new context.
- **Type**
```luau
function context<T>(default: T): Context<T>
type Context<T> =
() -> T -- get
& <U>(T, () -> U) -> U -- set
```
- **Details**
Calling `context()` returns a new context function.
Call this function with no arguments to get the context value.
Call this function with a value and a function to create a new context with
the given value.
The new context is run under a stable scope.
- **Example**
```luau
local theme = context()
local function Button()
print(theme())
end
root(function()
theme("light", function()
Button() -- prints "light"
theme("dark", function()
Button() -- prints "dark"
end)
end)
end)
```

View file

@ -0,0 +1,37 @@
# Strict Mode
Strict mode is library-wide and can get set by doing:
```luau
vide.strict = true
```
It is automatically enabled when Vide is first required and not running in O2
optimization level.
Strict mode is designed to help the development process by adding safety checks
and identifying improper usage.
Currently, strict mode will:
1. Run reactive scopes twice when a source updates.
2. Throw an error if yields occur where they are not allowed.
3. Checks for `indexes()` and `values()` outputting primitive values.
4. Checks for `values()` input having duplicate values.
5. Checks for duplicate nested properties at same depth.
6. Checks for destruction of an active scope.
7. Better error reporting and stack traces.
By rerunning reactive scopes twice each time they update, it helps ensure that
computations are pure, and that any cleanup is done correctly.
Accidental yielding within reactive scopes can break Vide's reactive graph,
which strict mode will catch.
As well as additional safety checks, Vide will dedicate extra resources to
recording and better emitting stack traces where errors occur, particularly
when implicit effects are created for instance property updating.
It is recommended to develop UI with strict mode and to disable it when pushing to
production. In Roblox, production code compiles at O2 by default, so you do not
need to worry about disabling strict mode unless you have manually enabled it.

View file

@ -0,0 +1,22 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
pageClass: home
next:
text: 'Introduction'
link: '/tut/crash-course/1-introduction'
hero:
name: "Vide"
tagline: A reactive UI library for Luau.
image:
src: /logo.svg
actions:
- theme: brand
text: Tutorials
link: /tut/crash-course/1-introduction
- theme: alt
text: API Reference
link: /api/reactivity-core
---

View file

@ -0,0 +1,12 @@
{
"type": "module",
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "^1.4.5",
"vitepress-plugin-mermaid": "2.0.17"
}
}

View file

@ -0,0 +1,67 @@
<svg width="375" height="220" viewBox="15 7 115 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_1_41)">
<g clip-path="url(#clip0_1_41)">
<rect x="18" y="12" width="110" height="48" rx="8" fill="url(#paint0_radial_1_41)"/>
<path d="M65.4148 32.2727L68.0057 39.6193H68.108L70.6989 32.2727H71.8068L68.6023 41H67.5114L64.3068 32.2727H65.4148ZM77.223 32.2727V41H76.1662V32.2727H77.223ZM85.0352 41H82.342V32.2727H85.1545C86.0011 32.2727 86.7255 32.4474 87.3278 32.7969C87.93 33.1435 88.3917 33.642 88.7127 34.2926C89.0337 34.9403 89.1942 35.7159 89.1942 36.6193C89.1942 37.5284 89.0323 38.3111 88.7085 38.9673C88.3846 39.6207 87.913 40.1236 87.2937 40.4759C86.6744 40.8253 85.9215 41 85.0352 41ZM83.3988 40.0625H84.967C85.6886 40.0625 86.2866 39.9233 86.761 39.6449C87.2354 39.3665 87.5891 38.9702 87.8221 38.456C88.055 37.9418 88.1715 37.3295 88.1715 36.6193C88.1715 35.9148 88.0565 35.3082 87.8263 34.7997C87.5962 34.2884 87.2525 33.8963 86.7951 33.6236C86.3377 33.348 85.7681 33.2102 85.0863 33.2102H83.3988V40.0625ZM93.967 41V32.2727H99.234V33.2102H95.0238V36.1591H98.9613V37.0966H95.0238V40.0625H99.3022V41H93.967ZM109.022 32.2727H111.127V37.902C111.127 38.5526 110.973 39.1193 110.663 39.6023C110.353 40.0824 109.922 40.4545 109.368 40.7188C108.814 40.9801 108.17 41.1108 107.437 41.1108C106.696 41.1108 106.048 40.9801 105.494 40.7188C104.94 40.4545 104.51 40.0824 104.203 39.6023C103.896 39.1193 103.743 38.5526 103.743 37.902V32.2727H105.852V37.7188C105.852 38.0199 105.917 38.2884 106.048 38.5241C106.181 38.7599 106.368 38.9446 106.606 39.0781C106.845 39.2116 107.122 39.2784 107.437 39.2784C107.752 39.2784 108.028 39.2116 108.264 39.0781C108.502 38.9446 108.689 38.7599 108.822 38.5241C108.956 38.2884 109.022 38.0199 109.022 37.7188V32.2727ZM117.559 32.2727V41H115.45V32.2727H117.559Z" fill="white"/>
<g filter="url(#filter1_ddd_1_41)">
<path d="M39.4019 47.5C40.5566 49.5 43.4434 49.5 44.5981 47.5L56.4546 26.9639C57.2786 25.5367 56.1196 23.7812 54.4834 23.9783L48.3548 24.7164C46.9029 24.8913 45.6622 25.8444 45.1191 27.2021L42 35L38 25L29.5166 23.9783C27.8804 23.7812 26.7214 25.5367 27.5454 26.9639L39.4019 47.5Z" fill="url(#paint1_radial_1_41)"/>
<path d="M38 25L42 35L46 45.0718C45.1412 46.5593 42.9736 46.4973 42.2012 44.9632L31.7728 24.25L38 25Z" fill="url(#paint2_radial_1_41)"/>
<path d="M56.4546 26.9639L44.5981 47.5C43.4434 49.5 40.5566 49.5 39.4019 47.5L38 45.0718L34 38.1436C35.6267 40.9319 39.7492 40.627 40.9481 37.6298L42 35L46 25L54.4834 23.9783C56.1196 23.7812 57.2786 25.5367 56.4546 26.9639Z" fill="url(#paint3_radial_1_41)"/>
</g>
</g>
</g>
<defs>
<filter id="filter0_d_1_41" x="0" y="0" width="150" height="88" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect1_dropShadow_1_41"/>
<feOffset dx="2" dy="8"/>
<feGaussianBlur stdDeviation="12"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0690196 0 0 0 0 0.313726 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_41"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_41" result="shape"/>
</filter>
<filter id="filter1_ddd_1_41" x="13.2732" y="11.9631" width="61.4536" height="57.0369" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="2" dy="4"/>
<feGaussianBlur stdDeviation="8"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_41"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="1" dy="2"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_1_41" result="effect2_dropShadow_1_41"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="effect2_dropShadow_1_41" result="effect3_dropShadow_1_41"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1_41" result="shape"/>
</filter>
<radialGradient id="paint0_radial_1_41" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(73 12) rotate(90) scale(48 58.022)">
<stop stop-color="#3D506C"/>
<stop offset="1" stop-color="#2F415C"/>
</radialGradient>
<radialGradient id="paint1_radial_1_41" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(42 51.5) rotate(-90) scale(28 32.3316)">
<stop stop-color="#2A57A2"/>
<stop offset="1" stop-color="#407BBA"/>
</radialGradient>
<radialGradient id="paint2_radial_1_41" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39 24) rotate(74.3578) scale(25.9615 15.2089)">
<stop stop-color="#1D314F"/>
<stop offset="1" stop-color="#0D1A2E"/>
</radialGradient>
<radialGradient id="paint3_radial_1_41" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(34 52) rotate(-49.3987) scale(36.8782 31.6434)">
<stop stop-color="#5792D9"/>
<stop offset="1" stop-color="#6AA8E9"/>
</radialGradient>
<clipPath id="clip0_1_41">
<rect x="18" y="12" width="110" height="48" rx="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View file

@ -0,0 +1,33 @@
<svg width="200" height="200" viewBox="0 0 152 152" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_ii_282_2)">
<path d="M63.659 130.625C69.1438 140.125 82.8561 140.125 88.341 130.625L144.659 33.0785C148.573 26.2993 143.068 17.9607 135.296 18.8969L106.185 22.4029C99.2887 23.2337 93.3954 27.7609 90.8157 34.21L76 71.25L57 23.75L16.7038 18.8969C8.93178 17.9607 3.42658 26.2993 7.34067 33.0785L63.659 130.625Z" fill="url(#paint0_radial_282_2)"/>
<path d="M57 23.75L76 71.25L95 119.091C90.9207 126.157 80.6246 125.862 76.9557 118.575L27.4208 20.1875L57 23.75Z" fill="url(#paint1_radial_282_2)"/>
<path d="M144.659 33.0785L88.3409 130.625C82.8561 140.125 69.1438 140.125 63.659 130.625L57 119.091L38 86.1821C45.7268 99.4265 65.3087 97.9783 71.0034 83.7416L76 71.25L95 23.75L135.296 18.8969C143.068 17.9607 148.573 26.2993 144.659 33.0785Z" fill="#4896F3"/>
</g>
<defs>
<filter id="filter0_ii_282_2" x="6.04761" y="18.8246" width="163.905" height="166.925" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="24" dy="48"/>
<feGaussianBlur stdDeviation="96"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_282_2"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="12" dy="24"/>
<feGaussianBlur stdDeviation="48"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="effect1_innerShadow_282_2" result="effect2_innerShadow_282_2"/>
</filter>
<radialGradient id="paint0_radial_282_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(76 149.625) rotate(-90) scale(133 153.575)">
<stop stop-color="#24447F"/>
<stop offset="1" stop-color="#3661A2"/>
</radialGradient>
<radialGradient id="paint1_radial_282_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(61.75 19) rotate(74.3578) scale(123.317 72.2423)">
<stop stop-color="#1D314F"/>
<stop offset="1" stop-color="#0D1A2E"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,148 @@
# Dynamic Scopes
Dynamic scopes are scopes that are created and destroyed in response to
source updates. This is needed for conditionally rendering parts of your UI,
such as opening and closing menus.
While Vide provides functions for common ways to do this, this section will
show how you can implement them yourself so you are not limited by only what is
provided.
## Recreating [`show()`](/api/reactivity-dynamic#show-reactive)
The most basic one, `show()`, can be
implemented yourself like so:
```luau
local function show(toggle: () -> unknown, component: () -> Instance)
return derive(function()
return if toggle() then untrack(component) else nil
end)
end
```
The main thing to note here is the use of `untrack()`. This function runs its
callback in a new stable scope. Without this, if the component were to create
a reactive scope, an error would occur since a reactive scope cannot be created
within a reactive scope.
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#444455",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#444455"
}
}}%%
graph
subgraph derive ["derive (reactive)"]
subgraph untrack ["untrack (stable)"]
subgraph effect ["effect (reactive)"]
end
end
end
```
You can see from the above graph how the effect would not be created directly
inside the derive, there is a stable scope between them. This requirement exists
as a guard against unintentional rerendering of UI.
## Recreating [`switch()`](/api/reactivity-dynamic#switch-reactive)
```luau
local function switch(key)
return function(map)
return derive(function()
local component = map[key()]
return if component then untrack(component) else nil
end)
end
end
```
## Recreating [`indexes()`](/api/reactivity-dynamic#indexes-reactive)
This is a more complicated function because it manages multiple scopes at the
same time, unlike the previous functions. Because some scopes may persist
between reruns, we cannot use `untrack()` anymore which automatically destroys
on rerun; we must use `root()` where the lifetime of each scope is managed
manually and independently.
```luau
local function indexes<I, VI, VO>(
input: () -> Map<I, VI>,
transform: (value: () -> VI, index: I) -> VO
)
local index_caches = {} :: Map<I, {
previous_input: VI,
output: VO,
source: (VI) -> VI,
destroy: () -> ()
}?>
-- destroy all scopes if the parent scope is destroyed
cleanup(function()
for _, cache in index_caches do
assert(cache).destroy()
end
end)
return derive(function()
local new_input = input()
-- destroy scopes of removed indexes
for i, cache in index_caches do
if new_input[i] == nil then
assert(cache).destroy()
index_caches[i] = nil
end
end
-- create scopes or update sources of added or changed index values
for i, v in new_input do
local cache = index_caches[i]
if cache == nil then -- no scope created for this index, create one
local src = source(v)
local destroy, result = root(function()
return transform(src, i)
end)
index_caches[i] = {
destroy = destroy,
source = src,
output = result,
previous_input = v
}
elseif cache.previous_input ~= v then -- scope exists, update source
cache.previous_input = v
cache.source(v)
else -- scope exists and value has not changed; do nothing
end
end
-- return the cached output values as an array
local array = table.create(#index_caches)
for _, cache in index_caches do
table.insert(array, assert(cache).output)
end
return array
end)
end
```
--------------------------------------------------------------------------------
Though the above functions are already provided to you by Vide, this serves as
an example for how you may create your own dynamic scope functions.

View file

@ -0,0 +1,21 @@
# Introduction
This is a tutorial that introduces the concepts and usage of Vide.
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
## Why Vide?
Vide's reactive and declarative API aims to let you program UI as simply as
possible, with a strong focus on how data flows through your application.
Some of Vide's main design choices:
- Syntax minimal.
- Data oriented.
- Typechecking compatible.
- Instance independent.
Vide's reactivity operates with the concept
of scopes which carries a learning curve, though is what makes Vide's minimal
syntax possible. The crash course will introduce these concepts gradually.

View file

@ -0,0 +1,38 @@
# Cleanup
Sometimes you may need to do some cleanup when destroying a component or after
a side-effect from a source update. Vide provides a function `cleanup()` which
is used to queue a callback for the next time a reactive scope is rerun or
destroyed, or when a stable scope is destroyed.
```luau
local root = vide.root
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
local count = source(0)
local destroy = root(function()
effect(function()
local x = count()
cleanup(function() print(x) end)
end)
cleanup(function() print "root destroyed" end)
end)
count(1) -- prints "0"
count(2) -- prints "1"
destroy() -- prints "2" and "root destroyed"
```
::: tip
Roblox instances do not need to be explicitly destroyed for their
memory to be freed, they only need to be parented to `nil`. So there is no
need to use `cleanup()` to destroy instances. However, be wary of connecting
a function that references an instance to an event from the same instance,
this causes the instance to reference itself and never be freed. In such a case
you would need to use `cleanup()` to disconnect this connection or to explicitly
destroy the instance.
:::

View file

@ -0,0 +1,211 @@
# Dynamic Scopes
Eventually you may need a way to dynamically create and destroy UI elements
resulting from source updates. Vide provides functions to help you do this,
known as *dynamic scope* functions.
These functions create and destroy scopes for you in response to source
updates. They return a source containing the created component. This source can
be parented as a child which will update the shown children whenever the source
updates.
The simplest example is using `show()`.
```luau
local source = vide.source
local create = vide.create
local show = vide.show
local root = vide.root
function Button(props: { Text: string, Activated: () -> () })
return create "TextButton" {
Text = props.Text,
Activated = props.Activated
}
end
function Menu()
return create "TextLabel" {
Text = "This is a menu"
}
end
function App()
local toggled = source(false)
return create "ScreenGui" {
Button {
Text = "Toggle Menu",
Activated = function()
toggled(not toggled())
end
},
show(toggled, Menu) -- [!code highlight]
}
end
root(function()
App().Parent = game.StarterGui
end)
```
This is a complete example of rendering UI which has a single button that
toggles the opening of a menu.
--------------------------------------------------------------------------------
Another common function is `indexes()`. This function creates a component for
each index in a table.
Each component created is done so in a new and independent stable scope. The
indexes of the table are checked each source update to prevent redunant
destruction and recreation of UI elements.
```luau
local source = vide.source
local create = vide.create
local indexes = vide.indexes
local root = vide.root
local function Todo(props: {
Text: () -> string,
Position: number,
Activated: () -> ()
})
return create "TextButton" {
Text = function() return props.Position .. ": " .. props.Text() end,
LayoutOrder = props.Position,
Activated = Activated
}
end
local function TodoList(props: { List: () -> Array<string> })
return create "Frame" {
create "UIListLayout" {},
indexes(props.List, function(text, i) -- [!code highlight]
return Todo {
Text = text,
Position = i,
Activated = function() -- remove the todo when clicked
local list = props.List()
table.remove(list, i)
props.List(list)
end
}
end)
}
end
function App()
local list = source {
"finish the crash course",
"star Vide's GitHub"
}
return create "ScreenGui" {
TodoList { List = list },
}
end
root(function()
App().Parent = game.StarterGui
end)
```
The reactive graph for the above example:
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
}
}}%%
graph
subgraph root ["root"]
direction LR
todoList --> indexes -.- subroot1 & subroot2
subgraph subroot1 ["indexes scope 1"]
direction LR
value1[todo] --> prop1["prop binding"]
end
subgraph subroot2 ["indexes scope 2"]
direction LR
value2[todo] --> prop2[prop binding]
end
end
```
When you edit a table in a source, you must set that table again to actually
update the source.
```luau
local src = source { 1, 2 }
local data = src()
table.insert(data, 3) -- no effects will run
src(data) -- effects will run
```
--------------------------------------------------------------------------------
All dynamic scope functions also support delaying the destruction of the scope.
This is useful for playing any sort of animation or effect before the UI
instance is removed.
If you have the following code, for example:
```lua
local function Menu()
return create "Frame" {}
end
local toggled = source(true)
create "ScreenGui" {
show(toggled, function()
return Menu {}
end)
}
toggled(false) -- menu will disappear immediately
```
```lua
local function Menu(props: { Visible: () -> boolean })
local transparency = spring(function()
return if p.Visible then 0 else 1
end
return create "Frame" {
BackgroundTransparency = transparency
}
end
local toggled = source(true)
create "ScreenGui" {
show(toggled, function(_, present)
return Menu { p.Visible = present }, 3 -- give a generous 3 seconds for the spring to complete before destroying
end)
}
toggled(false)
-- `present` will go `false` immediately
-- transparency will begin being sprung
-- after 3 seconds the scope is destroyed, giving the spring enough time to complete
```
If `toggled` goes from truthy to falsey, beginning the timer, but then back
to truthy before the timer finishes, the timer is cancelled and the scope is
not destroyed.

View file

@ -0,0 +1,55 @@
# Actions
Actions are special callbacks that you can pass along with properties,
to run some code on an instance receiving them.
```luau
local action = vide.action
create "TextLabel" {
Text = "test",
action(function(instance)
print(instance.Text)
end)
}
-- will print "test"
```
Actions can be wrapped with functions for reuse. Below is an example of an
action used to listen for property changes:
```luau
local action = vide.action
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
local function changed(property: string, callback: (new) -> ())
return action(function(instance)
local connection = instance:GetPropertyChangedSignal(property):Connect(function()
callback(instance[property])
end)
-- remember to clean up the connection when the reactive scope the action
-- is ran in is destroyed, so the instance can be garbage collected
cleanup(connection)
end)
end
local output = source ""
local instance = create "TextBox" {
changed("Text", output)
}
effect(function()
print(output())
end)
instance.Text = "foo" -- "foo" will be printed by the effect
```
The source `output` will be updated with the new property value any time it is
changed externally.

View file

@ -0,0 +1,36 @@
# Strict Mode
While developing UI with Vide, you should use Vide's strict mode, which can
be set with `vide.strict = true` once when you first require Vide. Strict mode
will add extra safety checks and emit better error traces, particularly when
errors occur in property bindings.
Strict mode is automatically enabled when Vide is required in O0 or O1
optimization (default studio level). You can `vide.strict = false` if you do not
want this.
Strict mode will run derived sources and effects twice each time they update.
This is to help ensure that derived source computations are pure, and that any
cleanups made in derived sources or effects are done properly.
```luau
local source = vide.source
local effect = vide.effect
vide.strict = true
local count = source(0)
local ran = 0
effect(function()
count()
ran += 1
end)
print(ran) -- 2
count(1)
print(ran) -- 4
```
A full list of what strict mode will do can be found
[here](../../api/strict-mode).

View file

@ -0,0 +1,113 @@
# Concepts Summary
A summary of all the concepts covered during the crash course.
## Source
A source of data.
Stores a single value that can be updated.
Created with `source()`.
## Derived Source
A new source composed of other sources.
Created with a plain function or with `derive()`.
## Effect
Anything that happens in response to a source update.
Created with `effect()`.
## Stable Scope
One of the two types of Vide scopes.
Created by:
- `root()`
- `untrack()`
- `show()`
- `indexes()`
Stable scopes do not track sources and never rerun.
New stable or reactive scopes can be created within a stable scope.
## Reactive Scope
Created by:
- `effect()`
- `derive()`
Reactive scopes do track sources and will rerun when those sources update.
Reactive scopes cannot be created within a reactive scope, but stable scopes
can be created within a reactive scope.
## Scope Cleanup
When a scope is rerun or destroyed, all scopes created within it are
automatically destroyed.
Any functions queued by `cleanup()` are also ran.
## Reactive Graph
The combination of stable and reactive scopes can viewed graphically, called a
*reactive graph*. This can be a more intuitive way to think of the
relationships between effects and the sources they depend on.
### Code
```luau
local count = source(0)
root(function()
local text = derive(function()
return "count: " .. count()
end)
effect(function()
print(text())
end)
end)
```
### Graph resulting from code
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#202530"
}
}}%%
graph LR
subgraph root
text --> effect
end
count --> text
```
Notes:
- Since `count` is a source, not an effect, it can exist
outside of scopes.
- An update to `count` will cause `text` to rerun, which
then causes `effect` to rerun.
- When the root scope is destroyed, `text` and
`effect` will be destroyed alongside it, since they were created within it.
`count` will be untouched and future updates to `count` will have no effect.

View file

@ -0,0 +1,38 @@
# Creating UI
Instances are created using `create()`.
Parentheses `()` can be omitted when calling functions with string or
table literals for brevity.
```luau
local create = vide.create
return create "ScreenGui" {
create "Frame" {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromScale(0.4, 0.7),
create "TextLabel" {
Text = "hi"
},
create "TextLabel" {
Text = "bye"
},
create "TextButton" {
Text = "click me",
Activated = function()
print "clicked!"
end
}
}
}
```
Assign a value to a string key to set a property, and assign a value to a
number key to set a child. Events can be connected to by assigning a function
to a string key.

View file

@ -0,0 +1,65 @@
# Components
Vide encourages separating different parts of your UI into functions called
*components*.
A component is a function that creates and returns a piece of UI.
This is a way to separate your UI into small chunks that you can reuse and put
together.
::: code-group
```luau [Button.luau]
local create = vide.create
local function Button(props: {
Position: UDim2,
Text: string,
Activated: () -> ()
})
return create "TextButton" {
BackgroundColor3 = Color3.fromRGB(50, 50, 50),
TextColor3 = Color3.fromRGB(255, 255, 255),
Size = UDim2.fromOffset(200, 150),
Position = props.Position,
Text = props.Text,
Activated = props.Activated,
create "UICorner" {}
}
end
return Button
```
```luau [Menu.luau]
local create = vide.create
local Button = require(Button)
local function Menu()
return create "ScreenGui" {
Button {
Position = UDim2.fromOffset(200, 200),
Text = "back",
Activated = function()
print "go to previous page"
end
},
Button {
Position = UDim2.fromOffset(400, 200),
Text = "next",
Activated = function()
print "go to next page"
end
}
}
end
```
:::
A single parameter `props` is used to pass properties to the component.

View file

@ -0,0 +1,38 @@
# Sources
Sources are special objects that store a single value and are the core of
Vide's reactivity.
A source can be created using `source()`.
```luau
local source = vide.source
local count = source(0)
```
The value passed to `source()` is the initial value of the source.
The value of a source can be set by calling it with an argument, and can be read
by calling it with no arguments.
```luau
count(count() + 1) -- increment count by 1
```
Sources can be *derived* by wrapping them in functions.
```luau
local count = source(0)
local text = function()
return "count: " .. tostring(count())
end
print(text()) -- "count: 0"
count(1)
print(text()) -- "count: 1"
```
While the above can be achieved with plain variables, the use for sources will
be obvious in the next part.

View file

@ -0,0 +1,71 @@
# Effects
Effects are functions that are ran in response to source updates.
A source and effect is analogous to a signal and connection.
Effects are created using `effect()`.
```luau
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
```
Any source read inside an effect is tracked and will rerun the effect when
that source is updated.
The effect runs its callback once immediately to initially figure out what
sources are being read.
Derived sources are also tracked, it does not matter how deeply nested
inside a function a source is.
```luau
local source = vide.source
local effect = vide.effect
local count = source(1)
local doubled = function()
return count() * 2
end
effect(function()
print("doubled count: " .. doubled())
end)
-- "doubled count: 2" printed
count(2)
-- "doubled count: 4" printed
```
If a source is updated with the same value it already had, it will not rerun
effects depending on it.
You can also read from a source within an effect without the effect tracking it.
```luau
local source = vide.source
local effect = vide.effect
local untrack = vide.untrack
local a = source(0)
local b = source(0)
effect(function()
print(`a: {a()} b: {untrack(b)}`)
end)
a(1) -- prints "a: 1 b: 0"
b(1) -- prints nothing
a(2) -- prints "a: 2 b: 1"
```

View file

@ -0,0 +1,88 @@
# Scopes
Just like how a signal's connection may need to be disconnected, a source's
effect also may need to be disconnected.
But the disconnecting of many signals and connections is tedious and verbose.
Vide instead operates on the concept of scopes which provides a much cleaner
API, given that you follow a few rules.
There are two types of scopes: stable and reactive.
- A scope must be created within another scope.
- Stable scopes never rerun.
- Reactive scopes can rerun.
- A reactive scope cannot be created within another reactive scope, only within
a stable scope.
An exception to the first rule is `root()`, which creates the initial scope that
you destroy manually with a destructor function it returns.
`root()` creates a stable scope.
`effect()` creates a reactive scope.
Whenever a scope is destroyed, any scope created within that scope is also
destroyed, and so on.
```luau
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() -- error, effect() tried to create a reactive scope with no stable scope
local destroy = root(setup) -- ok since effect() was called in a stable scope
count(1) -- prints "1"
count(2) -- prints "2"
destroy()
count(3) -- reactive scope created by effect() is destroyed, it does not rerun
```
Vide's reactivity can be represented graphically, as a *reactive graph*.
The reactive graph for the above example looks like so:
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
}
}}%%
graph
subgraph root
direction LR
count --> effect
end
```
When the stable `root()` scope is destroyed, the reactive `effect()`
scope will also be destroyed since it was created within it.
This is important because you may have an effect that updates the property of a
UI instance, meaning the effect is referencing and holding that instance in
memory. The effect being destroyed will remove this reference, allowing the
instance to be garbage collected.
You don't need to worry about ensuring all your effects are created within a
stable scope, since you should be creating all your UI and effects within a
single top-level `root()` call that puts all your UI together, making it safe to
assume any effect created will be created under this stable scope.

View file

@ -0,0 +1,60 @@
# Reactive Components
Reactive components in Vide are created using sources and effects - sources to
store the data, and effects to display the data.
```luau
local create = vide.create
local source = vide.source
local effect = vide.effect
local function Counter()
local count = source(0)
local instance = create "TextButton" {
Activated = function()
count(count() + 1)
end
}
effect(function()
instance.Text = "count: " .. count()
end)
return instance
end
```
Above is an example of a counter component, that when clicked, will increment
its internal count, and automatically update its text to reflect that count.
Each instance of `Counter()` will maintain its own independent count, since the
count source is created inside the component.
External sources can also be passed into components for them to use.
```luau
local function CountDisplay(props: { count: () -> number })
local count = props.count
local instance = create "TextLabel" {}
effect(function()
instance.Text = "count: " .. count()
end)
return instance
end
local count = source(0)
CountDisplay {
count = count
}
count(1) -- the CountDisplay component will update to display this count
```
Sources can be created internally or passed in from externally, there are no
restrictions on how they are used as long as the effect using it is created
within a stable scope.

View file

@ -0,0 +1,83 @@
# Implicit Effects
Explicitly creating effects to update properties is tedious. You can
*implicitly* create an effect to update properties instead.
::: code-group
```luau [Implicit Effect]
local create = vide.create
local source = vide.source
local function Counter()
local count = source(0)
return create "TextButton" {
Activated = function()
count(count() + 1)
end,
Text = function()
return "count: " .. count()
end
}
end
```
```luau [Explicit Effect]
local create = vide.create
local source = vide.source
local effect = vide.effect
local function Counter()
local count = source(0)
local instance = create "TextButton" {
Activated = function()
count(count() + 1)
end
}
effect(function()
instance.Text = "count: " .. count()
end)
return instance
end
```
:::
This example is equivalent to the example seen on the previous page.
Instead of explicitly creating an effect, assigning a (non-event) property a
function will implicitly create an effect to update that property.
## Children
Children can also be set in a similar manner. A source passed as a child (passed
with a number key instead of string key) can return an instance or an array of
instances. An effect is automatically created to unparent removed instances and
parent new instances on source update.
```luau
local items = source {
create "TextLabel" { Text = "A" }
}
local function List(props: { children: () -> { Instance } })
return create "Frame" {
create "UIListLayout" {},
props.children
}
end
local list = List { children = items } -- creates a list with text label "A"
items {
create "TextLabel" { Text = "B" },
create "TextLabel" { Text = "C" }
}
-- this will automatically unparent text label "A", and parent labels "B" and "C"
```

View file

@ -0,0 +1,93 @@
# Derived Sources
We have seen the basic way to derive a source:
```luau
local count = source(0)
local text = function()
return "count: " .. tostring(count())
end
print(text()) -- "count: 0"
count(1)
print(text()) -- "count: 1"
```
However, in some cases where this source could be used by multiple effects at
the same time, the function wrapping the source will needlessly rerun to convert
the count into a string for each effect using it.
```luau
local source = vide.source
local effect = vide.effect
local count = source(0)
local text = function()
print "ran"
return "count: " .. tostring(count())
end
effect(function() text() end)
effect(function() text() end)
count(1) -- prints "ran" x2
```
To avoid this, you can use `derive()` to derive a new source instead. This will
run a function in a reactive scope only when a source used inside updated.
Reading this derived source multiple times will just return a cached result.
```luau
local source = vide.source
local effect = vide.effect
local derive = vide.derive
local count = source(0)
local text = derive(function()
print "ran"
return "count: " .. tostring(count())
end)
effect(function() text() end)
effect(function() text() end)
count(1) -- prints "ran" x1
```
Because `derive()` creates a reactive scope, it must be called within a stable
scope, just like `effect()`.
If the recalculated value is the same as the old value, the derived source will
not rerun the effects using it.
The reactive graph for the above example:
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
}
}}%%
graph
subgraph root
direction LR
count --> text --> effect1 & effect2
end
```
Deriving a source in this manner is similar to creating an effect to update
another source. You should avoid doing this using an effect however.
Improper usage could accidently create infinite loops in the reactive graph.
Always favour deriving when you need one source to update based on another
source.