-
-```Lua
-Fusion.Ref: SpecialKey
-```
-
-A [special key](../../types/specialkey) which outputs the instance it's being
-applied to.
-
-When paired with a [value object](../../../state/types/value) in a
-[property table](../../types/propertytable), the special key sets the value to
-the instance.
-
------
-
-## Learn More
-
-- [References tutorial](../../../../tutorials/roblox/references)
\ No newline at end of file
diff --git a/docs/api-reference/state/types/value.md b/docs/api-reference/state/types/value.md
index 65cfa665f..697456cf2 100644
--- a/docs/api-reference/state/types/value.md
+++ b/docs/api-reference/state/types/value.md
@@ -44,19 +44,21 @@ can be used to tell types of state object apart.
set
- -> ()
+ -> T
```Lua
function Value:set(
newValue: T
-): ()
+): T
```
-Updates the value of this state object.
+Updates the value of this state object. Other objects using the value are
+notified of the change.
-Other objects using the value are notified immediately of the change.
+The `newValue` is always returned, so that `:set()` can be used to capture
+values inside of expressions.
-----
diff --git a/docs/tutorials/best-practices/references.md b/docs/tutorials/best-practices/references.md
new file mode 100644
index 000000000..0f620c1bb
--- /dev/null
+++ b/docs/tutorials/best-practices/references.md
@@ -0,0 +1,116 @@
+At some point, you might need to refer to another part of the UI. There are
+various techniques that can let you do this.
+
+```Lua hl_lines="8"
+local ui = scope:New "Folder" {
+ [Children] = {
+ scope:New "SelectionBox" {
+ -- the box should adorn to the part, but how do you reference it?
+ Adornee = ???,
+ },
+ scope:New "Part" {
+ Name = "Selection Target",
+ }
+ }
+}
+```
+
+-----
+
+## Constants
+
+The first technique is simple - instead of creating the UI all at once, you can
+extract part of the UI that you want to reference later.
+
+In practice, that means you'll move some of the creation code into a new `local`
+constant, so that you can refer to it later by name.
+
+```Lua
+-- the part is now constructed first, whereas before it was constructed second
+local selectionTarget = scope:New "Part" {
+ Name = "Selection Target",
+}
+
+local ui = scope:New "Folder" {
+ [Children] = {
+ scope:New "SelectionBox" {
+ Adornee = selectionTarget
+ },
+ selectionTarget
+ }
+}
+```
+
+While this is a simple and robust technique, it has some disadvantages:
+
+- By moving parts of your UI code into different local variables, your UI will
+be constructed in a different order based on which local variables come first
+- Refactoring code in this way can be bothersome and inelegant, disrupting the
+structure of the code
+- You can't have two pieces of UI refer to each other cyclically
+
+Constants work well for trivial examples, but you should consider a more
+flexible technique if those disadvantages are relevant.
+
+-----
+
+## Value Objects
+
+Where it's impossible or inelegant to use named constants, you can use
+[value objects](../../fundamentals/values) to easily set up references.
+
+Because their `:set()` method returns the value that's passed in, you can use
+`:set()` to reference part of your code without disrupting its structure:
+
+```Lua
+-- `selectionTarget` will show as `nil` to all code trying to use it, until the
+-- `:set()` method is called later on.
+local selectionTarget = scope:Value(nil :: Part?)
+
+local ui = scope:New "Folder" {
+ [Children] = {
+ scope:New "SelectionBox" {
+ Adornee = selectionTarget
+ },
+ selectionTarget:set(
+ scope:New "Part" {
+ Name = "Selection Target",
+ }
+ )
+ }
+}
+```
+
+It's important to note that the value object will briefly be `nil` (or whichever
+default value you provide in the constructor). This is because it takes time to
+reach the `:set()` call, so any in-between code will see the `nil`.
+
+In the above example, the `Adornee` is briefly set to `nil`, but because
+`selectionTarget` is a value object, it will change to the part instance when
+the `:set()` method is called.
+
+While dealing with the brief `nil` value can be annoying, it is also useful,
+because this lets you refer to parts of your UI that haven't yet been created.
+In particular, this lets you create cyclic references.
+
+```Lua
+local aliceRef = scope:Value(nil :: Instance?)
+local bobRef = scope:Value(nil :: Instance?)
+
+-- These two `ObjectValue` instances will refer to each other once the code has
+-- finished running.
+local alice = aliceRef:set(
+ scope:New "ObjectValue" {
+ Value = bobRef
+ }
+)
+local bob = bobRef:set(
+ scope:New "ObjectValue" {
+ Value = aliceRef
+ }
+)
+```
+
+Value objects are generally easier to work with than named constants, so they're
+often used as the primary way of referencing UI, but feel free to mix both
+techniques based on what your code needs.
\ No newline at end of file
diff --git a/docs/tutorials/fundamentals/values.md b/docs/tutorials/fundamentals/values.md
index 1b3b1323a..5bdf7e3cc 100644
--- a/docs/tutorials/fundamentals/values.md
+++ b/docs/tutorials/fundamentals/values.md
@@ -53,6 +53,21 @@ health:set(25)
print(peek(health)) --> 25
```
+??? tip "`:set()` returns the value you give it"
+ You can use `:set()` in the middle of calculations:
+
+ ```Lua
+ local myNumber = scope:Value(0)
+ local computation = 10 + myNumber:set(2 + 2)
+ print(computation) --> 14
+ print(peek(myNumber)) --> 4
+ ```
+
+ This is useful when building complex expressions. On a later page, you'll
+ see one such use case.
+
+ Generally though, it's better to keep your expressions simple.
+
Value objects are Fusion's simplest 'state object'. State objects contain a
single value - their *state*, you might say - and that single value can be read
out at any time using `peek()`.
diff --git a/docs/tutorials/roblox/references.md b/docs/tutorials/roblox/references.md
deleted file mode 100644
index 047307264..000000000
--- a/docs/tutorials/roblox/references.md
+++ /dev/null
@@ -1,89 +0,0 @@
-The `[Ref]` key allows you to save a reference to an instance you're hydrating
-or creating.
-
-```Lua
-local myRef = scope:Value()
-
-local thing = scope:New "Part" {
- [Ref] = myRef
-}
-
-print(peek(myRef)) --> Part
-print(peek(myRef) == thing) --> true
-```
-
------
-
-## Usage
-
-`Ref` doesn't need a scope - import it into your code from Fusion directly.
-
-```Lua linenums="1" hl_lines="2"
-local Fusion = require(ReplicatedStorage.Fusion)
-local Ref = Fusion.Ref
-```
-
-When creating an instance with `New`, `[Ref]` will save that instance to a value
-object.
-
-```Lua
-local myRef = scope:Value()
-
-scope:New "Part" {
- [Ref] = myRef
-}
-
-print(peek(myRef)) --> Part
-```
-
-Among other things, this allows you to refer to instances from other instances.
-
-```Lua
-local myPart = scope:Value()
-
-New "SelectionBox" {
- -- the selection box should adorn to the part
- Adornee = myPart
-}
-
-New "Part" {
- -- sets `myPart` to this part, which sets the adornee to this part
- [Ref] = myPart
-}
-```
-
-You can also get references to instances from deep inside function calls.
-
-```Lua
--- this will refer to the part, once we create it
-local myPart = scope:Value()
-
-scope:New "Folder" {
- [Children] = scope:New "Folder" {
- [Children] = scope:New "Part" {
- -- save a reference into the value object
- [Ref] = myPart
- }
- }
-}
-```
-
-!!! warning "Nil hazard"
- Before the part is created, the `myPart` value object will be `nil`. Be
- careful not to use it before it's created.
-
- If you need to know about the instance ahead of time, you should create the
- instance early, and parent it in later, when you create the rest of the
- instances.
-
- ```Lua
- -- build the part elsewhere, so it can be saved to a variable
- local myPart = scope:New "Part" {}
-
- local folders = scope:New "Folder" {
- [Children] = scope:New "Folder" {
- -- parent the part into the folder here
- [Children] = myPart
- }
- }
- ```
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index 5ea624260..251f3c007 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -77,10 +77,10 @@ nav:
- Events: tutorials/roblox/events.md
- Change Events: tutorials/roblox/change-events.md
- Outputs: tutorials/roblox/outputs.md
- - References: tutorials/roblox/references.md
- Best Practices:
- Components: tutorials/best-practices/components.md
- Instance Handling: tutorials/best-practices/instance-handling.md
+ - References: tutorials/best-practices/references.md
- Callbacks: tutorials/best-practices/callbacks.md
- State: tutorials/best-practices/state.md
- Sharing Values: tutorials/best-practices/sharing-values.md
@@ -153,7 +153,6 @@ nav:
- OnChange: api-reference/roblox/members/onchange.md
- OnEvent: api-reference/roblox/members/onevent.md
- Out: api-reference/roblox/members/out.md
- - Ref: api-reference/roblox/members/ref.md
- Animation:
- Types:
- Animatable: api-reference/animation/types/animatable.md
diff --git a/src/Instances/Ref.luau b/src/Instances/Ref.luau
deleted file mode 100644
index 69e1c91a8..000000000
--- a/src/Instances/Ref.luau
+++ /dev/null
@@ -1,43 +0,0 @@
---!strict
---!nolint LocalUnused
---!nolint LocalShadow
-local task = nil -- Disable usage of Roblox's task scheduler
-
---[[
- A special key for property tables, which stores a reference to the instance
- in a user-provided Value object.
-]]
-
-local Package = script.Parent.Parent
-local Types = require(Package.Types)
-local External = require(Package.External)
-local isState = require(Package.State.isState)
-local whichLivesLonger = require(Package.Memory.whichLivesLonger)
-
-return {
- type = "SpecialKey",
- kind = "Ref",
- stage = "observer",
- apply = function(
- self: Types.SpecialKey,
- scope: Types.Scope,
- value: unknown,
- applyTo: Instance
- )
- if not isState(value) then
- External.logError("invalidRefType")
- end
- local value = value :: Types.StateObject
- if value.kind ~= "Value" then
- External.logError("invalidRefType")
- end
- local value = value :: Types.Value
-
- if value.scope == nil then
- External.logError("useAfterDestroy", nil, "The Value object, which [Ref] outputs to,", `the {applyTo} instance`)
- elseif whichLivesLonger(scope, applyTo, value.scope, value.oldestTask) == "definitely-a" then
- External.logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`)
- end
- value:set(applyTo)
- end
-} :: Types.SpecialKey
\ No newline at end of file
diff --git a/src/State/Value.luau b/src/State/Value.luau
index d56ae731d..d1e724f26 100644
--- a/src/State/Value.luau
+++ b/src/State/Value.luau
@@ -33,13 +33,14 @@ local CLASS_METATABLE = {__index = class}
function class:set(
newValue: unknown,
force: boolean?
-)
+): unknown
local self = self :: InternalTypes.Value
local oldValue = self._value
if force or not isSimilar(oldValue, newValue) then
self._value = newValue
updateAll(self)
end
+ return newValue
end
--[[
diff --git a/src/Types.luau b/src/Types.luau
index 3b7c1a659..f8b294286 100644
--- a/src/Types.luau
+++ b/src/Types.luau
@@ -99,8 +99,8 @@ export type Use = (target: UsedAs) -> T
-- A state object whose value can be set at any time by the user.
export type Value = StateObject & {
kind: "State",
- set: (Value, newValue: S, force: boolean?) -> (),
- ____phantom_setType: (never) -> S -- phantom data so this contains a T
+ set: (Value, newValue: S, force: boolean?) -> S,
+ ____phantom_setType: (never) -> S -- phantom data so this contains a T
}
export type ValueConstructor = (
scope: Scope,
@@ -256,7 +256,6 @@ export type Fusion = {
New: NewConstructor,
Hydrate: HydrateConstructor,
- Ref: SpecialKey,
Child: ({Child}) -> Child,
Children: SpecialKey,
Out: (propertyName: string) -> SpecialKey,
diff --git a/src/init.luau b/src/init.luau
index 87759eb2d..841f392b4 100644
--- a/src/init.luau
+++ b/src/init.luau
@@ -70,7 +70,6 @@ local Fusion: Types.Fusion = table.freeze {
OnChange = require(script.Instances.OnChange),
OnEvent = require(script.Instances.OnEvent),
Out = require(script.Instances.Out),
- Ref = require(script.Instances.Ref),
-- Animation
Tween = require(script.Animation.Tween),
diff --git a/test/Spec/Instances/Ref.spec.luau b/test/Spec/Instances/Ref.spec.luau
deleted file mode 100644
index ae0319eaa..000000000
--- a/test/Spec/Instances/Ref.spec.luau
+++ /dev/null
@@ -1,30 +0,0 @@
---!strict
---!nolint LocalUnused
-local task = nil -- Disable usage of Roblox's task scheduler
-
-local ReplicatedStorage = game:GetService("ReplicatedStorage")
-local Fusion = ReplicatedStorage.Fusion
-
-local New = require(Fusion.Instances.New)
-local Ref = require(Fusion.Instances.Ref)
-local Value = require(Fusion.State.Value)
-local peek = require(Fusion.State.peek)
-local doCleanup = require(Fusion.Memory.doCleanup)
-
-return function()
- local it = getfenv().it
-
- it("should set State objects passed as [Ref]", function()
- local expect = getfenv().expect
-
- local scope = {}
- local refValue = Value(scope, nil)
-
- local child = New(scope, "Folder") {
- [Ref] = refValue
- }
-
- expect(peek(refValue)).to.equal(child)
- doCleanup(scope)
- end)
-end
diff --git a/test/Spec/State/Value.spec.luau b/test/Spec/State/Value.spec.luau
index f945bb425..0538b4bb6 100644
--- a/test/Spec/State/Value.spec.luau
+++ b/test/Spec/State/Value.spec.luau
@@ -59,4 +59,26 @@ return function()
doCleanup(scope)
end)
+
+
+ it("always returns the set value", function()
+ local expect = getfenv().expect
+
+ local scope = {}
+ local value = Value(scope, 0 :: string | number)
+
+ local foo = value:set(10)
+ expect(foo).to.equal(10)
+
+ local bar = value:set(10)
+ expect(bar).to.equal(10)
+
+ local baz = value:set(25)
+ expect(baz).to.equal(25)
+
+ local garb = value:set(25)
+ expect(garb).to.equal(25)
+
+ doCleanup(scope)
+ end)
end