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") --[[ The reconciler is the mechanism in Roact that constructs the virtual tree that later gets turned into concrete objects by the renderer. Roact's reconciler is constructed with the renderer as an argument, which enables switching to different renderers for different platforms or scenarios. When testing the reconciler itself, it's common to use `NoopRenderer` with spies replacing some methods. The default (and only) reconciler interface exposed by Roact right now uses `RobloxRenderer`. ]] local function createReconciler(renderer) local reconciler local mountVirtualNode local updateVirtualNode local unmountVirtualNode --[[ Unmount the given virtualNode, replacing it with a new node described by the given element. Preserves host properties, depth, and context from parent. ]] local function replaceVirtualNode(virtualNode, newElement) local hostParent = virtualNode.hostParent local hostKey = virtualNode.hostKey local depth = virtualNode.depth local parentContext = virtualNode.parentContext unmountVirtualNode(virtualNode) local newNode = mountVirtualNode(newElement, hostParent, hostKey, parentContext) -- mountVirtualNode can return nil if the element is a boolean if newNode ~= nil then newNode.depth = depth end return newNode end --[[ Utility to update the children of a virtual node based on zero or more updated children given as elements. ]] 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 local removeKeys = {} -- Changed or removed children for childKey, childNode in pairs(virtualNode.children) do local newElement = ElementUtils.getElementByKey(newChildElements, childKey) local newNode = updateVirtualNode(childNode, newElement) 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 -- Added children 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) -- mountVirtualNode can return nil if the element is a boolean if childNode ~= nil then childNode.depth = virtualNode.depth + 1 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 "" ), 0) end end --[[ Unmounts the given virtual node and releases any held resources. ]] function unmountVirtualNode(virtualNode) if config.internalTypeChecks then internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") end 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 --[[ Update the given virtual node using a new element describing what it should transform into. `updateVirtualNode` will return a new virtual node that should replace the passed in virtual node. This is because a virtual node can be updated with an element referencing a different component! In that case, `updateVirtualNode` will unmount the input virtual node, mount a new virtual node, and return it in this case, while also issuing a warning to the user. ]] function updateVirtualNode(virtualNode, newElement, newState) 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 nothing changed, we can skip this update 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 -- Stateful components can abort updates via shouldUpdate. If that -- happens, we should stop doing stuff at this point. if not shouldContinueUpdate then return virtualNode end virtualNode.currentElement = newElement return virtualNode end --[[ Constructs a new virtual node but not does mount it. ]] local function createVirtualNode(element, hostParent, hostKey, context) 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") 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, children = {}, hostParent = hostParent, hostKey = hostKey, context = context, -- This copy of context is useful if the element gets replaced -- with an element of a different component type parentContext = context, } 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 --[[ Constructs a new virtual node and mounts it, but does not place it into the tree. ]] function mountVirtualNode(element, hostParent, hostKey, context) 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") 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 -- Boolean values render as nil to enable terse conditional rendering. if typeof(element) == "boolean" then return nil end local kind = ElementKind.of(element) local virtualNode = createVirtualNode(element, hostParent, hostKey, context) 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 --[[ Constructs a new Roact virtual tree, constructs a root node for it, and mounts it. ]] 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] = { -- The root node of the tree, which starts into the hierarchy of -- Roact component instances. rootNode = nil, mounted = true, }, } tree[InternalData].rootNode = mountVirtualNode(element, hostParent, hostKey) return tree end --[[ Unmounts the virtual tree, freeing all of its resources. No further operations should be done on the tree after it's been unmounted, as indicated by its the `mounted` field. ]] 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 --[[ Utility method for updating the root node of a virtual tree given a new element. ]] 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