Roact App not rendering after a few times?

My roact app isn’t rendering after a bit, and I can’t figure out why. I’m dying to figure this out, and I can’t figure it out. Somebody who knows about Roact, please help me out. The rodux state is updating, but the tree isn’t. I need some reviewing of this code, because it may not be proper Roact code.

My structure:

Client
├── App
│   ├── AFK
│   ├── Inventory
│   ├── Money
│   ├── Shop
│   ├── Spectate
│   ├── Status
│   ├── Voting

Client

local Players = game:GetService("Players")

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

local GetRemoteEvent = require("GetRemoteEvent")

local TimeSyncService = require("TimeSyncService")

TimeSyncService:Init()

local Roact = require("Roact")

local RoactRodux = require("RoactRodux")

local Store = require("Store")

local TIMER_BEGIN_REMOTE = GetRemoteEvent("TimerBegin")

local STATUS_REMOTE = GetRemoteEvent("StatusRemote")

local App = require(script:WaitForChild("App"))

local PlayerGui = Players.LocalPlayer.PlayerGui

local ScreenGui = Instance.new("ScreenGui", PlayerGui)

ScreenGui.ResetOnSpawn = false

ScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Global

local app = Roact.createElement(RoactRodux.StoreProvider, {

store = Store,

}, {

App = Roact.createElement(App)

})

local handle = Roact.mount(app, ScreenGui, "UI")

App

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

-- Modules
local RoactRodux = require("RoactRodux")
local Roact = require("Roact")

local TimeSyncService = require("TimeSyncService")
local clock = TimeSyncService:WaitForSyncedClock()

local GetRemoteEvent = require("GetRemoteEvent")
local GetRemoteFunction = require("GetRemoteFunction")

-- Remotes
local TIMER_BEGIN_REMOTE = GetRemoteEvent("TimerBegin")
local STATUS_REMOTE = GetRemoteEvent("StatusRemote")
local DATA_CHANGED_EVENT = GetRemoteEvent("DataChanged")
local VOTE_REMOTE = GetRemoteEvent("VoteForGame")

local GET_ITEMS_EVENT = GetRemoteFunction("GetItems")

-- Local Modules
local Status = require(script:WaitForChild("Status"))
local Inventory = require(script:WaitForChild("Inventory"))
local Money = require(script:WaitForChild("Money"))
local Shop = require(script:WaitForChild("Shop"))
local Spectate = require(script:WaitForChild("Spectate"))
local AFK = require(script:WaitForChild("AFK"))
local Voting = require(script:WaitForChild("Voting"))

local itemTypes, shopItems = {"Swords"}, {}
for _, itemType in ipairs(itemTypes) do
	shopItems[itemType] = GET_ITEMS_EVENT:InvokeServer(itemType)
end

-- Module
local App = Roact.PureComponent:extend("App")

function App:init()		
	STATUS_REMOTE.OnClientEvent:Connect(function(text)
		self.props.setStatus(text)
	end)
	
	VOTE_REMOTE.OnClientEvent:Connect(function(type, data)
		if type == "Initiate" then
			self.props.setUI("VOTING", 2)
			self.props.setVote(data)
		end
		
		if type == "Update" then
			self.props.setVote(data)
		end
		
		
		if type == "Ended" then
			self.props.setUI("GOD", 2)
			self.props.setVote({})
		end
	end)
	
	DATA_CHANGED_EVENT.OnClientEvent:Connect(function(data)
		self.props.setData(data)
		
		if self.props.data.Coins then
			if self.props.data.Coins < data.Coins then
				script.Cash:Play()
			end
		else
			script.Cash:Play()
		end
	end)
	
	TIMER_BEGIN_REMOTE.OnClientEvent:Connect(function(endTick)
		self.endTick = endTick
		self.props.roundStart(math.floor(self.endTick - clock:GetTime()))
	end)
end

function App:didMount()
	self.running = true
	
	coroutine.wrap(function()
		while self.running do
			wait(1)
			
			if self.props.time and self.endTick then
				if self.props.time >= 0 then
					self.props.roundStart(math.floor(self.endTick - clock:GetTime()))
				end
			end
		end
	end)()
end

function App:willUnmount()
	self.running = false
end

function App:render()
	return Roact.createElement("Frame", {
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1
	}, {
		Status = Roact.createElement(Status, {
			anchorPoint = Vector2.new(0.5, 0),
			position = UDim2.fromScale(0.5, 0),
			size = UDim2.fromScale(0.5, 0.1),
			time = self.props.time,
			status = self.props.status
		}),
		
		Shop = Roact.createElement(Shop, {
			open = self.props.open,
			setUI = self.props.setUI,
			data = self.props.data,
			shopItems = self.shopItems,
			viewingShopItem = self.props.viewingShopItem,
			changeShopItem = self.props.changeShopItem
		}),
		
		Inventory = Roact.createElement(Inventory, {
			open = self.props.open,
			setUI = self.props.setUI,
			data = self.props.data,
			shopItems = self.shopItems
		}),
		
		Money = Roact.createElement(Money, {
			data = self.props.data
		}),
		
		Spectate = Roact.createElement(Spectate, {
			open = self.props.open,
			setUI = self.props.setUI,
			index = self.props.index,
			spectate = self.props.spectate,
			setSpectate = self.props.setSpectate,
			setIndex = self.props.setIndex
		}),
		
		AFK = Roact.createElement(AFK, {
			data = self.props.data,
			setData = self.props.setData
		}),
		
		Voting = Roact.createElement(Voting, {
			open = self.props.open,
			setUI = self.props.setUI,
			vote = self.props.vote
		}),
		
		UIPadding = Roact.createElement("UIPadding", {
			PaddingBottom = UDim.new(0, 5),
			PaddingLeft = UDim.new(0, 5),
			PaddingRight = UDim.new(0, 5),
			PaddingTop = UDim.new(0, 5)
		})
	})
