diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index dec9146..7c820ff 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -43,13 +43,15 @@ export default withMermaid({ { 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: "Stateful Components", link: "/tut/crash-course/6-stateful-component" }, - { text: "Property Binding", link: "/tut/crash-course/7-property-binding" }, - { text: "Cleanup", link: "/tut/crash-course/8-cleanup" }, - { text: "Control Flow", link: "/tut/crash-course/9-control-flow" }, - { text: "Property Nesting", link: "/tut/crash-course/10-property-nesting" }, - { text: "Actions", link: "/tut/crash-course/11-actions" }, - { text: "Strict Mode", link: "/tut/crash-course/12-strict-mode" }, + { text: "Root Scopes", link: "/tut/crash-course/6-root" }, + { text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" }, + { text: "Property Binding", link: "/tut/crash-course/8-property-binding" }, + { text: "Derived Sources", link: "/tut/crash-course/9-derived-source" }, + { text: "Cleanup", link: "/tut/crash-course/10-cleanup" }, + { 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" } ] }, { diff --git a/docs/tut/advanced/reactive-scoping.md b/docs/tut/advanced/reactive-scoping.md index f75525e..c9ed084 100644 --- a/docs/tut/advanced/reactive-scoping.md +++ b/docs/tut/advanced/reactive-scoping.md @@ -204,22 +204,23 @@ The above code produces a graph like so: "primaryBorderColor": "#1B1B1F", "lineColor": "#79B8FF", "tertiaryColor": "#161618", - "tertiaryBorderColor": "#fff" + "tertiaryBorderColor": "#1B1B1F" } }}%% flowchart LR subgraph root + direction LR show - subgraph subroot[show subroot] + subgraph subroot["show() subroot"] p1[prop binding] end end visible --> show count --> p1 - show .-> subroot + show -.- subroot ``` This can be recreated without the `show()` control-flow function, with the diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md index 371950a..ccf739a 100644 --- a/docs/tut/crash-course/1-introduction.md +++ b/docs/tut/crash-course/1-introduction.md @@ -15,11 +15,10 @@ 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. +- Concise syntax. - Being completely typecheckable. - 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. +- Real reactivity. ## Structure Of A Vide App @@ -33,8 +32,10 @@ called *components*. ```lua local function App() - return create "ScreenGui" { - create "TextLabel" { Text = "hi" } + return { + PlayerStats(), + Inventory(), + Settings() } end diff --git a/docs/tut/crash-course/8-cleanup.md b/docs/tut/crash-course/10-cleanup.md similarity index 64% rename from docs/tut/crash-course/8-cleanup.md rename to docs/tut/crash-course/10-cleanup.md index 16d611d..df4614d 100644 --- a/docs/tut/crash-course/8-cleanup.md +++ b/docs/tut/crash-course/10-cleanup.md @@ -41,8 +41,30 @@ 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. +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. + +The reactive graph for the above example: + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#161618" + } +}}%% + +flowchart + +subgraph root + direction LR + cleanup([cleanup]) ~~~ count + count --> bind[text binding] +end +``` diff --git a/docs/tut/crash-course/9-control-flow.md b/docs/tut/crash-course/11-control-flow.md similarity index 67% rename from docs/tut/crash-course/9-control-flow.md rename to docs/tut/crash-course/11-control-flow.md index afeabfc..dcb0bbf 100644 --- a/docs/tut/crash-course/9-control-flow.md +++ b/docs/tut/crash-course/11-control-flow.md @@ -63,6 +63,37 @@ local function JoinMenu() end ``` +The reactive graph for the above example: + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#1C1C1F" + } +}}%% + +flowchart + +subgraph root ["mount() scope"] + direction LR + joined --> show -.- subroot + + subgraph subroot ["show() scope"] + direction LR + Button + end +end +``` + +The dotted line indicates that the new reactive scope isn't actually connected +to the `show` on the graph, it is only managed internally through code. + ## switch() Similar to `show()`, `switch()`, also condtionally displays one instance at a @@ -113,6 +144,34 @@ switch(menu) { } ``` +The reactive graph for the above example: + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#1C1C1F" + } +}}%% + +flowchart + +subgraph root ["mount() scope"] + direction LR + joined --> show -.- subroot + + subgraph subroot ["switch() scope"] + direction LR + Button + end +end +``` + ## indexes() Often, you will have a table of values that will be displayed in a similar @@ -121,7 +180,7 @@ UI element, `indexes()` allows you to create an instance for each table index, to display the value at that index. ```lua -local todoList = { +local todoList = source { "finish the crash course", "star vide's GitHub" } @@ -153,5 +212,38 @@ given source for that index is updated. An element is only destroyed if the value of an index is set to `nil`. +The reactive graph for the above example: + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#1C1C1F" + } +}}%% + +flowchart + +subgraph root ["mount() scope"] + 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 +``` + Together, these control flow functions cover the majority of cases where you need to dynamically create and destroy parts of your UI. diff --git a/docs/tut/crash-course/10-property-nesting.md b/docs/tut/crash-course/12-property-nesting.md similarity index 100% rename from docs/tut/crash-course/10-property-nesting.md rename to docs/tut/crash-course/12-property-nesting.md diff --git a/docs/tut/crash-course/11-actions.md b/docs/tut/crash-course/13-actions.md similarity index 100% rename from docs/tut/crash-course/11-actions.md rename to docs/tut/crash-course/13-actions.md diff --git a/docs/tut/crash-course/12-strict-mode.md b/docs/tut/crash-course/14-strict-mode.md similarity index 100% rename from docs/tut/crash-course/12-strict-mode.md rename to docs/tut/crash-course/14-strict-mode.md diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md index 4419b72..f8f9e86 100644 --- a/docs/tut/crash-course/4-source.md +++ b/docs/tut/crash-course/4-source.md @@ -24,6 +24,9 @@ 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) @@ -36,7 +39,8 @@ count(1) print(text()) -- "count: 1" ``` -Derived sources should be pure functions. This is where the same output is -always produced for the same input. As well as making source updates more -predictable, knowing that updates are pure allows Vide to use optimizations such -as caching, to avoid updating derived sources if their inputs are the same. +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. diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md index 406ccd2..22cd199 100644 --- a/docs/tut/crash-course/5-effect.md +++ b/docs/tut/crash-course/5-effect.md @@ -1,8 +1,8 @@ # Effects Effects are functions that are ran in response to source updates. They are -called effects because they cause *side-effects* when reacting to source updates -which are pure. +called effects because they cause *side-effects* when reacting to source +updates. Effects are created using `effect()`. @@ -26,10 +26,10 @@ from inside a reactive scope will be tracked, so that if any of those sources update, the effect will be reran too. The callback is first ran immediately inside the `effect()` call to initially -figure out what sources are being used. +track sources used. -Effects also work with derived sources, it doesn't matter how deeply nested a -source is. +Effects also work with derived sources, it doesn't matter how deeply nested +inside a function a source is. ```lua local source = vide.source @@ -50,15 +50,7 @@ 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, the following graph is formed. Anywhere -an update occures, everything further down the graph is updated. +The reactive graph for the above example: ```mermaid %%{init: { @@ -69,25 +61,12 @@ an update occures, everything further down the graph is updated. "primaryBorderColor": "#1B1B1F", "lineColor": "#79B8FF", "tertiaryColor": "#161618", - "tertiaryBorderColor": "#161618" + "tertiaryBorderColor": "#fff" } }}%% flowchart LR - count --> doubled --> effect + +count --> effect + ``` - -You should not update other sources using an effect. Improper usage can lead to -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. diff --git a/docs/tut/crash-course/6-root.md b/docs/tut/crash-course/6-root.md new file mode 100644 index 0000000..ff45184 --- /dev/null +++ b/docs/tut/crash-course/6-root.md @@ -0,0 +1,49 @@ +# Root Reactive Scopes + +Any reactive scopes created, such as one from `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. + +This is so that when the app is unmounted, it can clean up any reactive scopes +created within it, since reactive scopes track any reactive scopes created +within them. + +```lua +local source = vide.source +local effect = vide.effect + +local function App() + local count = source(0) + + effect(function() + print(count()) + end) +end + +vide.mount(App) -- works! + +App() -- will error since effect() was not called within a reactive scope +``` + +The reactive graph for the above example: + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#161618" + } +}}%% + +flowchart + +subgraph root + direction LR + count --> effect +end +``` diff --git a/docs/tut/crash-course/6-stateful-component.md b/docs/tut/crash-course/7-stateful-component.md similarity index 95% rename from docs/tut/crash-course/6-stateful-component.md rename to docs/tut/crash-course/7-stateful-component.md index 3d16279..246e050 100644 --- a/docs/tut/crash-course/6-stateful-component.md +++ b/docs/tut/crash-course/7-stateful-component.md @@ -30,6 +30,9 @@ 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 called *property +binding*. + Each instance of `Counter()` will maintain its own independent count, since the count source is created inside the scope of the component. diff --git a/docs/tut/crash-course/7-property-binding.md b/docs/tut/crash-course/8-property-binding.md similarity index 87% rename from docs/tut/crash-course/7-property-binding.md rename to docs/tut/crash-course/8-property-binding.md index 9488fba..c8985c9 100644 --- a/docs/tut/crash-course/7-property-binding.md +++ b/docs/tut/crash-course/8-property-binding.md @@ -2,9 +2,7 @@ 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*, since changes to a source will automatically update the -property. +an effect to update properties on source update. ```lua local create = vide.create @@ -40,7 +38,9 @@ that are updated. ## Children Binding -Children can also be set in a similar manner. +Children can also be set in a similar manner. Sources bound to properties can +return an instance or an array of instances. Vide will automatically unparent +removed instances and parent new instances. ```lua local items = source { diff --git a/docs/tut/crash-course/9-derived-source.md b/docs/tut/crash-course/9-derived-source.md new file mode 100644 index 0000000..52574eb --- /dev/null +++ b/docs/tut/crash-course/9-derived-source.md @@ -0,0 +1,95 @@ +# Derived Sources + +We have seen the basic way to derive a source: + +```lua +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. + +```lua +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() -- prints "ran" +end) + +effect(function() + text() -- prints "ran" again +end) +``` + +To avoid this, you can use `derive()` to derive a new source instead. This will +run a callback in a new reactive scope only when a dependent source has updated. +Reading this derived source multiple times will just return a cached result from +when it last updated. + +```lua +local source = vide.source +local derive = vide.derive +local effect = vide.effect + +local count = source(0) + +local text = derive(function() + print "ran" + return "count: " .. tostring(count()) +end) + +effect(function() + text() -- prints "ran" +end) + +effect(function() + text() -- does not print +end) +``` + +Deriving a source in this manner is similar to creating an effect to update +another source. You should never manually do 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. + +`derive()` must also be used within a root reactive scope, just like `effect()`. + +The reactive graph for the above example: + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#161618" + } +}}%% + +flowchart + +subgraph root + direction LR + count --> text --> effect1 & effect2 +end +```