Fusion sometimes throwing useAfterDestroy error on a reactive & recursive node tree

Hi! I’ve been working on a project where I have to translate instances to a reactive node tree, but I ran into useAfterDestroy error. I’ve been struggling with it for the past 2 days despite simplifying my code to just a single self-contained script. Also, for the record, I did read Fusion’s documentation and watched a video covering scopes.

Source code
--!strict

local Fusion = require("@self/Fusion")

local localPlayer = game:GetService("Players").LocalPlayer
assert(localPlayer, "This script is intended to run on a client")

local terrain = workspace.Terrain
assert(terrain, "Terrain must be present")

local function insert<T>(
	reactiveArray: Fusion.Value<{T}>,
	item: T
): ()
	local array = Fusion.peek(reactiveArray) :: {T}
	table.insert(array, item)
	reactiveArray:set(array, true) -- Trigger update
end

local function removeByItem<T>(
	reactiveArray: Fusion.Value<{T}>,
	item: T
): boolean
	local array = Fusion.peek(reactiveArray) :: {T}
	local index = table.find(array, item)
	if not index then
		return false -- Nothing has been removed
	end
	table.remove(array, index)
	reactiveArray:set(array, true) -- Trigger update
	return true
end

local function unsafeReadProperty(
	instance: Instance,
	property: string
): unknown
	return (instance :: any)[property]
end

local function useProperty(
	scope: Fusion.Scope,
	instance: Instance,
	property: string
): Fusion.UsedAs<unknown>
	local value = scope:Value(unsafeReadProperty(instance, property))
	table.insert(scope, instance:GetPropertyChangedSignal(property):Connect(function(): ()
		value:set(unsafeReadProperty(instance, property))
	end))
	return value
end

local function useChildren(
	scope: Fusion.Scope,
	instance: Instance
): Fusion.UsedAs<{Instance}>
	local children = scope:Value(instance:GetChildren()) :: Fusion.Value<{Instance}>
	
	table.insert(scope, instance.ChildAdded:Connect(function(child): ()
		insert(children, child)
	end))
	
	table.insert(scope, instance.ChildRemoved:Connect(function(child): ()
		removeByItem(children, child)
	end))
	
	return children
end

type Node = {
	read name: Fusion.UsedAs<string>;
	read children: Fusion.UsedAs<{Node}>;
}

local function buildNode(
	scope: Fusion.Scope,
	instance: Instance,
	registry: {[Instance]: Node?}
): Node
	-- Prevent infinite feedback loop
	local existing = registry[instance]
	if existing then
		return existing
	end
	
	print("create", instance)
	
	table.insert(scope, function()
		print("delete", instance)
		registry[instance] = nil -- Unregister node
	end)
	
	local name = useProperty(scope, instance, "Name") :: Fusion.UsedAs<string>
	
	-- Get child instances and convert them to nodes
	local childInstances = useChildren(scope, instance)
	local childNodes = scope:ForValues(childInstances, function(
		use: Fusion.Use,
		childScope: Fusion.Scope,
		childInstance: Instance
	): Node
		return buildNode(childScope, childInstance, registry)
	end)
	
	local node: Node = {
		name = name,
		children = childNodes
	}
	
	registry[instance] = node -- Register node
	return node
end

local function buildUserInterface(
	scope: Fusion.Scope,
	node: Node,
	callback: (scope: Fusion.Scope, node: Node) -> Fusion.Child
): Fusion.Child
	return scope:New("Frame")({
		AutomaticSize = Enum.AutomaticSize.XY,
		
		[Fusion.Children] = {
			scope:New("UIListLayout")({
				SortOrder = Enum.SortOrder.LayoutOrder,
				FillDirection = Enum.FillDirection.Vertical
			}),
			callback(scope, node),
			scope:ForValues(node.children, function(
				use: Fusion.Use,
				childScope: Fusion.Scope,
				childNode: Node
			): Fusion.Child
				return buildUserInterface(childScope, childNode, callback)
			end)
		}
	})