end

return RoactRodux.connect(
	function(state, props)
		return {
			status = state.status,
			data = state.data,
			open = state.ui,
			viewingShopItem = state.viewingShopItem,
			index = state.index,
			spectate = state.spectate,
			vote = state.vote
		}
	end,
	function(dispatch)
		return {
			roundStart = function(time)
				dispatch({
					type = "ROUND_START",
					time = time
				})
			end,
			setStatus = function(status)
				dispatch({
					type = "SET_STATUS",
					text = status
				})
			end,
			setShopItems = function(itemType, items)
				dispatch({
					type = "SET_SHOP_ITEMS",
					items = items,
					key = itemType
				})
			end,
			setData = function(data)
				dispatch({
					type = "SET_DATA",
					data = data
				})
			end,
			setUI = function(type, key)
				local ui, key = (key and type or nil), key or type
				dispatch({
					type = "OPEN_UI",
					ui = ui,
					key = key
				})
			end,
			changeShopItem = function(item)
				dispatch({
					type = "CHANGE_SHOP_ITEM",
					item = item
				})
			end,
			setIndex = function(index)
				dispatch({
					type = "SET_INDEX",
					index = index
				})
			end,
			setSpectate = function(spectate)
				dispatch({
					type = "SET_SPECTATE",
					spectate = spectate
				})
			end,
			setVote = function(vote)
				dispatch({
					type = "SET_VOTE",
					vote = vote
				})
			end
		}
	end
)(App)

AFK

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

-- Modules
local Roact = require("Roact")
local GetRemoteEvent = require("GetRemoteEvent")

-- Module
local AFK = Roact.Component:extend("AFK")

-- Events
local AFK_EVENT = GetRemoteEvent("ToggleAFK")

function AFK:init()
	self.canvasSize, self.updateCanvasSize = Roact.createBinding(Vector2.new(0, 0))
end

function AFK:render()
	
	return Roact.createFragment({
		AFKButton = Roact.createElement("TextButton", {
			-- Size/Position
			Size = UDim2.fromScale(0.1, 0.075),
			Position = UDim2.fromScale(0, 0.7), -- 0.15 y distance between side buttons
			AnchorPoint = Vector2.new(0, 0.5),
			
			-- Text
			Text = "AFK:" .. (self.props.data.AFK and " On" or " Off"),
			TextColor3 = Color3.new(1, 1, 1),
			Font = Enum.Font.GothamBold,
			TextStrokeColor3 = Color3.new(0, 0, 0),
			TextStrokeTransparency = 0.8,
			TextScaled = true,
			
			-- Color
			BackgroundColor3 = (self.props.data.AFK and Color3.new(0.152941, 1, 0.0588235) or Color3.new(0.835294, 0.0980392, 0.0980392)),
			
			-- Border
			BorderSizePixel = 0,
			
			-- Events
			[Roact.Event.Activated] = function()
				AFK_EVENT:FireServer()
			end
		}, {
			UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
				MaxTextSize = 24
			}),
			
			UICorner = Roact.createElement("UICorner", {
				CornerRadius = UDim.new(0, 5)
			}),
		}),
	})
end

return AFK

Inventory

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

-- Modules
local Roact = require("Roact")
local GetRemoteEvent = require("GetRemoteEvent")

-- Variables
local ITEM_TYPE = "Swords"

-- Module
local Inventory = Roact.Component:extend("Inventory")

-- Events
local EQUIP_ITEM_EVENT = GetRemoteEvent("EquipItem")

function Inventory:init()
	self.canvasSize, self.updateCanvasSize = Roact.createBinding(Vector2.new(0, 0))
end

