Update crash course

This commit is contained in:
Aaron Smith 2023-09-19 14:52:28 +01:00
parent 520b0cca32
commit bb08e2e97a
15 changed files with 272 additions and 160 deletions

View file

@ -16,11 +16,7 @@ worrying about manually updating UI instances.
Some of the main focuses behind Vide's design choices:
- Concise syntax to reduce verbosity as much as possible.
- Reducing the amount of imports needed for usage by leveraging Luau's syntax
and semantics.
- Being completely typecheckable.
- Flexibility with integrating other libraries and allowing users to use their
own patterns.
- Independence from instance lifetimes.
- A powerful reactive system that can update specific properties as a result of
state changes, updates are immediate with no diffing needed.
@ -36,7 +32,6 @@ specific part of your app, and can be reused if needed. These functions are
called *components*.
```lua
local function App()
return create "ScreenGui" {
create "TextLabel" { Text = "hi" }

View file

@ -77,7 +77,8 @@ be parented.
```lua
type Children = {
Children = Array<Instance>
-- allows us to also optionally pass a source that returns an array of children instead
Children = Array<Instance> | () -> Array<Instance>
}
local function List(props: Children & Layout)
@ -107,8 +108,8 @@ properties, this can be used to create overridable default properties.
local function List(props: Children & Layout)
return create "Frame" {
props.Children,
props.Layout,
-- can be overriden by `props.Layout`
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.fromScale(0.5, 0),

View file

@ -24,19 +24,32 @@ Actions can be wrapped with functions to re-use specific behaviors. Below is
an example of an action used to listen for property changes:
```lua
local action = vide.action
local cleanup = vide.cleanup
local function changed(property: string, callback: (new) -> ())
return action(function(instance)
instance:GetPropertyChangedSignal(property):Connect(function()
local con = 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(function()
con:Disconnect()
end)
end)
end
local output = source ""
create "TextBox" {
local instance = create "TextBox" {
changed("Text", output)
}
instance.Text = "foo"
print(output()) -- "foo"
```
The source `output` will be updated with the new property value any time it is

View file

@ -0,0 +1,9 @@
# 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.
A full list of what strict mode will do can be found
[here](../../api/strict-mode).

View file

@ -9,7 +9,6 @@ Luau allows us to omit parentheses `()` when calling functions with string or
table literals which Vide takes advantage of for brevity.
```lua
local vide = require(vide)
local mount = vide.mount
local create = vide.create
@ -56,3 +55,9 @@ create "Frame" {
UDim2 = { 0.5, 0, 0.5, 0 }
}
```
When creating an instance with no properties, it is important to not forget to
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
To be clear, `create "Frame"` returns a *function* which is a constructor for
that class, not an instance of that class. This would result in you attempting
to parent a function instead of an instance which is not the correct behavior.

View file

@ -6,7 +6,6 @@ By using components you can make your application more modular and better
organized.
```lua [Button.luau]
local vide = require(vide)
local create = vide.create
local function Button(props: {
@ -28,7 +27,6 @@ return Button
```
```lua [App.luau]
local vide = require(vide)
local mount = vide.mount
local create = vide.create
@ -56,11 +54,11 @@ being reused across files.
A single parameter `props` is used to pass properties to the component.
Components allow you to *encapsulate* behavior. You can only modify the
component in ways that you allow in the component.
component in ways that you allow in the component, through the `props` parameter.
This also promotes code reusability. Anytime you want a new button all you do
is call `Button {}` instead of creating and setting every property each time.
When changing the button in future, any changes to the button file will be
reflected anywhere the button is used throughout your app.
To create a new button all you must do is call the `Button` function, passing in
values through props. This saves having to create and set every property each
time. Also, when updating the button component in future, any changes to the
button file will be seen anywhere the button is used in your app.
This can be extended to much more complicated UI.

View file

@ -1,31 +1,15 @@
# Source
*Sources* in Vide are special objects that store a single value. They are the
core of reactivity in Vide, as updates to a source can automatically update
properties or other sources depending on that source.
core of reactivity in Vide. Each source represents a source of data, and they
can be composed and derived to create new sources of data.
A source in Vide can be created using `source()`.
```lua
local vide = require(vide)
local source = vide.source
local function Counter()
local count = source(0)
return create "TextButton" {
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = count,
Activated = function()
count(count() + 1)
end
}
end
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
local count = source(0)
```
The value passed to `source()` is the initial value of the source.
@ -37,15 +21,23 @@ by calling it with no arguments.
count(count() + 1) -- increment count by 1
```
Each call of `Counter {}` will create a new counter, each maintaining their
own count.
Sources can be *derived* by wrapping them in functions. A wrapped source
effectively becomes a new source.
When you assign a function to a non-event property, Vide will immediately run it
and check what sources were read from. When updating those sources again after,
this function will be re-ran and its return value applied to the property.
This is known as *binding* properties.
```lua
local count = source(0)
This allows you as the programmer to not need to manually update UI as the state
of your program changes. You just define how the data maps to UI, and Vide's
reactive system will automatically update any properties depending on sources
that are updated.
local text = function()
return "count: " .. tostring(count())
end
print(text()) -- "count: 0"
count(1)
print(text()) -- "count: 1"
```
You may be wondering why we are using sources instead of plain variables to do
this. The reason is that Vide has an entire reactive system based on sources.
You can write functions to automatically run each time a source is updated. This
can be to update properties, create new instances, print to the terminal, etc.
How this is done will be covered next.

View file

@ -1,45 +1,75 @@
# Effect
An effect is a function that is run anytime a source updates. They are called
effects because they can produce side-effects when reacting to source changes.
Effects are functions that are ran in response to source updates. They are
alled effects because they cause *side-effects* when reacting to source updates.
Effects are created using `effect()`.
```lua
local vide = require(vide)
local source = vide.source
local effect = vide.effect
local function Counter()
local count = source(0)
local count = source(0)
effect(function()
print("count has updated to: " .. count())
end)
effect(function()
print("count: " .. count())
end)
return create "TextButton" {
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = count,
Activated = function()
count(count() + 1)
end
}
end
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
-- "count: 0" printed
count(1)
-- "count: 1" printed
```
This will print to the terminal anytime the count is changed.
The callback given to `effect()` is ran in a *reactive-scope*. Any source read
from inside a reactive scope will be tracked, so that if any of those sources
update, the effect will be re-ran too.
`effect()` creates an explicit side-effect. There are other side-effects in the
above code sample. The setting of `Text = count` creates another side-effect;
the updating of the Text property anytime the count is changed.
The callback is first ran immediately inside the `effect()` call to initially
figure out what sources are being used.
All observable changes to the user are considered to be side-effects of the
reactive system.
Effects also work with derived sources, it doesn't matter how deeply nested a
source is.
```lua
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
```
Derived sources should be a *pure computation*. A pure computation is one where
the same input will always produce the same output.
All observable changes to the user are considered to be side-effects of pure
computations.
Sources, derived sources, and effects form what is called a *reactive graph*.
In the above example a graph `count -> doubled -> effect` is formed. Anywhere
an update occures, everything further down the graph is updated.
You should not update other sources using an effect. Improper usage can lead to
unecessary updates and infinite loops.
a cyclic loop in the graph, causing an infinite loop when it tries to update.
Sources should be derived instead.
## Root Reactive Scopes
Effects must be created within another reactive scope. This is so that the
effect itself can be tracked and later freed when the parent reactive scope is
destroyed, such as from unmounting an app. The example code above will not
actually work unless it is ran inside a root reactive scope, such as one created
by `vide.mount(function)`. This generally isn't a concern since you can assume
that all your components will be created within a single `mount()` call, which
happens only once at the top level, where you put together your UI and parent it
to a ScreenGUI.

View file

@ -1,76 +0,0 @@
# Derived Source
You can create new sources from existing sources. This is known as *deriving
sources*.
A function that wraps a source effectively becomes a new source. If a source
used inside a function is updated, the whole function can be re-ran to recompute
its value.
```lua
local vide = require(vide)
local source = vide.source
local function Counter()
local count = source(0)
local function doubled()
return count() * 2
end
return create "TextButton" {
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = doubled,
Activated = function()
count(count() + 1)
end
}
end
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
```
Now the counter will increment in 2s each time it is clicked.
Sometimes when using expensive computations to derive state, you only want to
recalculate it once when a source state has changed. Although not needed in
most cases, you can use `derive()` to create a new source that will cache its
value, only recomputing when an input source has changed.
```lua
local vide = require(vide)
local source = vide.source
local derive = vide.derive
local function Counter()
local count = source(0)
local factorial = derive(function()
local n = 1
for i = 2, count() do
n *= i
end
return n
end)
return create "TextButton" {
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = function()
return factorial() + factorial() + factorial()
end,
Activated = function()
count(count() + 1)
end
}
end
```
This can improve performance in cases where a source is read from multiple times
between recalculations. In the above example, the factorial is only ever
calculated once each time the count changes.

View file

@ -0,0 +1,66 @@
# Stateful Component
A stateful component is a component that stores and displays some data.
Stateful components in Vide are created using sources and effects - sources to
store the data, and effects to display the data.
```lua
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 count
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 scope of the component.
External sources can also be passed into components for them to use.
```lua
local function Counter(props: { count: () -> number })
local count = props.count
local instance = create "TextButton" {
Activated = function()
count(count() + 1)
end
}
effect(function()
instance.Text = "count: " .. count()
end)
return count
end
local count = source(0)
Counter {
count = count
}
count(1) -- the Counter 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 is created within a
reactive scope so that it can be tracked.

View file

@ -0,0 +1,64 @@
# Property Binding
Explicitly creating effects to update properties can become verbose when there
are a lot of properties to update. Vide provides a way to *implicitly* create
an effect to update properties on source update. This is also known as
*property binding*, as a property is binded to reflect some data.
```lua
local create = vide.create
local source = vide.source
local function Counter()
local count = source(0)
return create "TextButton" {
Text = function()
return "count: " .. count()
end,
Activated = function()
count(count() + 1)
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 a side-effect to update that property anytime
a dependent source is updated.
Just like effects, the function is ran immediately in a reactive-scope to set
the property initially and determine what sources are being depended on.
This allows you as the programmer to not need to manually update UI as the state
of your program changes. You just define how the data maps to UI, and Vide's
reactive system will automatically update any properties depending on sources
that are updated.
## Children Binding
Children can also be set in a similar manner.
```lua
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 a single text label "A"
items {
create "TextLabel" { Text = "B" },
create "TextLabel" { Text = "C" }
}
-- this will automatically unparent the text label "A", and parent the labels "B" and "C".
```

View file

@ -6,7 +6,7 @@ is used to register a cleanup callback for the next time the reactive scope
it is called in re-runs.
```lua
local vide = require(vide)
locla mount = vide.mount
local source = vide.source
local cleanup = vide.cleanup
@ -26,15 +26,23 @@ local function Timer()
Size = UDim2.fromOffset(200, 50),
Text = function()
return "seconds: " .. count()
return "seconds: " .. math.floor(count())
end,
}
end
mount(function() return create "ScreenGui" { Timer {} } end, game.StarterGui)
local unmount = mount(Timer)
unmount() -- all registered cleanups are ran, heartbeat connection stopped
```
In the above example, this allows us to disconnect the heartbeat connection
when the timer component is destroyed, whether that is from unmounting the app
or if it is dynamically created by a control-flow function, which will be
covered next.
On a related note: the reason why `mount()` is used to create your app, is so
that any top-level components that need to be cleaned up, can be cleaned up
when the app is later unmounted, since `mount()` runs in a reactive-scope to
track `cleanup()` calls. Vide's entire reactive system is independent from the
life-time of instances; instances are just a side-effect of the reactive system.

View file

@ -52,6 +52,10 @@ Above is an example of using a switch to create a login menu. Each time
`loggedIn` toggles, the current button will be destroyed, and a new button
created, which the text to represent the current action, to log in or log out.
The callbacks given to control flow functions are ran in a new reactive-scope,
so any cleanups registered will be ran when the input is changed and a new
output is created.
Another control flow function, `indexes()`, is used to create elements from an
input table.