local Type = require(script.Parent.Type)
local ElementKind = require(script.Parent.ElementKind)
local ElementUtils = require(script.Parent.ElementUtils)
local Children = require(script.Parent.PropMarkers.Children)
local Symbol = require(script.Parent.Symbol)
local internalAssert = require(script.Parent.internalAssert)
local config = require(script.Parent.GlobalConfig).get()
local InternalData = Symbol.named("InternalData")
local function createReconciler(renderer)
local reconciler
local mountVirtualNode
local updateVirtualNode
local unmountVirtualNode
local function replaceVirtualNode(virtualNode, newElement)
local hostParent = virtualNode.hostParent
local hostKey = virtualNode.hostKey
local depth = virtualNode.depth
local parent = virtualNode.parent
local context = virtualNode.originalContext or virtualNode.context
local parentLegacyContext = virtualNode.parentLegacyContext
if not virtualNode.wasUnmounted then
unmountVirtualNode(virtualNode)
end
local newNode = mountVirtualNode(newElement, hostParent, hostKey, context, parentLegacyContext)
if newNode ~= nil then
newNode.depth = depth
newNode.parent = parent
end
return newNode
end
local function updateChildren(virtualNode, hostParent, newChildElements)
if config.internalTypeChecks then
internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode")
end
virtualNode.updateChildrenCount = virtualNode.updateChildrenCount + 1
local currentUpdateChildrenCount = virtualNode.updateChildrenCount
local removeKeys = {}
for childKey, childNode in pairs(virtualNode.children) do
local newElement = ElementUtils.getElementByKey(newChildElements, childKey)
local newNode = updateVirtualNode(childNode, newElement)
if virtualNode.updateChildrenCount ~= currentUpdateChildrenCount then
if newNode and newNode ~= virtualNode.children[childKey] then
unmountVirtualNode(newNode)
end
return
end
if newNode ~= nil then
virtualNode.children[childKey] = newNode
else
removeKeys[childKey] = true
end
end
for childKey in pairs(removeKeys) do
virtualNode.children[childKey] = nil
end
for childKey, newElement in ElementUtils.iterateElements(newChildElements) do
local concreteKey = childKey
if childKey == ElementUtils.UseParentKey then
concreteKey = virtualNode.hostKey
end
if virtualNode.children[childKey] == nil then
local childNode = mountVirtualNode(
newElement,
hostParent,
concreteKey,
virtualNode.context,
virtualNode.legacyContext
)
if virtualNode.updateChildrenCount ~= currentUpdateChildrenCount then
if childNode then
unmountVirtualNode(childNode)
end
return
end
if childNode ~= nil then
childNode.depth = virtualNode.depth + 1
childNode.parent = virtualNode
virtualNode.children[childKey] = childNode
end
end
end
end
local function updateVirtualNodeWithChildren(virtualNode, hostParent, newChildElements)
updateChildren(virtualNode, hostParent, newChildElements)
end
local function updateVirtualNodeWithRenderResult(virtualNode, hostParent, renderResult)
if Type.of(renderResult) == Type.Element or renderResult == nil or typeof(renderResult) == "boolean" then
updateChildren(virtualNode, hostParent, renderResult)
else
error(
("%s\n%s"):format(
"Component returned invalid children:",
virtualNode.currentElement.source or "<enable element tracebacks>"
),
0
)
end
end
function unmountVirtualNode(virtualNode)
if config.internalTypeChecks then
internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode")
end
virtualNode.wasUnmounted = true
local kind = ElementKind.of(virtualNode.currentElement)
if kind == ElementKind.Host then
renderer.unmountHostNode(reconciler, virtualNode)
elseif kind == ElementKind.Function then
for _, childNode in pairs(virtualNode.children) do
unmountVirtualNode(childNode)
end
elseif kind == ElementKind.Stateful then
virtualNode.instance:__unmount()
elseif kind == ElementKind.Portal then
for _, childNode in pairs(virtualNode.children) do
unmountVirtualNode(childNode)
end
elseif kind == ElementKind.Fragment then
for _, childNode in pairs(virtualNode.children) do
unmountVirtualNode(childNode)
end
else
error(("Unknown ElementKind %q"):format(tostring(kind)), 2)
end
end
local function updateFunctionVirtualNode(virtualNode, newElement)
local children = newElement.component(newElement.props)
updateVirtualNodeWithRenderResult(virtualNode, virtualNode.hostParent, children)
return virtualNode
end
local function updatePortalVirtualNode(virtualNode, newElement)
local oldElement = virtualNode.currentElement
local oldTargetHostParent = oldElement.props.target
local targetHostParent = newElement.props.target
assert(renderer.isHostObject(targetHostParent), "Expected target to be host object")
if targetHostParent ~= oldTargetHostParent then
return replaceVirtualNode(virtualNode, newElement)
end
local children = newElement.props[Children]
updateVirtualNodeWithChildren(virtualNode, targetHostParent, children)
return virtualNode
end
local function updateFragmentVirtualNode(virtualNode, newElement)
updateVirtualNodeWithChildren(virtualNode, virtualNode.hostParent, newElement.elements)
return virtualNode
end
function updateVirtualNode(virtualNode, newElement, newState: { [any]: any }?): { [any]: any }?
if config.internalTypeChecks then
internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode")
end
if config.typeChecks then
assert(
Type.of(newElement) == Type.Element or typeof(newElement) == "boolean" or newElement == nil,
"Expected arg #2 to be of type Element, boolean, or nil"
)
end
if virtualNode.currentElement == newElement and newState == nil then
return virtualNode
end
if typeof(newElement) == "boolean" or newElement == nil then
unmountVirtualNode(virtualNode)
return nil
end
if virtualNode.currentElement.component ~= newElement.component then
return replaceVirtualNode(virtualNode, newElement)
end
local kind = ElementKind.of(newElement)
local shouldContinueUpdate = true
if kind == ElementKind.Host then
virtualNode = renderer.updateHostNode(reconciler, virtualNode, newElement)
elseif kind == ElementKind.Function then
virtualNode = updateFunctionVirtualNode(virtualNode, newElement)
elseif kind == ElementKind.Stateful then
shouldContinueUpdate = virtualNode.instance:__update(newElement, newState)
elseif kind == ElementKind.Portal then
virtualNode = updatePortalVirtualNode(virtualNode, newElement)
elseif kind == ElementKind.Fragment then
virtualNode = updateFragmentVirtualNode(virtualNode, newElement)
else
error(("Unknown ElementKind %q"):format(tostring(kind)), 2)
end
if not shouldContinueUpdate then
return virtualNode
end
virtualNode.currentElement = newElement
return virtualNode
end
local function createVirtualNode(element, hostParent, hostKey, context, legacyContext)
if config.internalTypeChecks then
internalAssert(
renderer.isHostObject(hostParent) or hostParent == nil,
"Expected arg #2 to be a host object"
)
internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil")
internalAssert(
typeof(legacyContext) == "table" or legacyContext == nil,
"Expected arg #5 to be of type table or nil"
)
end
if config.typeChecks then
assert(hostKey ~= nil, "Expected arg #3 to be non-nil")
assert(
Type.of(element) == Type.Element or typeof(element) == "boolean",
"Expected arg #1 to be of type Element or boolean"
)
end
return {
[Type] = Type.VirtualNode,
currentElement = element,
depth = 1,
parent = nil,
children = {},
hostParent = hostParent,
hostKey = hostKey,
updateChildrenCount = 0,
wasUnmounted = false,
legacyContext = legacyContext,
parentLegacyContext = legacyContext,
context = context or {},
originalContext = nil,
}
end
local function mountFunctionVirtualNode(virtualNode)
local element = virtualNode.currentElement
local children = element.component(element.props)
updateVirtualNodeWithRenderResult(virtualNode, virtualNode.hostParent, children)
end
local function mountPortalVirtualNode(virtualNode)
local element = virtualNode.currentElement
local targetHostParent = element.props.target
local children = element.props[Children]
assert(renderer.isHostObject(targetHostParent), "Expected target to be host object")
updateVirtualNodeWithChildren(virtualNode, targetHostParent, children)
end
local function mountFragmentVirtualNode(virtualNode)
local element = virtualNode.currentElement
local children = element.elements
updateVirtualNodeWithChildren(virtualNode, virtualNode.hostParent, children)
end
function mountVirtualNode(element, hostParent, hostKey, context, legacyContext)
if config.internalTypeChecks then
internalAssert(
renderer.isHostObject(hostParent) or hostParent == nil,
"Expected arg #2 to be a host object"
)
internalAssert(
typeof(legacyContext) == "table" or legacyContext == nil,
"Expected arg #5 to be of type table or nil"
)
end
if config.typeChecks then
assert(hostKey ~= nil, "Expected arg #3 to be non-nil")
assert(
Type.of(element) == Type.Element or typeof(element) == "boolean",
"Expected arg #1 to be of type Element or boolean"
)
end
if typeof(element) == "boolean" then
return nil
end
local kind = ElementKind.of(element)
local virtualNode = createVirtualNode(element, hostParent, hostKey, context, legacyContext)
if kind == ElementKind.Host then
renderer.mountHostNode(reconciler, virtualNode)
elseif kind == ElementKind.Function then
mountFunctionVirtualNode(virtualNode)
elseif kind == ElementKind.Stateful then
element.component:__mount(reconciler, virtualNode)
elseif kind == ElementKind.Portal then
mountPortalVirtualNode(virtualNode)
elseif kind == ElementKind.Fragment then
mountFragmentVirtualNode(virtualNode)
else
error(("Unknown ElementKind %q"):format(tostring(kind)), 2)
end
return virtualNode
end
local function mountVirtualTree(element, hostParent, hostKey)
if config.typeChecks then
assert(Type.of(element) == Type.Element, "Expected arg #1 to be of type Element")
assert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object")
end
if hostKey == nil then
hostKey = "RoactTree"
end
local tree = {
[Type] = Type.VirtualTree,
[InternalData] = {
rootNode = nil,
mounted = true,
},
}
tree[InternalData].rootNode = mountVirtualNode(element, hostParent, hostKey)
return tree
end
local function unmountVirtualTree(tree)
local internalData = tree[InternalData]
if config.typeChecks then
assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle")
assert(internalData.mounted, "Cannot unmounted a Roact tree that has already been unmounted")
end
internalData.mounted = false
if internalData.rootNode ~= nil then
unmountVirtualNode(internalData.rootNode)
end
end
local function updateVirtualTree(tree, newElement)
local internalData = tree[InternalData]
if config.typeChecks then
assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle")
assert(Type.of(newElement) == Type.Element, "Expected arg #2 to be a Roact Element")
end
internalData.rootNode = updateVirtualNode(internalData.rootNode, newElement)
return tree
end
reconciler = {
mountVirtualTree = mountVirtualTree,
unmountVirtualTree = unmountVirtualTree,
updateVirtualTree = updateVirtualTree,
createVirtualNode = createVirtualNode,
mountVirtualNode = mountVirtualNode,
unmountVirtualNode = unmountVirtualNode,
updateVirtualNode = updateVirtualNode,
updateVirtualNodeWithChildren = updateVirtualNodeWithChildren,
updateVirtualNodeWithRenderResult = updateVirtualNodeWithRenderResult,
}
return reconciler
end
return createReconciler