function Inventory:render()
	local inventory = self.props.data[ITEM_TYPE] or {}
	local shopItems = self.props.shopItems[ITEM_TYPE] or {}
	local equipped = (self.props.data.Equipped and self.props.data.Equipped[ITEM_TYPE] or "")
	
	local i = 0
	for _, _ in pairs(shopItems) do
		i = i + 1
	end
	
	
	local FrameData = {}
	
	FrameData.UIGridLayout = Roact.createElement("UIGridLayout", {
		[Roact.Change.AbsoluteContentSize] = function(rbx)
			self.updateCanvasSize(rbx.AbsoluteContentSize)
		end,
		CellSize = self.absoluteSize
	})
	
	FrameData.UIPadding = Roact.createElement("UIPadding", {
		PaddingBottom = UDim.new(0, 5),
		PaddingLeft = UDim.new(0, 5),
		PaddingRight = UDim.new(0, 5),
		PaddingTop = UDim.new(0, 5)
	})
	
	if i > 0 then
		table.sort(inventory, function(a, b)
			return (shopItems[a].Name > shopItems[b].Name)
		end)
		
		for _, name in ipairs(inventory) do
			FrameData[name] = Roact.createElement("TextButton", {
				Text = "",
				ZIndex = 7,
				BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
				BorderSizePixel = 0,
				
				[Roact.Event.Activated] = function()
					EQUIP_ITEM_EVENT:FireServer(name, ITEM_TYPE)
				end
			}, {
				UIPadding = Roact.createElement("UIPadding", {
					PaddingBottom = UDim.new(0, 5),
					PaddingLeft = UDim.new(0, 5),
					PaddingRight = UDim.new(0, 5),
					PaddingTop = UDim.new(0, 5)
				}),
				
				UICorner = Roact.createElement("UICorner", {
					CornerRadius = UDim.new(0, 5)
				}),
				
				TextLabel = Roact.createElement("TextLabel", {
					AnchorPoint = Vector2.new(0.5, 1),
					Size = UDim2.fromScale(0.8, 0.2),
					Position = UDim2.fromScale(0.5, 1),
					
					Font = Enum.Font.GothamSemibold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextScaled = true,
					
					TextStrokeTransparency = 0.8,
					
					BackgroundTransparency = 1,
					Text = (shopItems[name] and shopItems[name].Name or ""),
					ZIndex = 8
				}, {
					UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
						MaxTextSize = 32
					}),
				}),
				
				ImageLabel = Roact.createElement("ImageLabel", {
					AnchorPoint = Vector2.new(0.5, 0.25),
					Size = UDim2.fromScale(0.7, 0.7),
					Position = UDim2.fromScale(0.5, 0.25),
					
					BackgroundTransparency = 1,
					Image = (shopItems[name] and shopItems[name].Icon or ""),
					ZIndex = 8
				})
			})
		end
	end
	
	return Roact.createFragment({
		InventoryButton = Roact.createElement("TextButton", {
			-- Size/Position
			Size = UDim2.fromScale(0.15, 0.075),
			Position = UDim2.fromScale(0, 0.5), -- 0.15 y distance between side buttons
			AnchorPoint = Vector2.new(0, 0.5),
			
			-- Text
			Text = "Inventory",
			TextColor3 = Color3.new(1, 1, 1),
			Font = Enum.Font.GothamBold,
			TextStrokeColor3 = Color3.new(0, 0, 0),
			TextStrokeTransparency = 0.8,
			TextScaled = true,
			
			-- Color
			BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
			
			-- Border
			BorderSizePixel = 0,
			
			-- Events
			[Roact.Event.Activated] = function()
				if self.props.open[1] == "INVENTORY" then
					self.props.setUI(1)
				else
					self.props.setUI("INVENTORY", 1)
				end
			end
		}, {
			UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
				MaxTextSize = 24
			}),
			
			UICorner = Roact.createElement("UICorner", {
				CornerRadius = UDim.new(0, 5)
			}),
		}),
		
		InventoryFrame = Roact.createElement("Frame", {
			-- Size/Position
			Size = UDim2.fromScale(0.5, 0.75),
			Position = UDim2.fromScale(0.5, 0.5),
			AnchorPoint = Vector2.new(0.5, 0.5),
			
			-- Color
			BackgroundColor3 = Color3.new(1, 1, 1),
			BackgroundTransparency = 1,
			
			-- Border
			BorderSizePixel = 0,
			
			-- Visibility
			ClipsDescendants = true,
			Visible = (self.props.open[1] == "INVENTORY")
		}, {
			
			UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
				AspectRatio = 1.625
			}),
			
			TopbarFrame = Roact.createElement("Frame", {
				BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
				Size = UDim2.fromScale(1, 0.1),
				BorderSizePixel = 0,
				ZIndex = 4
			}, {
				CloseButton = Roact.createElement("ImageButton", {
					Size = UDim2.fromScale(1, 1),
					Image = "rbxassetid://2419293754",
					BackgroundColor3 = Color3.new(1, 1, 1),
					BorderSizePixel = 0,
					ZIndex = 5,
					[Roact.Event.Activated] = function()
						self.props.setUI(1)
					end
				}, {
					UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint"),
					UICorner = Roact.createElement("UICorner", {
						CornerRadius = UDim.new(0, 5)
					})
				}),
				
				UIPadding = Roact.createElement("UIPadding", {
					PaddingBottom = UDim.new(0, 5),
					PaddingLeft = UDim.new(0, 5),
					PaddingRight = UDim.new(0, 5),
					PaddingTop = UDim.new(0, 5)
				}),
				
				TextLabel = Roact.createElement("TextLabel", {
					AnchorPoint = Vector2.new(0.5, 0),
					Position = UDim2.fromScale(0.5, 0),
					Size = UDim2.fromScale(0.75, 1),
					BackgroundTransparency = 1,
					FontSize = Enum.FontSize.Size32,
					Font = Enum.Font.GothamBold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextStrokeTransparency = 0.8,
					Text = "Inventory",
					ZIndex = 5
				})
			}),
			
			ScrollingFrame = Roact.createElement("ScrollingFrame", {
				AnchorPoint = Vector2.new(0, 1),
				Size = UDim2.fromScale(0.75, 0.9),
				Position = UDim2.fromScale(0, 1),
				CanvasSize = UDim2.fromOffset(self.canvasSize.X, self.canvasSize.Y),
				BorderSizePixel = 0,
				BackgroundColor3 = Color3.new(1, 1, 1),
				ZIndex = 2,
				[Roact.Change.AbsoluteSize] = function(rbx)
					self.absoluteSize = UDim2.fromScale(100 / rbx.AbsoluteSize.X, 100 / rbx.AbsoluteSize.Y)
				end
			}, FrameData),
			
			Sidebar = Roact.createElement("Frame", {
				AnchorPoint = Vector2.new(1, 1),
				Size = UDim2.fromScale(0.25, 0.9),
				Position = UDim2.fromScale(1, 1),
				
				BackgroundColor3 = Color3.new(240/255, 240/255, 240/255),
				
				BorderSizePixel = 0,
				ZIndex = 3,
			}, {				
				UIPadding = Roact.createElement("UIPadding", {
					PaddingBottom = UDim.new(0, 5),
					PaddingLeft = UDim.new(0, 5),
					PaddingRight = UDim.new(0, 5),
					PaddingTop = UDim.new(0, 5)
				}),
				
				UIListLayout = Roact.createElement("UIListLayout", {
					Padding = UDim.new(0, 5),
					SortOrder = Enum.SortOrder.LayoutOrder
				}),
				
				Icon = Roact.createElement("ImageLabel", {
					Image = ((i > 0 and equipped and shopItems[equipped]) and shopItems[equipped].Icon or ""),
					Size = UDim2.fromScale(1, 1),
					Position = UDim2.fromScale(0, 0),
					BorderSizePixel = 0,
					BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
					LayoutOrder = 1,
					ZIndex = 4,
				}, {
					UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint")
				}),
				
				NameLabel = Roact.createElement("TextLabel", {
					Text = ((i > 0 and equipped and shopItems[equipped]) and shopItems[equipped].Name or ""),
					Size = UDim2.fromScale(1, 0.1),
					Position = UDim2.new(0, 0, 0.45, 5),
					BorderSizePixel = 0,
					BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
					
					TextScaled = true,
					
					FontSize = Enum.FontSize.Size32,
					Font = Enum.Font.GothamBold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextStrokeTransparency = 0.8,
					
					LayoutOrder = 2,
					
					ZIndex = 4
				}, {
					UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
						MaxTextSize = 24
					}),	
				}),
				
				DescriptionLabel = Roact.createElement("TextLabel", {
					Text = ((i > 0 and equipped and shopItems[equipped]) and shopItems[equipped].Description or ""),
					Size = UDim2.fromScale(1, 0.2),
					Position = UDim2.new(0, 0, 0.55, 10),
					BorderSizePixel = 0,
					BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
					
					TextScaled = true,
					
					FontSize = Enum.FontSize.Size32,
					Font = Enum.Font.GothamBold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextStrokeTransparency = 0.8,
					
					LayoutOrder = 3,
					
					ZIndex = 4
				}, {
					UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
						MaxTextSize = 24
					}),	
				}),
			})
		})
	})