end

local function main(scope: Fusion.Scope): Fusion.Child
	local rootNode = buildNode(scope, terrain, {})
	
	return scope:New("ScreenGui")({
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
		ResetOnSpawn = false,
		AutoLocalize = false,
		Parent = localPlayer.PlayerGui,
		
		[Fusion.Children] = {
			buildUserInterface(scope, rootNode, function(
				scope: Fusion.Child,
				node: Node
			): Fusion.Child
				return scope:New("TextLabel")({
					AutomaticSize = Enum.AutomaticSize.XY,
					BackgroundTransparency = 1,
					Text = node.name,
					TextSize = 14,
					TextXAlignment = Enum.TextXAlignment.Left
				})
			end)
		}
	})
end

main(Fusion:scoped())
Full Error
[Fusion] Error in callback:
    [Fusion] The Value (bound to the Text property) is no longer valid - it was destroyed before the TextLabel instance. See discussion #292 on GitHub for advice. 
        ID: useAfterDestroy
        Learn more: https://elttob.uk/Fusion/0.3/api-reference/general/errors/#useafterdestroy (while processing value table: [memory address]) 
    ID: callbackError
    Learn more: https://elttob.uk/Fusion/0.3/api-reference/general/errors/#callbackerror 
    ---- Stack trace ----
    ReplicatedStorage.NodeTree.Fusion.External:91 function logError
    ReplicatedStorage.NodeTree.Fusion.Memory.checkLifetime:121 function bOutlivesA
    ReplicatedStorage.NodeTree.Fusion.Instances.applyInstanceProps:82 function bindProperty
    ReplicatedStorage.NodeTree.Fusion.Instances.applyInstanceProps:114 function applyInstanceProps
    ReplicatedStorage.NodeTree.Fusion.Instances.New:47
    ReplicatedStorage.NodeTree:153
    ReplicatedStorage.NodeTree:127 function buildUserInterface
    ReplicatedStorage.NodeTree:133
    ReplicatedStorage.NodeTree.Fusion.State.ForValues:59
    ReplicatedStorage.NodeTree.Fusion.State.Computed:113 function _evaluate
    ReplicatedStorage.NodeTree.Fusion.Graph.evaluate:44 function evaluate
    ReplicatedStorage.NodeTree.Fusion.Graph.depend:24 function depend
    ReplicatedStorage.NodeTree.Fusion.State.For:99
    ReplicatedStorage.NodeTree.Fusion.State.ForValues:40 function useOutputPair
    ReplicatedStorage.NodeTree.Fusion.State.For.Disassembly:98 function populate
    ReplicatedStorage.NodeTree.Fusion.State.For:93 function _evaluate
    ReplicatedStorage.NodeTree.Fusion.Graph.evaluate:44 function evaluate
    ReplicatedStorage.NodeTree.Fusion.Graph.evaluate:30 function evaluate
    ReplicatedStorage.NodeTree.Fusion.Graph.change:77 function change
    ReplicatedStorage.NodeTree.Fusion.State.Value:74 function set
    ReplicatedStorage.NodeTree:30 function removeByItem
    ReplicatedStorage.NodeTree:64

Reproduction steps:

  1. Create a Script with context set to Client.
  2. Parent the script to ReplicatedStorage.
  3. Place Fusion v0.3 module under the script.
  4. Start play test
  5. Repeatably insert and re-parent instances under Terrain until an error occurs.

Alternatively, you can skip the first 3 steps, by downloading the script:

repo.rbxm (54.3 KB)

It turns out I was re-using nodes, which caused their scopes to get re-used, ultimately leading them to move from one parent to another, thus disconnecting them. If the new parent were to be cleaned up, the node wouldn’t get cleaned up, since its scope belongs to the previous parent. This would lead to all sorts of issues in the reactive graph which Fusion detected, hence the errors.

-- Prevent infinite feedback loop
local existing = registry[instance]
if existing then
    return existing -- Scope would be reused here, leading to all sorts of issues
end

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.