From d682161c06229f9cb280041fe817680456ad9784 Mon Sep 17 00:00:00 2001 From: Aaron Smith <83140718+centau@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:26:01 +0000 Subject: [PATCH] Update docs --- docs/.vitepress/config.ts | 2 +- docs/tut/advanced/nested-scoping.md | 188 +++++++++++++ docs/tut/advanced/reactive-scoping.md | 253 ------------------ docs/tut/crash-course/10-cleanup.md | 45 +--- docs/tut/crash-course/11-control-flow.md | 166 ++++-------- docs/tut/crash-course/15-concepts.md | 75 +++--- docs/tut/crash-course/2-creation.md | 5 +- docs/tut/crash-course/3-components.md | 8 +- docs/tut/crash-course/5-effect.md | 8 +- docs/tut/crash-course/6-root.md | 32 ++- docs/tut/crash-course/7-stateful-component.md | 9 +- docs/tut/crash-course/8-property-binding.md | 13 +- src/graph.luau | 4 +- 13 files changed, 315 insertions(+), 493 deletions(-) create mode 100644 docs/tut/advanced/nested-scoping.md delete mode 100644 docs/tut/advanced/reactive-scoping.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 0a3500d..87dada1 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -58,7 +58,7 @@ export default withMermaid({ { text: "Advanced Reactivity", items: [ - { text: "Reactive Scopes", link: "/tut/advanced/reactive-scoping.md"} + { text: "Nested Scopes", link: "/tut/advanced/nested-scoping.md"} ] } ], diff --git a/docs/tut/advanced/nested-scoping.md b/docs/tut/advanced/nested-scoping.md new file mode 100644 index 0000000..17c95d2 --- /dev/null +++ b/docs/tut/advanced/nested-scoping.md @@ -0,0 +1,188 @@ +# Nested Reactive Scopes + +Nesting reactive scopes gives you finer control over the reactive graph, but +needs more work to do. The built-in control flow functions try to cover the +most common cases, but they do not cover all of them. + +This tutorial will demonstrate how to implement a `show()` control flow function +using just sources and effects. + +```lua +local mount = vide.mount +local source = vide.source +local show = vide.show + +local function Counter() + local count = source(0) + + return create "TextButton" { + Text = count, + Activated = function() count(count() + 1) end + } +end + +mount(function() + local toggled = source(true) + + show(toggled, Button) +end) +``` + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#1C1C1F" + } +}}%% + +graph + +subgraph mount + direction LR + toggle --> show + + subgraph show[show effect] + text[Text effect] + end +end +``` + +Above is the reactive graph for `show()`. It creates a new effect depending on +`toggle` where anytime `toggle` is truthy, it will create a new `Counter`. The +`show` effect calls `Counter`, which creates a new reactive scope to update its +text whenever `count` changes. As per the rules of reactive scopes, a reactive +scope rerunning will destroy any reactive scope created within it. So the text +effect's reactive scope is destroyed whenever the show effect is rerun. + +The same can be achieved without the use of `show()`: + +```lua +local mount = vide.mount +local source = vide.source +local effect = vide.effect +local cleanup = vide.cleanup + +local function Counter() + local count = source(0) + + return create "TextButton" { + Text = count, + Activated = function() count(count() + 1) end + } +end + +mount(function() + local toggled = source(true) + + effect(function() + if toggled() then + local destroy = mount(Button) + cleanup(destroy) + end + end) +end) +``` + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#1C1C1F" + } +}}%% + +graph + +subgraph mount + direction LR + toggle --> effect + + subgraph effect + subgraph mount2[inner mount] + text[Text effect] + end + end +end +``` + +This is another way to achieve the same. Here we use `mount()` within the effect +to manually create and destroy a new reactive scope whenever the effect reruns. + +Alternatively, instead of using `mount()`, a new reactive scope can be created +directly within the effect: + +```lua +local mount = vide.mount +local source = vide.source +local effect = vide.effect +local untrack = vide.untrack + +local function Counter() + local count = source(0) + + return create "TextButton" { + Text = count, + Activated = function() count(count() + 1) end + } +end + +mount(function() + local toggled = source(true) + + effect(function() + if toggled() then + untrack(Button) + end + end) +end) +``` + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#1B1B1F", + "primaryTextColor": "#fff", + "primaryBorderColor": "#1B1B1F", + "lineColor": "#79B8FF", + "tertiaryColor": "#161618", + "tertiaryBorderColor": "#1C1C1F" + } +}}%% + +graph + +subgraph mount + direction LR + toggle --> effect + + subgraph effect + text[Text effect] + end +end +``` + +Without the use of `untrack()`, an error would occur, since Vide does not allow +the creation of reactive scopes inside reactive scopes that are tracking. The +reason for this, is because if the `Counter` component reads from a source +internally, that can cause the reactive scope calling `Counter()` to track that +source, causing unintentional reruns. As a guard against this, you are forced to +use `untrack()` to create nested reactive scopes. + +The final result is the same as using the `show()` component. An effect is +created which creates the counter, which creates its own reactive scope. The +effect rerunning causes the counter's internal reactive scope to be destroyed, +making sure everything is cleaned up. + + diff --git a/docs/tut/advanced/reactive-scoping.md b/docs/tut/advanced/reactive-scoping.md deleted file mode 100644 index 20872cf..0000000 --- a/docs/tut/advanced/reactive-scoping.md +++ /dev/null @@ -1,253 +0,0 @@ -# Reactive Scoping - -This is a brief document designed to give the user more insight into how Vide's -reactive system works. - -## Graph Basics - -Vide's reactivity can be represented as a graph, where each source, derived -source, and effect is a node on that graph. The term "*reactive scope*" is just -an abstraction used to refer to these nodes. Each node is a reactive scope. - -Each node stores a cached value, a side-effect function, cleanup functions, -its parents and children, and its owner and owned. - -Whenever a node is updated it will: - -1. destroy its owned nodes -2. run its cleanups -3. rerun its side-effect and update its cached value -4. if its cached value changes, update its children recursively. - -There is a difference between children nodes and owned nodes: - -- children nodes are updated when a parent is updated. -- owned nodes are destroyed when a parent is updated. -- both children and owned are destroyed when a parent is destroyed. - -Nodes created by `root()` generally have no children, and only tracks owned. -Nodes created by `derive()` generally have no owned, and only tracks children. - -## Basic Example - -```lua -root(function() - local forename = source "quan" - local surname = source "xi" - - local name = derive(function() - return forename() .. " " .. surname() - end) - - effect(function() - print("new name: " .. name()) - end) -end) -``` - -This code will produce a graph that looks like so: - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#161618" - } -}}%% - -graph - subgraph root - forename & surname --> name - name --> effect - end -``` - -Nodes connected by arrows represent parent and children connections. -Nodes within other nodes represent owner and owned connections. - -Any time a node is updated, Vide will traverse and update that node's children, -its children's children, etc, until all nodes descending from that node has been -updated. Traversal will stop at a node if that node's cached value does not -change after an update. - -When the side-effect for a node is being reran when a node is updated, any -other nodes read within that side-effect are set as parents of the node -currently being reran. As those nodes are read, we know that the current node -depends on them, so any time those nodes are updated, they will update dependent -nodes since they will be stored as children. - -When destroying a node, its descendents are traversed and also destroyed. -When being destroyed, a node's connections (parents and children, owner and -owned) are cleared, and any pending cleanup functions are ran. - -The purpose of `root()` (which is called internally by `mount()`) is to setup -the root node which will track any node created inside its scope, or any -cleanups registered. Without it, nodes could be garbage collected without a -chance to run pending cleanups which can cause memory leakage. - -Nodes created by `source()` can actually exist outside of root nodes, since -they do not have direct side-effects or cleanups, they do not have to be -explicitly destroyed. - -## Control-flow Graph Example - -Control flow functions in Vide are special, as they can dynamically create and -destroy new root scopes. - -It is the combination of the above which allows us to write components like so: - -```lua -local function Counter(props: { text: string }) - local count = source(0) - - local connection = stepped:Connect(function() count(count() + 1) end) - - cleanup(function() connection:Disconnect() end) - - return create "TextLabel" { - Text = function() - return props.text() .. ": " .. count() - end - } -end -``` - -Vide doesn't recognise this as a "component", that is a user abstraction. Vide -just sees this as a function that creates nodes in the reactive graph. - -```lua -root(function() - local counters = { "A", "B" } - - indexes(counters, function(name) - return Counter { text = name } - end) -end) -``` - -This code produces a graph like so: - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#fff" - } -}}%% - -graph LR - subgraph root - counters --> indexes - - subgraph root1[subroot 1] - n1[name] --> p1[prop binding] - end - - subgraph root2[subroot 2] - n2[name] --> p2[prop binding] - end - end - - indexes .-> root1 & root2 -``` - -This shows how the `indexes()` control flow function creates and manages new -root scopes. The function creates an effect seen as `indexes` in the graph, -which manages the new roots `subroot 1` and `subroot 2`, as well as the sources -`name` for which one exists for each index value in the input table. - -When the input table changes, `indexes()` can automatically destroy and create -subroots based on the changed indexes. Destroyed nodes run any cleanups made, in -this case it is the cleanups to disconnect the counters connection. The same -applies to all other control flow functions. - -Whenever the root reactive scope is destroyed, all its children, `counters` and -`indexes` will be destroyed too, which means that `indexes` children, the -subroots, will also be destroyed. Everything is nicely cleaned up. - -## Custom Control-flow Example - -Below is a simple example of the `show()` control-flow function. - -Each time `visible` changes, `show()` will destroy the current reactive scope -and rerun its function in a new one. - -```lua -local visible = source(true) -local count = source(0) - -root(function() - show(visible, function() - return create "TextLabel" { Text = count } - end) -end) -``` - -The above code produces a graph like so: - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#1B1B1F" - } -}}%% - -graph LR - subgraph root - direction LR - show - - subgraph subroot["show() subroot"] - p1[prop binding] - end - end - - visible --> show - count --> p1 - show -.- subroot -``` - -This can be recreated without the `show()` control-flow function, with the -following code: - -```lua -local visible = source(true) -local count = source(0) - -root(function() - local output = derive(function() - visible() - - -- untrack so any source read from within this scope - -- will not cause the outer `derive()` call to rerun, - -- we only want `derive()` to rerun when `visible` changes - return untrack(function() - local label = create "TextLabel" {} - - effect(function() - label.Text = count() - end) - - return label - end) - end) -end) -``` - -Both of the above code samples will produce the same visible result. diff --git a/docs/tut/crash-course/10-cleanup.md b/docs/tut/crash-course/10-cleanup.md index b9361ec..bf632f5 100644 --- a/docs/tut/crash-course/10-cleanup.md +++ b/docs/tut/crash-course/10-cleanup.md @@ -2,7 +2,8 @@ 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 cleanup callback for the next time a reactive scope re-runs. +is used to queue a cleanup callback for the next time a reactive scope is rerun +or destroyed. ```lua local mount = vide.mount @@ -32,53 +33,19 @@ end local unmount = mount(Timer) -unmount() -- all queued cleanups are ran, heartbeat connection stopped +unmount() -- all queued cleanups are ran, heartbeat connection disconnected ``` In the above example, this allows us to disconnect the heartbeat connection 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 +::: 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. - -The reactive graph for the above example: - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#161618" - } -}}%% - -graph - -subgraph mount - direction LR - cleanup([cleanup]) ~~~ count - count --> bind["effect (text binding)"] -end -``` +::: diff --git a/docs/tut/crash-course/11-control-flow.md b/docs/tut/crash-course/11-control-flow.md index e5430b8..91f95ac 100644 --- a/docs/tut/crash-course/11-control-flow.md +++ b/docs/tut/crash-course/11-control-flow.md @@ -1,123 +1,57 @@ # Control Flow -Eventually you will need a way to dynamically create and destroy UI elements +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 *control flow* functions. These functions return new sources, which hold the instances to be displayed. -These sources can be assigned as children, meaning the displayed children -will update when the input source updates. -Control flow functions are special, because they run their components in a new -reactive scope, which can be destroyed independently of the reactive scope that -called the control flow function itself. This means that parts of your app can -be independently created then destroyed. - -## show() - -The most basic control flow function is `show()`, which is used to conditionally -show a component. - -```lua -local source = vide.source -local show = vide.show - -local function JoinMenu() - local joined = source(false) - - local function JoinButton() - return Button { - Activated = function() joined(true) end - } - end - - return create "Frame" { - show(function() return not joined() end, JoinButton) - } -end -``` - -This will make a button to join if you have not joined already. - -You can also pass a third argument, a fallback to show if the condition is falsey. - -```lua -local function JoinMenu() - local joined = source(false) - - local function JoinButton() - return Button { - Activated = function() joined(true) end - } - end - - local function LeaveButton() - return Button { - Activated = function() joined(false) end - } - end - - return create "Frame" { - show(joined, LeaveButton, JoinButton) - } -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" - } -}}%% - -graph - -subgraph root["mount() scope"] - direction LR - joined --> show -.- subroot - - subgraph subroot["show() scope"] - direction LR - Button - end -end -``` - -`show()` will implicitly create an effect depending on `joined`, which can be -seen as `show` on the graph. This effect manages, and can create or destroy -a separate reactive scope seen as `show() scope` on the graph. The dotted line -indicates that it isn't actually connected, only indirectly managed through -code. +Control flow functions run their components in a new reactive scope, which can +be destroyed independently of the reactive scope that called the control flow +function. This means parts of your app can be independently created and +destroyed. ## switch() -Similar to `show()`, `switch()`, also condtionally displays one instance at a -time. It is more flexible since it can show one of many components, based on a -table used to map a source value to a component. +`switch()` condtionally displays one instance at a time. It uses a table to map +a source value to a component. ```lua local source = vide.source local switch = vide.switch +local function Button(props: { + Text: string, + Activated: () -> () +}) + local hovered = source(false) + + return create "TextButton" { + Text = props.Text, + Activated = props.Activated, + + TextColor3 = function() + return hovered() and Color3.new(1, 1, 1) or Color3.new(.7, .7, .7) + end, + + MouseEnter = function() hovered(true) end, + MouseLeave = function() hovered(false) end + } +end + local function JoinMenu() local joined = source(false) local function JoinButton() return Button { + Text = "Join", Activated = function() joined(true) end } end local function LeaveButton() return Button { + Text = "Leave" Activated = function() joined(false) end } end @@ -131,22 +65,6 @@ local function JoinMenu() end ``` -This example is equivalent to the previous one. - -The switch can map any value to any component. - -```lua -type ActiveMenu = "none" | "inventory" | "shop" | "settings" - -local menu = source "inventory" - -switch(menu) { - inventory = InventoryMenu, - shop = ShopMenu, - settings = SettingsMenu -} -``` - The reactive graph for the above example: ```mermaid @@ -164,23 +82,30 @@ The reactive graph for the above example: graph -subgraph root["mount() scope"] +subgraph root["root scope"] direction LR - menu --> switch -.- subroot + joined --> switch -.- subroot - subgraph subroot["switch() scope"] + subgraph subroot["switch scope"] direction LR - Menu + effect["TextColor3 effect"] end end ``` +A `switch()` call creates a new effect and a new scope as seen in the above +graph. Whenever `menu` updates, it causes the `switch` effect to run, which +will destroy and recreate the switch scope with the new component. + +This will also destroy the internal effect that the button uses to highlight +itself when it is hovered, each time the switch is rerun. + ## indexes() Often, you will have a table of values with each value displayed in a similar manner. Rather than manually looping over each value to generate a corresponding -UI element, `indexes()` allows you to create elements for each table index, to -display the value at that index. +UI element, `indexes()` allows you to create elements each corresponding to a +table index, to display the value at that index. ```lua local todoList = source { @@ -222,7 +147,8 @@ value), will have its corresponding reactive scope destroyed to clean up that element. `indexes()` is said to *map* each table index to a new UI element that can -update to display the current value at that index. +update to display the current value at that index. Each table index is given a +single corresponding UI element. The reactive graph for the above example: @@ -241,16 +167,16 @@ The reactive graph for the above example: graph -subgraph root ["mount() scope"] +subgraph root ["root scope"] direction LR todoList --> indexes -.- subroot1 & subroot2 - subgraph subroot1 ["indexes() scope 1"] + subgraph subroot1 ["indexes scope 1"] direction LR value1[todo] --> prop1["prop binding"] end - subgraph subroot2 ["indexes() scope 2"] + subgraph subroot2 ["indexes scope 2"] direction LR value2[todo] --> prop2[prop binding] end diff --git a/docs/tut/crash-course/15-concepts.md b/docs/tut/crash-course/15-concepts.md index 2c2c313..62ee35d 100644 --- a/docs/tut/crash-course/15-concepts.md +++ b/docs/tut/crash-course/15-concepts.md @@ -6,73 +6,64 @@ A summary of all the concepts covered during the crash course. A source of data. -Stores a single value that can be updated by the user. +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 reponse to a source update. +Anything that happens in response 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 +Created with `effect()`. ## 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 +A scope created by certain functions such as: - `root()` - `effect()` - `derive()` -## Owner +Reactive scopes can: -A reactive scope created within an outer reactive scope, is *owned* by the outer -reactive scope. +- track sources that are read from within. +- rerun when a tracked source updates. +- track new reactive scopes created from within. -When a reactive scope is re-ran or destroyed, all reactive scopes owned by it -are also destroyed. +## Scope Owners -Vide does not let you create reactive scopes without owners. +A reactive scope created within another reactive scope is *owned* by the other +reactive scope, with the exception of the reactive scope created by `root()`. -## Root Reactive Scope +When a reactive scope is rerun or destroyed, all reactive scopes owned by it are +automatically destroyed. -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. +`root()`, which `mount()` uses internally, creates a reactive scope with no +owner, since it must be destroyed manually using a destructor +returned. ## Cleanup -Cleans up the result from an effect. +Arbitrary code to run whenever a reactive scope is rerun or destroyed. -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. +Queue a function to run using `cleanup()`. ## Tracking -Reactive scopes are tracking by default, meaning sources read from within scope -will be tracked. +Sources read from within a reactive scope will be tracked. This can be disabled +using `untrack()`, which will make reactive scopes temporarily ignore sources +read. -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()`. +The reactive scope created by `root()` is non-tracking by default. + +As a guard against misusage, a reactive scope cannot be created within a +reactive scope, unless it is made non-tracking using `untrack()`. ## Reactive Graph diff --git a/docs/tut/crash-course/2-creation.md b/docs/tut/crash-course/2-creation.md index c1cafc0..96f9dd2 100644 --- a/docs/tut/crash-course/2-creation.md +++ b/docs/tut/crash-course/2-creation.md @@ -6,7 +6,7 @@ Instances are created using `create()`. properties to assign when creating a new instance for that class. Luau allows us to omit parentheses `()` when calling functions with string or -table literals which Vide takes advantage of for brevity. +table literals which is recommended to use for brevity. ```lua local create = vide.create @@ -44,6 +44,5 @@ to a string key. 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 correct. +that class, not an instance of that class. ::: diff --git a/docs/tut/crash-course/3-components.md b/docs/tut/crash-course/3-components.md index f6b3ad4..aa81916 100644 --- a/docs/tut/crash-course/3-components.md +++ b/docs/tut/crash-course/3-components.md @@ -35,7 +35,6 @@ return Button ``` ```lua [App.luau] -local mount = vide.mount local create = vide.create local Button = require(Button) @@ -60,7 +59,7 @@ local function App() } end -mount(App, game.StarterGui) +App().Parent = game.StarterGui ``` ::: @@ -76,8 +75,3 @@ 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. - -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/5-effect.md b/docs/tut/crash-course/5-effect.md index 9f0dd6b..275a2e6 100644 --- a/docs/tut/crash-course/5-effect.md +++ b/docs/tut/crash-course/5-effect.md @@ -21,11 +21,11 @@ count(1) -- "count: 1" printed ``` -The callback given to `effect()` is initially ran immediately 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 reran too. +The callback given to `effect()` is ran immediately in a *reactive scope*. Any +source read from inside a reactive scope will be tracked, so when any of those +sources update, the effect will be reran too. -Effects also work with derived sources, it doesn't matter how deeply nested +Reactive scopes also track derived sources, it doesn't matter how deeply nested inside a function a source is. ```lua diff --git a/docs/tut/crash-course/6-root.md b/docs/tut/crash-course/6-root.md index abeec2f..4cb47da 100644 --- a/docs/tut/crash-course/6-root.md +++ b/docs/tut/crash-course/6-root.md @@ -1,12 +1,16 @@ # Root Reactive Scopes -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 UI. +Reactive scopes cannot be created on their own - they must be created within +another reactive scope so that it can be tracked and later destroyed when it is +no longer needed. -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. +This is the purpose of `mount()`, which creates an initial "root", or +"top-level" reactive scope, which all other reactive scopes, such as +ones created by `effect()`, can stem from. + +When this root reactive scope is destroyed, it will ensure all other reactive +scopes created within it are also destroyed, ensuring everything is cleaned up +properly. ```lua local source = vide.source @@ -20,13 +24,15 @@ local function App() end) end -vide.mount(App) -- works! App() -- will error since effect() was not called within a reactive scope + +vide.mount(App) -- works! + ``` -Mounting returns a function that when called will destroy any reactive scopes -created during the `mount()` call. +Mounting returns a function that when called will destroy its reactive scope, +along with any other reactive scopes created inside it. ```lua local unmount = mount(App) @@ -68,7 +74,7 @@ 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. +root reactive 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 12f16a2..85139db 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 can update in reponse to data. +A stateful component is a component that stores some data internally. Stateful components in Vide are created using sources and effects - sources to store the data, and effects to display the data. @@ -27,13 +27,18 @@ local function Counter() return instance end + +mount(Counter, game.StarterGui) ``` 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. +count source is created inside the component. + +We use `mount()` to create the counter within a reactive scope, which also takes +a second argument to parent the counter to another instance. ## External State diff --git a/docs/tut/crash-course/8-property-binding.md b/docs/tut/crash-course/8-property-binding.md index a981b8a..1fe18fc 100644 --- a/docs/tut/crash-course/8-property-binding.md +++ b/docs/tut/crash-course/8-property-binding.md @@ -1,8 +1,7 @@ # 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. +Explicitly creating effects to update properties can be tedious. Vide provides a +way to *implicitly* create an effect to update properties. ```lua local create = vide.create @@ -12,12 +11,12 @@ local function Counter() local count = source(0) return create "TextButton" { - Text = function() - return "count: " .. count() - end, - Activated = function() count(count() + 1) + end, + + Text = function() + return "count: " .. count() end } end diff --git a/src/graph.luau b/src/graph.luau index ecda1ab..2908e3d 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -45,9 +45,9 @@ local function assert_owning_scope(): Node if not scope then local caller_name = debug.info(2, "n") - return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`) + return throw(`cannot use {caller_name}() in a non-reactive scope`) elseif scope.effect then - throw("cannot create new reactive scope inside of a tracking scope") -- todo: allow this? + throw("cannot create new reactive scope in a tracking reactive scope") end return scope