end

return Inventory

Money

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

local Roact = require("Roact")

local Money = Roact.Component:extend("Money")

function Money:init()
	
end

function Money:render()	
	local coins = 0
	if self.props.data.Coins then
		coins = self.props.data.Coins
	end
	return Roact.createElement("TextLabel", {
		Size = UDim2.fromScale(0.15, 0.075),
		Position = UDim2.fromScale(0, 0.325), -- 0.15 y distance between side buttons
		AnchorPoint = Vector2.new(0, 0.5),
		
		BackgroundTransparency = 1,
		FontSize = Enum.FontSize.Size32,
		Font = Enum.Font.GothamBold,
		TextColor3 = Color3.new(1, 1, 1),
		TextStrokeColor3 = Color3.new(0, 0, 0),
		TextStrokeTransparency = 0.8,
		Text = coins .. " Coins"
	})
end

return Money

Shop

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

-- Modules
local Roact = require("Roact")

-- Module
local Shop = Roact.Component:extend("Shop")
local GetRemoteEvent = require("GetRemoteEvent")

-- Variables
local ITEM_TYPE = "Swords"

-- Events
local BUY_ITEM_EVENT = GetRemoteEvent("BuyItemEvent")

function Shop:init()
	self.canvasSize, self.updateCanvasSize = Roact.createBinding(Vector2.new(0, 0))
end

