From 338c66ed57162063c87b5a61b73950685ea5338e Mon Sep 17 00:00:00 2001 From: Aaron Smith <83140718+centau@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:48:47 +0000 Subject: [PATCH] Try improve crash course --- docs/.vitepress/config.ts | 3 +- docs/tut/crash-course/1-introduction.md | 46 ++---- docs/tut/crash-course/10-cleanup.md | 26 +++- docs/tut/crash-course/15-concepts.md | 132 ++++++++++++++++++ docs/tut/crash-course/2-creation.md | 39 +++--- docs/tut/crash-course/3-components.md | 14 +- docs/tut/crash-course/4-source.md | 8 +- docs/tut/crash-course/6-root.md | 16 ++- docs/tut/crash-course/7-stateful-component.md | 7 +- docs/tut/crash-course/8-property-binding.md | 14 +- docs/tut/crash-course/9-derived-source.md | 20 +-- 11 files changed, 223 insertions(+), 102 deletions(-) create mode 100644 docs/tut/crash-course/15-concepts.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 7c820ff..0a3500d 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -51,7 +51,8 @@ export default withMermaid({ { text: "Control Flow", link: "/tut/crash-course/11-control-flow" }, { text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" }, { 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" } ] }, { diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md index 604f7ae..261ce83 100644 --- a/docs/tut/crash-course/1-introduction.md +++ b/docs/tut/crash-course/1-introduction.md @@ -1,45 +1,25 @@ # Introduction -This is a brief tutorial designed to give you a quick run through the usage of -Vide. +This is a tutorial that introduces the concepts and usage of Vide. 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? -Creating UI is a slow and tedious process. The purpose of Vide is to make UI -declarative and concise, making it faster to create and more importantly easier -to maintain. Vide achieves this using a reactive style of programming which -allows you to focus on the flow of data through your application without -worrying about manually updating UI instances. +Creating UI is complicated, slow, and tedious. + +Vide tries to simplify and speed up this process by providing a declarative and +reactive of style programming, which lets you focus more on designing the UI +itself and not having to manually update or reparent UI instances. Some of the main focuses behind Vide's design choices: -- Concise syntax. -- Being completely typecheckable. -- Independence from instance lifetimes. -- Real reactivity. +- Minimal syntax. +- Complete typechecking +- Independence from instances. -## Structure Of A Vide App - -The entry point for all Vide apps is the `mount()` function. This function -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) -``` +As with most declarative libraries, there is an initial learning curve to +understand the concepts and usage. This tutorial tries to comprehensively +cover these concepts and usage, more so than you need just to use it. diff --git a/docs/tut/crash-course/10-cleanup.md b/docs/tut/crash-course/10-cleanup.md index 4f62233..b9361ec 100644 --- a/docs/tut/crash-course/10-cleanup.md +++ b/docs/tut/crash-course/10-cleanup.md @@ -2,8 +2,7 @@ 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 register a cleanup callback for the next time the reactive scope -it is called in re-runs. +is used to queue a cleanup callback for the next time a reactive scope re-runs. ```lua local mount = vide.mount @@ -33,18 +32,33 @@ end 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 -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. +when the reactive scope responsible for creating the timer component is +destroyed, such as when it is unmounted. + +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 that any registered cleanups created by your app components can be ran when 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: ```mermaid diff --git a/docs/tut/crash-course/15-concepts.md b/docs/tut/crash-course/15-concepts.md new file mode 100644 index 0000000..2c2c313 --- /dev/null +++ b/docs/tut/crash-course/15-concepts.md @@ -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. diff --git a/docs/tut/crash-course/2-creation.md b/docs/tut/crash-course/2-creation.md index e66f987..c1cafc0 100644 --- a/docs/tut/crash-course/2-creation.md +++ b/docs/tut/crash-course/2-creation.md @@ -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. ```lua -local mount = vide.mount local create = vide.create -local function App() - 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), +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 = "hi" + }, - create "TextLabel" { - Text = "bye" - }, + create "TextLabel" { + Text = "bye" + }, - create "TextButton" { - Text = "click me", + create "TextButton" { + Text = "click me", - Activated = function() - print "clicked!" - end - } + Activated = function() + print "clicked!" + end } } -end - -mount(App, game.StarterGui) +} ``` Assign a value to a string key to set a property, and assign a value to a diff --git a/docs/tut/crash-course/3-components.md b/docs/tut/crash-course/3-components.md index a9f38d9..f6b3ad4 100644 --- a/docs/tut/crash-course/3-components.md +++ b/docs/tut/crash-course/3-components.md @@ -1,8 +1,11 @@ # 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 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. ::: 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. -Components allow you to *encapsulate* behavior. You can only modify the -component in ways that you allow in the component, through the `props` parameter. +You can only modify the component in ways that you allow in the component, +through the `props` parameter. 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 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. +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. diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md index f5a848a..c356b6b 100644 --- a/docs/tut/crash-course/4-source.md +++ b/docs/tut/crash-course/4-source.md @@ -23,9 +23,6 @@ count(count() + 1) -- increment count by 1 Sources can be *derived* by wrapping them in functions. A wrapped 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 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 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 -scope, it can automatically rerun the scope that reads it when the source is -updated in the future. +with *effects*. Similar to a signal and connection, a source and effect allows +you to do things like automatically updating UI when a source is updated. diff --git a/docs/tut/crash-course/6-root.md b/docs/tut/crash-course/6-root.md index 2b2d7ad..abeec2f 100644 --- a/docs/tut/crash-course/6-root.md +++ b/docs/tut/crash-course/6-root.md @@ -2,9 +2,9 @@ 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 -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 within them. @@ -53,16 +53,22 @@ The reactive graph for the above example looks like so: graph -subgraph root["mount"] +subgraph root direction LR count --> effect end ``` -When the `mount` scope is destroyed, the `effect` scope will also be destroyed -since it was created within it. +When the root reactive scope created by `mount()` is destroyed, the `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 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 to assume that any effect you create will be created under this top level scope. +Vide will prevent you from accidently doing otherwise anyways. diff --git a/docs/tut/crash-course/7-stateful-component.md b/docs/tut/crash-course/7-stateful-component.md index 8de8209..12f16a2 100644 --- a/docs/tut/crash-course/7-stateful-component.md +++ b/docs/tut/crash-course/7-stateful-component.md @@ -1,6 +1,6 @@ # 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 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 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 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 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. diff --git a/docs/tut/crash-course/8-property-binding.md b/docs/tut/crash-course/8-property-binding.md index 8304565..a981b8a 100644 --- a/docs/tut/crash-course/8-property-binding.md +++ b/docs/tut/crash-course/8-property-binding.md @@ -25,17 +25,17 @@ 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. +Instead of explicitly creating an effect, assigning a (non-event) property a +function will implicitly create an effect to update that property anytime a +source used within 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. +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 -of your program changes. You just define how the data sources map to UI, and -Vide's reactive system will automatically update any properties depending on -those sources that were updated. +of your program changes. You just define how data sources map to UI, and Vide's +reactive system will automatically update any properties depending on those +sources. ## Children Binding diff --git a/docs/tut/crash-course/9-derived-source.md b/docs/tut/crash-course/9-derived-source.md index 17e3b77..d3b7156 100644 --- a/docs/tut/crash-course/9-derived-source.md +++ b/docs/tut/crash-course/9-derived-source.md @@ -29,13 +29,10 @@ local text = function() return "count: " .. tostring(count()) end -effect(function() - text() -- prints "ran" -end) +effect(function() text() end) +effect(function() text() end) -effect(function() - text() -- prints "ran" again -end) +source(1) -- prints "ran" x2 ``` 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()) end) -effect(function() - text() -- prints "ran" -end) +effect(function() text() end) +effect(function() text() end) -effect(function() - text() -- does not print, returns cached value -end) +source(1) -- prints "ran" x1 ``` -`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 not rerun the effects using it.