diff --git a/src/Components/DynamicList.lua b/src/Components/DynamicList.lua index 3877b14..49cc37e 100644 --- a/src/Components/DynamicList.lua +++ b/src/Components/DynamicList.lua @@ -5,37 +5,53 @@ local createElement = React.createElement local useState = React.useState local useEffect = React.useEffect local memo = React.memo +local cloneElement = React.cloneElement +-- DynamicList reconciles a dictionary of keyed children. +-- Fixes: previous code wrapped ALL children with removal props each render causing churn & potential loops. +-- Strategy: only wrap children flagged for removal; avoid mutating props; bail out when no change. local function DynamicList(props: { children: {} }) local children = props.children local list, setList = useState({}) useEffect(function() setList(function(prevState) - local state = table.clone(prevState) + local nextState = table.clone(prevState) + local changed = false + -- Add or update active children for key, child in children do - state[key] = ReactUtil.updateReactChild(child) - end - - for key, child in state do - if children[key] then - continue + local updated = ReactUtil.updateReactChild(child) + if nextState[key] ~= updated then + nextState[key] = updated + changed = true end + end - child.props.remove = true - child.props.destroy = function() - setList(function(currentState) - local clonedState = table.clone(currentState) - clonedState[key] = nil - return clonedState - end) + -- Handle removals: mark existing entries not in children + for key, existing in nextState do + if children[key] == nil and existing ~= nil then + if not (existing.props and existing.props.remove) then + local wrapped = cloneElement(existing, { + remove = true, + destroy = function() + setList(function(currentState) + local cloned = table.clone(currentState) + cloned[key] = nil + return cloned + end) + end, + }) + nextState[key] = ReactUtil.updateReactChild(wrapped) + changed = true + end end - - state[key] = ReactUtil.updateReactChild(child) end - return state + if changed then + return nextState + end + return prevState end) end, { children }) diff --git a/src/Utility/ReactUtil.lua b/src/Utility/ReactUtil.lua index 4425b08..7f24a70 100644 --- a/src/Utility/ReactUtil.lua +++ b/src/Utility/ReactUtil.lua @@ -1,9 +1,19 @@ -local TableUtil = require(script.Parent.TableUtil) +-- ReactUtil provides lightweight helpers for dealing with React element descriptors. +-- Deep copying a React element (as previously done) can erase Luau's refined type info +-- and produce 'unknown' for consumers. For reconciliation we only need to ensure +-- that a new table reference exists when we conceptually changed props. -local function updateReactChild(child) - local cloned = TableUtil.deepCopy(child) - cloned.type = child.type +local function updateReactChild(child: any) + -- If child is nil or not a table, just return it. + if type(child) ~= "table" then + return child + end + -- Shallow copy preserves element shape and keeps type field intact. + local cloned = {} + for k, v in child do + cloned[k] = v + end return cloned end diff --git a/src/Utility/TableUtil.lua b/src/Utility/TableUtil.lua deleted file mode 100644 index cbb47a8..0000000 --- a/src/Utility/TableUtil.lua +++ /dev/null @@ -1,17 +0,0 @@ -type MixedArray = { [string | number]: any } | { any } - -local function deepCopy(tbl: T): T - local new = table.clone(tbl :: any) - - for key, value in tbl :: any do - if type(value) == "table" then - new[key] = deepCopy(value) - end - end - - return (new :: any) :: T -end - -return { - deepCopy = deepCopy, -}