function Shop:render()	
	local inventory = self.props.data[ITEM_TYPE] or {}
	local shopItems = self.props.shopItems[ITEM_TYPE] or {}
	
	local viewingShopItem = self.props.viewingShopItem
	
	local i = 0
	for name, _ in pairs(shopItems) do
		i = i + 1
	end
	
	local FrameData = {}
	
	FrameData.UIGridLayout = Roact.createElement("UIGridLayout", {
		[Roact.Change.AbsoluteContentSize] = function(rbx)
			self.updateCanvasSize(rbx.AbsoluteContentSize)
		end,
		CellSize = self.absoluteSize
	})
	
	FrameData.UIPadding = Roact.createElement("UIPadding", {
		PaddingBottom = UDim.new(0, 5),
		PaddingLeft = UDim.new(0, 5),
		PaddingRight = UDim.new(0, 5),
		PaddingTop = UDim.new(0, 5)
	})
	
	if i > 0 then
		local item = {}
		for name, data in pairs(shopItems) do
			if not table.find(inventory, name) then
				if data.Price then
					if typeof(data.Price) == "number" then
						data.Id = name
						table.insert(item, data)
					end
				end
			end
		end
		
		table.sort(item, function(a, b)
			local alphabet = a.Name > b.Name
			local price = a.Price > b.Price
			
			return alphabet and price
		end)
		
		if not viewingShopItem then
			self.props.changeShopItem(item.Id)
		end
		
		for _, data in ipairs(item) do
			FrameData[data.Id] = Roact.createElement("TextButton", {
				Text = "",
				ZIndex = 7,
				BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
				BorderSizePixel = 0,
				
				[Roact.Event.Activated] = function()
					self.props.changeShopItem(data.Id)
				end
			}, {
				UIPadding = Roact.createElement("UIPadding", {
					PaddingBottom = UDim.new(0, 5),
					PaddingLeft = UDim.new(0, 5),
					PaddingRight = UDim.new(0, 5),
					PaddingTop = UDim.new(0, 5)
				}),
				
				UICorner = Roact.createElement("UICorner", {
					CornerRadius = UDim.new(0, 5)
				}),
				
				TextLabel = Roact.createElement("TextLabel", {
					AnchorPoint = Vector2.new(0.5, 1),
					Size = UDim2.fromScale(0.8, 0.2),
					Position = UDim2.fromScale(0.5, 1),
					
					Font = Enum.Font.GothamSemibold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextScaled = true,
					
					TextStrokeTransparency = 0.8,
					
					BackgroundTransparency = 1,
					Text = data.Name,
					ZIndex = 8
				}, {
					UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
						MaxTextSize = 32
					}),
				}),
				
				ImageLabel = Roact.createElement("ImageLabel", {
					AnchorPoint = Vector2.new(0.5, 0.25),
					Size = UDim2.fromScale(0.7, 0.7),
					Position = UDim2.fromScale(0.5, 0.25),
					
					BackgroundTransparency = 1,
					Image = data.Icon,
					ZIndex = 8
				})
			})
		end
	end
	
	print(self.props.open)
	
	return Roact.createFragment({
		ShopButton = Roact.createElement("TextButton", {
			-- Size/Position
			Size = UDim2.fromScale(0.15, 0.075),
			Position = UDim2.fromScale(0, 0.4), -- 0.15 y distance between side buttons
			AnchorPoint = Vector2.new(0, 0.5),
			
			-- Text
			Text = "Shop",
			TextColor3 = Color3.new(1, 1, 1),
			Font = Enum.Font.GothamBold,
			TextStrokeColor3 = Color3.new(0, 0, 0),
			TextStrokeTransparency = 0.8,
			TextScaled = true,
			
			-- Color
			BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
			
			-- Border
			BorderSizePixel = 0,
			
			-- Events
			[Roact.Event.Activated] = function()
				if self.props.open[1] == "SHOP" then
					self.props.setUI(1)
				else
					self.props.setUI("SHOP", 1)
				end
			end
		}, {
			UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
				MaxTextSize = 24
			}),
			
			UICorner = Roact.createElement("UICorner", {
				CornerRadius = UDim.new(0, 5)
			}),
		}),
		
		ShopFrame = Roact.createElement("Frame", {
			-- Size/Position
			Size = UDim2.fromScale(0.5, 0.75),
			Position = UDim2.fromScale(0.5, 0.5),
			AnchorPoint = Vector2.new(0.5, 0.5),
			
			-- Color
			BackgroundColor3 = Color3.new(1, 1, 1),
			BackgroundTransparency = 1,
			
			-- Border
			BorderSizePixel = 0,
			
			-- Visibility
			ClipsDescendants = true,
			Visible = (self.props.open[1] == "SHOP")
		}, {
			UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
				AspectRatio = 1.625
			}),
			
			TopbarFrame = Roact.createElement("Frame", {
				BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
				Size = UDim2.fromScale(1, 0.1),
				BorderSizePixel = 0,
				ZIndex = 4
			}, {
				CloseButton = Roact.createElement("ImageButton", {
					Size = UDim2.fromScale(1, 1),
					Image = "rbxassetid://2419293754",
					BackgroundColor3 = Color3.new(1, 1, 1),
					BorderSizePixel = 0,
					ZIndex = 5,
					[Roact.Event.Activated] = function()
						self.props.setUI(1)
					end
				}, {
					UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint"),
					UICorner = Roact.createElement("UICorner", {
						CornerRadius = UDim.new(0, 5)
					})
				}),
				
				UIPadding = Roact.createElement("UIPadding", {
					PaddingBottom = UDim.new(0, 5),
					PaddingLeft = UDim.new(0, 5),
					PaddingRight = UDim.new(0, 5),
					PaddingTop = UDim.new(0, 5)
				}),
				
				TextLabel = Roact.createElement("TextLabel", {
					AnchorPoint = Vector2.new(0.5, 0),
					Position = UDim2.fromScale(0.5, 0),
					Size = UDim2.fromScale(0.75, 1),
					BackgroundTransparency = 1,
					FontSize = Enum.FontSize.Size32,
					Font = Enum.Font.GothamBold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextStrokeTransparency = 0.8,
					Text = "Shop",
					ZIndex = 5
				})
			}),
			
			ScrollingFrame = Roact.createElement("ScrollingFrame", {
				AnchorPoint = Vector2.new(0, 1),
				Size = UDim2.fromScale(0.75, 0.9),
				Position = UDim2.fromScale(0, 1),
				CanvasSize = UDim2.fromOffset(self.canvasSize.X, self.canvasSize.Y),
				BorderSizePixel = 0,
				BackgroundColor3 = Color3.new(1, 1, 1),
				ZIndex = 2,
				[Roact.Change.AbsoluteSize] = function(rbx)
					self.absoluteSize = UDim2.fromScale(100 / rbx.AbsoluteSize.X, 100 / rbx.AbsoluteSize.Y)
				end
			}, FrameData),
			
			Sidebar = Roact.createElement("Frame", {
				AnchorPoint = Vector2.new(1, 1),
				Size = UDim2.fromScale(0.25, 0.9),
				Position = UDim2.fromScale(1, 1),
				
				BorderSizePixel = 0,
				
				BackgroundColor3 = Color3.new(240/255, 240/255, 240/255),
				ZIndex = 3
			}, {				
				UIPadding = Roact.createElement("UIPadding", {
					PaddingBottom = UDim.new(0, 5),
					PaddingLeft = UDim.new(0, 5),
					PaddingRight = UDim.new(0, 5),
					PaddingTop = UDim.new(0, 5)
				}),
				
				UIListLayout = Roact.createElement("UIListLayout", {
					Padding = UDim.new(0, 5),
					SortOrder = Enum.SortOrder.LayoutOrder
				}),
				
				Icon = Roact.createElement("ImageLabel", {
					Image = ((i > 0 and viewingShopItem and shopItems[viewingShopItem]) and shopItems[viewingShopItem].Icon or ""),
					Size = UDim2.fromScale(1, 1),
					BorderSizePixel = 0,
					BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
					LayoutOrder = 1,
					ZIndex = 4,
				}, {
					UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint")
				}),
				
				NameLabel = Roact.createElement("TextLabel", {
					Text = ((i > 0 and viewingShopItem and shopItems[viewingShopItem]) and shopItems[viewingShopItem].Name or ""),
					Size = UDim2.fromScale(1, 0.1),
					BorderSizePixel = 0,
					BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
					
					TextScaled = true,
					
					FontSize = Enum.FontSize.Size32,
					Font = Enum.Font.GothamBold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextStrokeTransparency = 0.8,
					
					LayoutOrder = 2,
					
					ZIndex = 4
				}, {
					UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
						MaxTextSize = 24
					}),	
				}),
				
				DescriptionLabel = Roact.createElement("TextLabel", {
					Text = ((i > 0 and viewingShopItem and shopItems[viewingShopItem]) and shopItems[viewingShopItem].Description or ""),
					Size = UDim2.fromScale(1, 0.2),
					BorderSizePixel = 0,
					BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
					
					TextScaled = true,
					
					FontSize = Enum.FontSize.Size32,
					Font = Enum.Font.GothamBold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextStrokeTransparency = 0.8,
					
					LayoutOrder = 3,
					
					ZIndex = 4
				}, {
					UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
						MaxTextSize = 24
					}),	
				}),
				
				PriceLabel = Roact.createElement("TextLabel", {
					Text = ((i > 0 and viewingShopItem) and shopItems[viewingShopItem].Price .. " " .. (shopItems[viewingShopItem].Currency or "Coins") or ""),
					Size = UDim2.fromScale(1, 0.1),
					BorderSizePixel = 0,
					BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
					
					TextScaled = true,
					
					FontSize = Enum.FontSize.Size32,
					Font = Enum.Font.GothamBold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextStrokeTransparency = 0.8,
					
					LayoutOrder = 4,
					
					ZIndex = 4
				}, {
					UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
						MaxTextSize = 24
					}),
				}),
				
				BuyButton = Roact.createElement("TextButton", {
					Text = ((i > 0 and viewingShopItem) and (inventory[viewingShopItem] and "OWNED" or "BUY") or "NO ITEM SELECTED"),
					Size = UDim2.fromScale(1, 0.1),
					BorderSizePixel = 0,
					BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
					
					TextScaled = true,
					
					FontSize = Enum.FontSize.Size32,
					Font = Enum.Font.GothamBold,
					TextColor3 = Color3.new(1, 1, 1),
					TextStrokeColor3 = Color3.new(0, 0, 0),
					TextStrokeTransparency = 0.8,
					
					[Roact.Event.Activated] = function()
						BUY_ITEM_EVENT:FireServer(viewingShopItem, ITEM_TYPE)
					end,
					
					LayoutOrder = 5,
					
					ZIndex = 4
				}, {
					UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
						MaxTextSize = 24
					}),
				}),
			})
		})
	})
