Merge reactive scope refactor

This commit is contained in:
aaron 2023-09-15 12:54:42 +01:00
parent 0e439f084f
commit efc4798ddb
48 changed files with 2750 additions and 1949 deletions

View file

@ -2,6 +2,38 @@
<br/>
## mount()
Runs a function and applies its result to a target instance.
- **Type**
```lua
function mount<T>(component: () -> T, target: Instance?): () -> ()
```
- **Details**
The result of the function is applies to the target in the same way
properties are using `create()`.
The function is ran in a new reactive scope, just like
[root()](reactivity-core.md#root).
Returns a function that when called will destroy the reactive scope.
- **Example**
```lua
local function App()
return create "ScreenGui" {
create "TextLabel" { Text = "Vide" }
}
end
mount(App, game.StarterGui)
```
## create()
Creates a new UI element, applying any given properties.
@ -28,19 +60,18 @@ Creates a new UI element, applying any given properties.
- **Property setting rules**
- If a table index is a string:
- If its value is a table then it will attempt to perform aggregate
initialization.
- If its value is a function then it will either bind that property to
the function or connect it if the property type is a `RBXScriptSignal`.
- If the value is not a function then the property will be set to that
value.
- If a table index is a number:
- If its value is a table then that table will be recursively
- processed just like the outer table.
- If its value is a function then it will parent and bind any instances
returned by that function as children.
- If its value is an action then that action will be queued to run after
properties are set.
- If its value is a table then that table will be recursively
processed just like the outer table.
- If its value is a function then it will bind the instances children to
that function.
- If its value is an instance then it will be parented to the instance.
- **Example**
@ -54,7 +85,7 @@ Creates a new UI element, applying any given properties.
}
```
A component using property nesting/grouping.
A component using property nesting.
```lua
type Layout = {

View file

@ -2,6 +2,30 @@
<br/>
## root()
Creates and runs a function in a new reactive scope.
- **Type**
```lua
function root<T...>(fn: (destroy: () -> ()) -> T...): T...
```
- **Details**
Creates a new root reactive scope, where creation and derivations of sources
can be tracked and properly disposed of.
Returns the result of the given function.
A function to destroy the root is passed into the callback, which will run
any cleanups and allow derived sources created to garbage collect.
::: warning
`fn()` cannot yield.
:::
## source()
Creates a new source with the given value.
@ -14,10 +38,10 @@ Creates a new source with the given value.
- **Details**
Calling the returned source with no arguments will return its stored value,
calling with arguments will set a new value.
Calling the returned source with no argument will return its stored value,
calling with an argument will set a new value.
Reading from the source from within any reactive scope will cause changes
Reading from the source from within a reactive scope will cause changes
to that source to be tracked and anything depending on it to update.
- **Example**
@ -30,44 +54,39 @@ Creates a new source with the given value.
count(count() + 1) -- 1
```
## watch()
## effect()
Runs a callback on source update.
Runs a side-effect on source update.
- **Type**
```lua
function watch(source: () -> ()): Unwatch
type Unwatch = () -> ()
function effect(callback: () -> ())
```
- **Details**
The source callback is ran immediately to determine what states are
referenced.
The callback is ran immediately.
Any time a source referenced in the callback is changed, the callback will
be reran.
Also returns a function that when called, stops the watcher immediately.
::: warning
`source()` cannot yield.
`callback()` cannot yield.
:::
- **Example**
```lua
local state = source(1)
local num = source(1)
watch(function()
print(state())
effect(function()
print(num())
end)
-- prints 1
state(state() + 1)
num(num() + 1)
-- prints 2
```
@ -110,150 +129,4 @@ Derives a new source from existing sources.
text() -- "count: 1"
```
## indexes()
Maps each index in a table source to an object.
- **Type**
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each index in the
source table. The first argument is a source containing the index's value
and the second argument is just the index.
Anytime a new index is added, the transform function will be called again
for that new index.
Anytime an existing index value changes, the transform function is not rerun,
instead the source value for that index will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
- **Example**
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
## values()
Maps each value in a table source to an object.
- **Type**
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each value in the
source table. The first argument is the index's value and
the second argument is a source containing the index.
Anytime a new value is added, the transform function will be called again
for that new value.
Anytime an existing value's index changes, the transform function is not
rerun, instead the source index for that value will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
::: warning
Having primitive values in the source table can cause unexpected behavior,
as duplicate primitives can result in multiple index sources being bound
to the same UI element.
:::
- **Example**
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = item.Name
Image = "rbxassetid://" .. item.icon,
LayoutOrder = i
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive value. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
In most cases, both functions will appear to have the same behavior.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders.
--------------------------------------------------------------------------------

186
docs/api/reactivity-flow.md Normal file
View file

@ -0,0 +1,186 @@
# Reactivity API: Control Flow
<br/>
## switch()
Changes object based on a source and a mapping table.
- **Type**
```lua
function switch<K, V>(source: () -> K): (map: Map<K, () -> V>) -> V?
```
- **Details**
The mapped function is ran in a new reactive scope that is destroyed when
the source changes and maps to a different function.
::: warning
Mapped functions cannot yield.
:::
- **Example**
```lua
local logged = source(false)
local button = switch(logged) {
[true] = function()
return Button { Text = "Log out", Toggle = logged }
end,
[false] = function()
return Button { Text = "Log in", Toggle = logged }
end
}
```
## indexes()
Maps each index in a table source to an object.
- **Type**
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each index in the
source table. The first argument is a source containing the index's value
and the second argument is just the index.
Anytime a new index is added, the transform function will be called again
for that new index.
Anytime an existing index value changes, the transform function is not rerun,
instead the source value for that index will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
- **Example**
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
## values()
Maps each value in a table source to an object.
- **Type**
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each value in the
source table. The first argument is the index's value and
the second argument is a source containing the index.
Anytime a new value is added, the transform function will be called again
for that new value.
Anytime an existing value's index changes, the transform function is not
rerun, instead the source index for that value will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
::: warning
Having primitive values in the source table can cause unexpected behavior,
as duplicate primitives can result in multiple index sources being bound
to the same UI element.
:::
- **Example**
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = item.Name
Image = "rbxassetid://" .. item.icon,
LayoutOrder = i
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive value. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
In most cases, both functions will appear to have the same behavior.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders.
--------------------------------------------------------------------------------

View file

@ -2,7 +2,7 @@
## cleanup()
Runs a callback anytime a function scope is re-ran.
Runs a callback anytime a reactive scope is re-ran.
- **Type**
@ -10,25 +10,12 @@ Runs a callback anytime a function scope is re-ran.
function cleanup(callback: () -> ())
```
- **Details**
The primary purpose of this function is to provide a means of cleaning up
side effects caused by source updates and `watch()` updates.
The stack is inspected to find the function that calls `cleanup()`. The
callback passed is called anytime the caller is re-ran, and when the caller
finally garbage collects.
::: warning
Only one `cleanup()` call is allowed per function scope.
:::
- **Example**
```lua
local data = source(1)
watch(function()
effect(function()
local label = create "TextLabel" { Text = data() }
cleanup(function()
@ -53,7 +40,7 @@ Runs a callback anytime a function scope is re-ran.
## untrack()
Gets the value of a source without reactively tracking it.
Runs a given function where any sources read will not track its reactive scope.
- **Type**
@ -82,3 +69,5 @@ Gets the value of a source without reactively tracking it.
a(1)
print(sum()) -- 2
```
--------------------------------------------------------------------------------

View file

@ -12,14 +12,14 @@ and identifying improper usage.
Currently, strict mode will:
1. Run derived sources twice a source updates.
2. Run watchers twice when a source updates.
2. Run effects twice when a source updates.
3. Throw an error if yields occur where they are not allowed.
4. Checks for `indexes()` and `values()` returning primitive values.
5. Checks for duplicate nested properties at same depth.
6. Better error reporting and stack traces.
7. Checks for multiple `cleanup()` calls in the same function scope.
By rerunning sources and watchers, any side-effects are made more apparent.
By rerunning sources and effects, any side-effects are made more apparent.
This also helps ensure that cleanups are being handled correctly.
Accidental yielding within reactive scopes can break Vide's reactive graph,