Try improve crash course

This commit is contained in:
Aaron Smith 2023-11-21 18:48:47 +00:00
parent c288cb92c4
commit 338c66ed57
11 changed files with 223 additions and 102 deletions

View file

@ -51,7 +51,8 @@ export default withMermaid({
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" }, { text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
{ text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" }, { text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" },
{ text: "Actions", link: "/tut/crash-course/13-actions" }, { text: "Actions", link: "/tut/crash-course/13-actions" },
{ text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" } { text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" },
{ text: "Concepts Summary", link: "/tut/crash-course/15-concepts" }
] ]
}, },
{ {

View file

@ -1,45 +1,25 @@
# Introduction # Introduction
This is a brief tutorial designed to give you a quick run through the usage of This is a tutorial that introduces the concepts and usage of Vide.
Vide.
Vide is heavily inspired by [Solid](https://www.solidjs.com/). Vide is heavily inspired by [Solid](https://www.solidjs.com/).
This tutorial assumes familiarity with Luau and Roblox GUI. This tutorial assumes familiarity with Luau and Roblox UI.
## Why Vide? ## Why Vide?
Creating UI is a slow and tedious process. The purpose of Vide is to make UI Creating UI is complicated, slow, and tedious.
declarative and concise, making it faster to create and more importantly easier
to maintain. Vide achieves this using a reactive style of programming which Vide tries to simplify and speed up this process by providing a declarative and
allows you to focus on the flow of data through your application without reactive of style programming, which lets you focus more on designing the UI
worrying about manually updating UI instances. itself and not having to manually update or reparent UI instances.
Some of the main focuses behind Vide's design choices: Some of the main focuses behind Vide's design choices:
- Concise syntax. - Minimal syntax.
- Being completely typecheckable. - Complete typechecking
- Independence from instance lifetimes. - Independence from instances.
- Real reactivity.
## Structure Of A Vide App As with most declarative libraries, there is an initial learning curve to
understand the concepts and usage. This tutorial tries to comprehensively
The entry point for all Vide apps is the `mount()` function. This function cover these concepts and usage, more so than you need just to use it.
sets up Vide's reactivity system. It takes and calls a function that should
create your entire app, and will apply its result to a target.
In Vide, your app should be composed of functions, each function creates a
specific part of your app, and can be reused if needed. These functions are
called *components*.
```lua
local function App()
return {
PlayerStats(),
Inventory(),
Settings()
}
end
mount(App, game.StarterGui)
```

View file

@ -2,8 +2,7 @@
Sometimes you may need to do some cleanup when destroying a component or after 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 a side-effect from a source update. Vide provides a function `cleanup()` which
is used to register a cleanup callback for the next time the reactive scope is used to queue a cleanup callback for the next time a reactive scope re-runs.
it is called in re-runs.
```lua ```lua
local mount = vide.mount local mount = vide.mount
@ -33,18 +32,33 @@ end
local unmount = mount(Timer) local unmount = mount(Timer)
unmount() -- all registered cleanups are ran, heartbeat connection stopped unmount() -- all queued cleanups are ran, heartbeat connection stopped
``` ```
In the above example, this allows us to disconnect the heartbeat connection 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 when the reactive scope responsible for creating the timer component is
or if it is dynamically created by a control-flow function, which will be destroyed, such as when it is unmounted.
covered next.
Vide does not see "components", it only sees reactive scopes and how they are
linked together. Components are just a user pattern that creates UI instances
alongside effects. In other words, instances are just a side-effect of the
reactive graph. When a reactive scope is created, you create a corresponding
instance to display that data, when that reactive scope is destroyed, any
cleanups queued will be ran and take care of anything that needs to be, such
as disconnecting connections.
This is another reason why `mount()` is used at the top level of your app, so This is another reason why `mount()` is used at the top level of your app, so
that any registered cleanups created by your app components can be ran when that any registered cleanups created by your app components can be ran when
they are destroyed. they are destroyed.
Side note: 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.
The reactive graph for the above example: The reactive graph for the above example:
```mermaid ```mermaid

View file

@ -0,0 +1,132 @@
# 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 by the user.
## Effect
Anything that happens in reponse to a source update.
Vide has built-in functions to create effects such as
- `effect()` - runs arbitrary user code on source update
- `derive()` - updates a derived source on source update
## Reactive Scope
A scope created by certain Vide functions where source updates can be tracked,
and cleanups queued.
When a source used inside a reactive scope is updated, the reactive scope will
rerun.
Reactive scopes are created by functions such as
- `root()`
- `effect()`
- `derive()`
## Owner
A reactive scope created within an outer reactive scope, is *owned* by the outer
reactive scope.
When a reactive scope is re-ran or destroyed, all reactive scopes owned by it
are also destroyed.
Vide does not let you create reactive scopes without owners.
## Root Reactive Scope
A top-level reactive scope. These scopes are an exception to the owner rule.
Created by `root()`, which `mount()` uses internally.
A root reactive scope can be created on its own. It allows other reactive scopes
to be created with an owner.
Root reactive scopes must be destroyed manually by the user, a function to do
this is given by `root()`.
A root reactive scope can be created within another reactive scope and it will
not automatically be owned by that scope.
## Cleanup
Cleans up the result from an effect.
Unneeded in most cases, a cleanup is arbitrary code that can be ran before
a reactive scope is rerun or destroyed, so that the result from the previous
run can be cleaned up. A cleanup can be queued by using `cleanup()` within
a reactive scope.
## Tracking
Reactive scopes are tracking by default, meaning sources read from within scope
will be tracked.
A reactive scope can be made temporarily non-tracking within `untrack()`, so
that any source used will be ignored. The only function that creates a
nontracking reactive scope by default is `root()`.
## Reactive Graph
The combination of 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
```lua
local count = source(0)
root(function()
local text = derive(function()
return "count: " .. text()
end)
effect(function()
print(text())
end)
end)
```
### Graph resulting from code
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#1B1B1F",
"primaryTextColor": "#fff",
"primaryBorderColor": "#1B1B1F",
"lineColor": "#79B8FF",
"tertiaryColor": "#161618",
"tertiaryBorderColor": "#1C1C1F"
}
}}%%
graph LR
subgraph root
text --> effect
end
count --> text
```
Notes:
- Since `count` is a source, not an effect, it can exist
outside of a root reactive scope.
- An update to `count` will cause `text` to rerun, which
then causes `effect` to rerun.
- When the root reactive scope is destroyed, `text` and
`effect` will be destroyed alongside it, since they are
owned by it. `count` will be untouched and future updates
to `count` will have no effect.

View file

@ -9,36 +9,31 @@ Luau allows us to omit parentheses `()` when calling functions with string or
table literals which Vide takes advantage of for brevity. table literals which Vide takes advantage of for brevity.
```lua ```lua
local mount = vide.mount
local create = vide.create local create = vide.create
local function App() return create "ScreenGui" {
return create "ScreenGui" { create "Frame" {
create "Frame" { AnchorPoint = Vector2.new(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromScale(0.4, 0.7),
Size = UDim2.fromScale(0.4, 0.7),
create "TextLabel" { create "TextLabel" {
Text = "hi" Text = "hi"
}, },
create "TextLabel" { create "TextLabel" {
Text = "bye" Text = "bye"
}, },
create "TextButton" { create "TextButton" {
Text = "click me", Text = "click me",
Activated = function() Activated = function()
print "clicked!" print "clicked!"
end end
}
} }
} }
end }
mount(App, game.StarterGui)
``` ```
Assign a value to a string key to set a property, and assign a value to a Assign a value to a string key to set a property, and assign a value to a

View file

@ -1,8 +1,11 @@
# Components # 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. A component is a function that creates and returns a piece of UI.
This is a way to separate your app into small chunks that you can reuse and put This is a way to separate your UI into small chunks that you can reuse and put
together. together.
::: code-group ::: code-group
@ -66,12 +69,15 @@ Above is a simple example of a button component being used across files.
A single parameter `props` is used to pass properties to the component. A single parameter `props` is used to pass properties to the component.
Components allow you to *encapsulate* behavior. You can only modify the 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. through the `props` parameter.
To create a new button all you must do is call the `Button` function, passing in To create a new button all you must do is call the `Button` function, passing in
values. This saves having to create and set every property each time. Also, when values. 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 updating the button component in future, any changes to the button file will be
seen anywhere the button is used in your app. seen anywhere the button is used in your app.
This can be extended to much more complicated UI. The `mount()` function is used to set up Vide's reactivity system when creating
your UI. It only needs to be called once at the top-level with the function that
puts together your entire app. It also parents the returned instance to another
a target instance for you.

View file

@ -23,9 +23,6 @@ count(count() + 1) -- increment count by 1
Sources can be *derived* by wrapping them in functions. A wrapped source Sources can be *derived* by wrapping them in functions. A wrapped source
effectively becomes a new source. effectively becomes a new source.
Derived sources should be pure functions. This is where the same output is
always produced for the same input no matter how many times it is reran.
```lua ```lua
local count = source(0) local count = source(0)
@ -40,6 +37,5 @@ print(text()) -- "count: 1"
Sources on their own aren't very special, the above can be achieved with plain Sources on their own aren't very special, the above can be achieved with plain
variables. The real use for sources become apparent when used in combination variables. The real use for sources become apparent when used in combination
with Vide's *reactive scopes*. When a source is read from within a reactive with *effects*. Similar to a signal and connection, a source and effect allows
scope, it can automatically rerun the scope that reads it when the source is you to do things like automatically updating UI when a source is updated.
updated in the future.

View file

@ -2,9 +2,9 @@
Any reactive scopes created, such as by `effect()`, must be done so within a Any reactive scopes created, such as by `effect()`, must be done so within a
"root" reactive scope. This is the main purpose of `mount()`, which you use "root" reactive scope. This is the main purpose of `mount()`, which you use
once at the top level to create your app as shown in the first introduction. once at the top level to create your UI.
This is so that when the app is unmounted, it can clean up any reactive scopes This is so that if you want to destroy your UI, it can stop any reactive scopes
created within it, since reactive scopes track any reactive scopes created created within it, since reactive scopes track any reactive scopes created
within them. within them.
@ -53,16 +53,22 @@ The reactive graph for the above example looks like so:
graph graph
subgraph root["mount"] subgraph root
direction LR direction LR
count --> effect count --> effect
end end
``` ```
When the `mount` scope is destroyed, the `effect` scope will also be destroyed When the root reactive scope created by `mount()` is destroyed, the `effect`
since it was created within it. 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 You don't need to worry about ensuring all your effects are created within a
root scope, since you should be creating all your UI and corresponding effects root scope, since you should be creating all your UI and corresponding effects
within a top-level `mount()` call that puts all your UI together. So it is safe within a top-level `mount()` call that puts all your UI together. So it is safe
to assume that any effect you create will be created under this top level scope. to assume that any effect you create will be created under this top level scope.
Vide will prevent you from accidently doing otherwise anyways.

View file

@ -1,6 +1,6 @@
# Stateful Components # Stateful Components
A stateful component is a component that stores and displays some data. A stateful component is a component that can update in reponse to data.
Stateful components in Vide are created using sources and effects - sources to Stateful components in Vide are created using sources and effects - sources to
store the data, and effects to display the data. store the data, and effects to display the data.
@ -32,9 +32,6 @@ end
Above is an example of a counter component, that when clicked, will increment 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. its internal count, and automatically update its text to reflect that count.
Making a property update based on a source is also referred to as *property
binding*.
Each instance of `Counter()` will maintain its own independent count, since the Each instance of `Counter()` will maintain its own independent count, since the
count source is created inside the scope of the component. count source is created inside the scope of the component.
@ -70,4 +67,4 @@ count(1) -- the Counter component will update to display this count
Sources can be created internally or passed in from externally, there are no 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 restrictions on how they are used as long as the effect using it is created
within a reactive scope so that it can be cleaned up later. within a reactive scope.

View file

@ -25,17 +25,17 @@ end
This example is equivalent to the example seen on the previous page. This example is equivalent to the example seen on the previous page.
Instead of explicitly creating an effect, assigning a (non-event) property Instead of explicitly creating an effect, assigning a (non-event) property a
a function will implicitly create a side-effect to update that property anytime function will implicitly create an effect to update that property anytime a
a dependent source is updated. source used within is updated.
Just like effects, the function is ran immediately in a reactive scope to set 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. the property initially and determine what sources are being used.
This allows you as the programmer to not need to manually update UI as the state 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 sources map to UI, and of your program changes. You just define how data sources map to UI, and Vide's
Vide's reactive system will automatically update any properties depending on reactive system will automatically update any properties depending on those
those sources that were updated. sources.
## Children Binding ## Children Binding

View file

@ -29,13 +29,10 @@ local text = function()
return "count: " .. tostring(count()) return "count: " .. tostring(count())
end end
effect(function() effect(function() text() end)
text() -- prints "ran" effect(function() text() end)
end)
effect(function() source(1) -- prints "ran" x2
text() -- prints "ran" again
end)
``` ```
To avoid this, you can use `derive()` to derive a new source instead. This will To avoid this, you can use `derive()` to derive a new source instead. This will
@ -55,16 +52,13 @@ local text = derive(function()
return "count: " .. tostring(count()) return "count: " .. tostring(count())
end) end)
effect(function() effect(function() text() end)
text() -- prints "ran" effect(function() text() end)
end)
effect(function() source(1) -- prints "ran" x1
text() -- does not print, returns cached value
end)
``` ```
`derive()` must also be used within a root reactive scope, just like `effect()`. `derive()` must also be called within a reactive scope, just like `effect()`.
If the recalculated value is the same as the old value, the derived source will If the recalculated value is the same as the old value, the derived source will
not rerun the effects using it. not rerun the effects using it.