end

return Shop

Spectate

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

-- Modules
local Roact = require("Roact")

-- Variable
local spectateFolder = game:GetService("ReplicatedStorage"):WaitForChild("Spectate")

-- Module
local Spectate = Roact.Component:extend("Spectate")

local function getSpectate()
	local spectate = {}
	
	for _, value in ipairs(spectateFolder:GetChildren()) do
		table.insert(spectate, value.Value)
	end
	
	return spectate
end

function Spectate:init()
	self.props.setSpectate(getSpectate())
	
	spectateFolder.ChildAdded:Connect(function()
		self.props.setSpectate(getSpectate())
	end)
	
	spectateFolder.ChildRemoved:Connect(function()
		self.props.setSpectate(getSpectate())
	end)
end

function Spectate:render()	
	local player = game.Players.LocalPlayer
	if player then
		local character = player.Character
		if character then
			local humanoid = character:FindFirstChildWhichIsA("Humanoid")
			if humanoid then
				workspace.CurrentCamera.CameraSubject = humanoid
			end
		end
	end
	
	if self.props.open[3] == "SPECTATE" then
		local player, chosenPlayer = game.Players.LocalPlayer, (self.props.spectate[self.props.index] and self.props.spectate[self.props.index] or nil)
		
		if chosenPlayer then
			local character, chosenCharacter = player.Character, chosenPlayer.Character
			if chosenCharacter then
				local humanoid, chosenHumanoid = character:FindFirstChildWhichIsA("Humanoid"), chosenCharacter:FindFirstChildWhichIsA("Humanoid")
				if chosenHumanoid then
					workspace.CurrentCamera.CameraSubject = chosenHumanoid
				end
			end
		end
	end
	
	if #self.props.spectate <= 0 then
		if self.props.open[3] == "SPECTATE" then
			self.props.setUI(3)
		end
	end
	
	if table.find(self.props.spectate, game.Players.LocalPlayer) then
		if self.props.open[3] == "SPECTATE" then
			self.props.setUI(3)
		end
	end
	
	return Roact.createFragment({
		SpectateButton = Roact.createElement("TextButton", {
			-- Size/Position
			Size = UDim2.fromScale(0.15, 0.075),
			Position = UDim2.fromScale(0, 0.6), -- 0.15 y distance between side buttons
			AnchorPoint = Vector2.new(0, 0.5),
			
			-- Text
			Text = "Spectate",
			TextColor3 = Color3.new(1, 1, 1),
			Font = Enum.Font.GothamBold,
			TextStrokeColor3 = Color3.new(0, 0, 0),
			TextStrokeTransparency = 0.8,
			TextScaled = true,
			
			-- Color
			BackgroundColor3 = Color3.new(200/255, 200/255, 200/255),
			
			-- Border
			BorderSizePixel = 0,
			
			-- Events
			[Roact.Event.Activated] = function()
				if self.props.open[3] == "SPECTATE" then
					self.props.setUI(3)
				else
					if #self.props.spectate > 0 then
						if not table.find(self.props.spectate, game.Players.LocalPlayer) then
							self.props.setUI("SPECTATE", 3)
						end
					end
				end
			end
		}, {
			UITextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
				MaxTextSize = 24
			}),
			
			UICorner = Roact.createElement("UICorner", {
				CornerRadius = UDim.new(0, 5)
			}),
		}),
		
		SpectateFrame = Roact.createElement("Frame", {
			-- Size/Position
			Size = UDim2.fromScale(1, 0.2),
			Position = UDim2.fromScale(0, 1),
			AnchorPoint = Vector2.new(0, 1),
			
			-- Color
			BackgroundColor3 = Color3.new(0, 0, 0),
			BackgroundTransparency = 0.5,
			
			-- Border
			BorderSizePixel = 0,
			
			-- Visibility
			ClipsDescendants = true,
			Visible = (self.props.open[3] == "SPECTATE" and #self.props.spectate > 0 and not table.find(self.props.spectate, game.Players.LocalPlayer))
		}, {
			SpectateLabel = Roact.createElement("TextLabel", {
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.5, 0.5),
				Size = UDim2.fromScale(0.35, 0.75),
				BackgroundTransparency = 1,
				FontSize = Enum.FontSize.Size32,
				Font = Enum.Font.GothamBold,
				TextColor3 = Color3.new(1, 1, 1),
				TextStrokeColor3 = Color3.new(0, 0, 0),
				TextStrokeTransparency = 0.8,
				Text = (#self.props.spectate > 0 and self.props.spectate[self.props.index].Name or "")
			}),
			
			BackButton = Roact.createElement("TextButton", {
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.25, 0.5),
				Size = UDim2.fromScale(0.1, 0.75),
				BackgroundTransparency = 0,
				FontSize = Enum.FontSize.Size32,
				Font = Enum.Font.GothamBold,
				TextColor3 = Color3.new(1, 1, 1),
				TextStrokeColor3 = Color3.new(0, 0, 0),
				TextStrokeTransparency = 0.8,
				Text = "<<",
				
				[Roact.Event.Activated] = function()
					self.props.setIndex((self.props.index <= 1 and #self.props.spectate or self.props.index - 1))
				end
			}),
			
			FowardButton = Roact.createElement("TextButton", {
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.75, 0.5),
				Size = UDim2.fromScale(0.1, 0.75),
				BackgroundTransparency = 0,
				FontSize = Enum.FontSize.Size32,
				Font = Enum.Font.GothamBold,
				TextColor3 = Color3.new(1, 1, 1),
				TextStrokeColor3 = Color3.new(0, 0, 0),
				TextStrokeTransparency = 0.8,
				Text = ">>",
				
				[Roact.Event.Activated] = function()
					self.props.setIndex((self.props.index >= #self.props.spectate and 1 or self.props.index + 1))
				end
			})
		})
	})
end

return Spectate

Status


local Roact = require("Roact")
local GetRemoteEvent = require("GetRemoteEvent")

local VOTE_EVENT 

local Status = Roact.Component:extend("Status")

local function timeToTimer(time)
	if time then
		if time >= 0 then
			local minutes = math.floor(time / 60)
			local seconds = time % 60
			
			if seconds <= 9 and seconds > 0 then
				seconds = "0" .. seconds
			elseif seconds == 0 then
				seconds = "00"	
			end
			
			return "(" .. minutes .. ":" .. seconds .. ")"
		end
	end
	
	return ""
end

function Status:init()
	
end

function Status:render()
	local status, time = self.props.status, timeToTimer(self.props.time)
	self.text = status .. " " .. time
	
	return Roact.createElement("TextLabel", {
		AnchorPoint = self.props.anchorPoint,
		Position = self.props.position,
		Size = self.props.size,
		BackgroundTransparency = 1,
		FontSize = Enum.FontSize.Size32,
		Font = Enum.Font.GothamBold,
		TextColor3 = Color3.new(1, 1, 1),
		TextStrokeColor3 = Color3.new(0, 0, 0),
		TextStrokeTransparency = 0.8,
		Text = self.text
	})
end

return Status

Voting

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

local Roact = require("Roact")
local GetRemoteEvent = require("GetRemoteEvent")

local VOTE_REMOTE = GetRemoteEvent("VoteForGame")

local Voting = Roact.Component:extend("Voting")

function Voting:init()
	
end

function Voting:render()
	local elements = {}
	
	elements.UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
		AspectRatio = 3,
	})
	
	elements.UIPadding = Roact.createElement("UIPadding", {
		PaddingBottom = UDim.new(0, 5),
		PaddingLeft = UDim.new(0, 5),
		PaddingRight = UDim.new(0, 5),
		PaddingTop = UDim.new(0, 5),
	})
	
	elements.UIListLayout = Roact.createElement("UIListLayout", {
		FillDirection = Enum.FillDirection.Horizontal,
		SortOrder = Enum.SortOrder.LayoutOrder,
		Padding = UDim.new(0, 10),
	})
	
	for i, data in ipairs(self.props.vote or {}) do
		elements[data.MapName .. data.GamemodeName] = Roact.createElement("ImageButton", {
			Name = "Vote",
			BackgroundColor3 = Color3.fromRGB(240, 240, 240),
			BorderSizePixel = 0,
			LayoutOrder = i,
			Size = UDim2.new(1, 0, 1, 0),
			ZIndex = 2,
			Image = data.Map.Icon,
			
			[Roact.Event.Activated] = function()
				VOTE_REMOTE:FireServer(i)
			end
		}, {
			UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint"),
			TextLabel = Roact.createElement("TextLabel", {
				BackgroundColor3 = Color3.fromRGB(255, 255, 255),
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 1, 0),
				Font = Enum.Font.GothamBold,
				TextColor3 = Color3.fromRGB(255, 255, 255),
				TextScaled = true,
				TextSize = 14,
				TextStrokeTransparency = 0.8,
				TextWrapped = true,
				Text = data.GamemodeName .. " on " .. data.MapName .. " (" .. #data.Votes .. " votes)",
				ZIndex = 3
			}, {
				Roact.createElement("UITextSizeConstraint", {
					MaxTextSize = 16,
				})
			})
		})
	end
	
	return Roact.createElement("Frame", {
		AnchorPoint = Vector2.new(0.5, 1),
		BackgroundColor3 = Color3.fromRGB(200, 200, 200),
		BorderSizePixel = 0,
		Position = UDim2.new(0.5, 0, 1, 0),
		Size = UDim2.new(0.2, 0, 0.2, 0),
		Visible = (self.props.vote and #self.props.vote > 0 and self.props.open[2] == "VOTING")
	}, elements)
end

return Voting

Store

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

local Rodux = require("Rodux")
local defaultState = {
	status = "",
	time = -1,
	shopItems = {},
	data = '',
	ui = {},
	viewingShopItem = nil,
	index = 1,
	spectate = {},
	vote = {}
}

function reducer(state, action)
	local state = state or defaultState
	
	-- Status
	if action.type == "SET_STATUS" and action.text then
		state.status = action.text
		return state
	end
	
	-- Time
	if action.type == "ROUND_START" and action.time then
		state.time = action.time
		return state
	end
	
	if action.type == "DECREMENT" and state.time >= 0 then
		state.time = state.time - 1
		return state
	end
	
	-- Shop
	if action.type == "SET_SHOP_ITEMS" and action.items and action.key then
		local items = state.shopItem
		items[action.key] = action.items
		state.shopItems = items
		return state
	end
	
	if action.type == "SET_DATA" and action.data then
		state.data = action.data
		return state
	end
	
	-- Data
	if action.type == "OPEN_UI" then
		local open = state.ui
		open[action.key] = action.ui or ""
		state.ui = open
		return state
	end
	
	if action.type == "CHANGE_SHOP_ITEM" and action.item then
		state.viewingShopItem = action.item
		return state
	end
	
	-- Spectate
	if action.type == "SET_INDEX" and action.index then
		state.index = action.index
		return state
	end
	
	if action.type == "SET_SPECTATE" and action.spectate then
		state.spectate = action.spectate
		return state
	end
	
	-- Voting
	if action.type == "SET_VOTE" then
		state.vote = action.vote
		return state
	end
	
	return state
end

return Rodux.Store.new(reducer)

This is what happens:

1 Like

Based on what I’ve debugged, I feel like it’s only updating the tree whenever a reducer is called from the App component.

Based on that, I can conclude there’s something wrong, but I can’t figure out how to fix it. As seen in the video in the topic, it updates the tree whenever I hit the AFK Button.

However, the previous state changes also go with the afk button. Say for example, I pressed the “Shop” button and then I press the AFK Button: The tree gets rendered. So, with that in mind; here’s why that happens.

There’s a function in the app component:

DATA_CHANGED_EVENT.OnClientEvent:Connect(function(data)
		self.props.setData(data)
		
		if self.props.data.Coins then
			if self.props.data.Coins < data.Coins then
				script.Cash:Play()
			end
		else
			script.Cash:Play()
		end
	end)

Which updates when the data is changed (DataStore2), which then updates the player’s data, and causes the whole app to render. But when I change the state from any other component, the state doesn’t update.

My question is: How do I fix this, and avoid any hacky workarounds. I feel like I’m doing something wrong, but I can’t figure it out.

I don’t know if this is right but, (this is from the shop script)

				CloseButton = Roact.createElement("ImageButton", {
					Size = UDim2.fromScale(1, 1),
					Image = "rbxassetid://2419293754",
					BackgroundColor3 = Color3.new(1, 1, 1),
					BorderSizePixel = 0,
					ZIndex = 5,
					[Roact.Event.Activated] = function()
						self.props.setUI(1)
					end

Try putting a breakpoint inside the Roact.Event.Activated function to see if the function is fired

I know for a fact that function is called, all of the functions are called, refer to my reply. It’s much more nuanced than that.

Oops, sorry, I forgot to read the reply.

I took a brief look at your reducer structure, and it appears the issue may stem from not treating your state as immutable.

Instead of editing the existing state, you’ll need to copy the state as-is, make changes on your copy, then return the copy.
This is important because Roact uses table equality to determine when to re-render. This stack overflow answer explains the concept in more detail better than I could.

llama is a great open source library to help with immutability issues like this.
Since you are also using Rodux, you may as well use the createReducer helper functions to make your code a bit more read-able and less error prone as well.

If I were you, I’d start refactoring your reducer function to look a little something like this:


local llama = require(llama)

local reducer = Rodux.createReducer(defaultState, {
	SET_STATUS = function(state, action)
		return llama.Dictionary.join(state, {
			status = action.text
		})
	end,

	ROUND_START = function(state, action)
		return llama.Dictionary.join(state, {
			time = action.time
		})
	end,

	DECREMENT = function(state, _action)
		if state.time >= 0 then
			return llama.Dictionary.join(state, {
				time = state.time - 1
			})
		else
			-- Make no changes
			return state
		end
	end,

	-- etc
})
8 Likes

I appreciate it, this completely fixed my app.

1 Like