21 minute read
I've been using Roact, and now react-lua on Roblox for several years. In recent years, hooks, whether through react-lua exposing them natively, or through my own roact-hooks package, and Luau have changed everything, and Zombie Strike is no longer as great a repository to learn from (though it's far far better than nothing!).
Here's an uncategorized list of tricks I do to make using React a lot easier and safer. A lot of these tricks work in Roact with roact-hooks, though I make no guarantees.
e = React.createElement
This one is common, but let's start with it because you'll see it in all my samples. React.createElement
is extremely common, and extremely noisy. The Roblox community has decided on the following idiom:
local e = React.createElement
While I am definitely generally against shortening variable names, even something like ply
, e
definitely improves the ability to read any React component, as it helps you keep focus on the structure that actually matters. Furthermore, as this is the de facto standard in the Roblox community, other experienced developers should know what you mean.
If you already have an established codebase, start putting --!strict
at the top of all your new files. If you're starting a new one, create a .luaurc with { "languageMode": "strict" }
, which Luau LSP will read.
Strict mode is critical for avoiding bugs in Luau in general, but especially in React, as it will immediately tell you if you are using a component wrong. For example, let's take the following component.
local function HealthBar(props: {
health: number,
})
-- code
end
In strict mode, writing out something like:
e(HealthBar, {
helath = 1, -- Typo!
})
...will provide an error.
However, if you are going to use strict mode, there's some caveats you'll need to remember.
useState
is typed in React, but Luau is not great at guessing those types on its own. Trivial cases work fine. In this example, amount
is typed as number
, and setAmount
takes both number
and (number) -> number
.
local amount, setAmount = React.useState(0)
However, let's say that we want amount
to be number?
. If you wrote it like this:
local amount, setAmount = React.useState(nil)
...then amount
and setAmount
both are typed for nil
. This is expected, but both obvious ways of resolving this have their own caveats.
If we try typing amount
as number?
...
local amount: number?, setAmount = React.useState(nil)
...then while amount
is treated as number?
, setAmount
still only takes nil
. If you instead try...
local amount, setAmount = React.useState(nil :: number?)
...then amount
and setAmount
will be anonymous types, and not actually be usable.
The complete incantation to get amount
as number?
, and setAmount
as number? | (number?) -> number?
is...
local amount: number?, setAmount = React.useState(nil :: number?)
Sigh.
A similar trick is also necessary for unions:
type MenuState = "open" | "closed"
local menuState: MenuState, setMenuState = React.useState("open" :: MenuState)
I haven't been able to come up with a minimum repro of this for the Luau team, but disappointingly, intersections are unusable as a props type.
type Base = {
x: number,
}
type Value = Base & {
y: number,
}
local function Component(props: {
value: Value,
})
-- code
end
e(Component, {
value = {
x = 1,
y = 2,
}
})
In this case, value
will incorrectly say that it is not valid in this context.
TypeError: Type '{ value: { x: number, y: number } }' could not be converted into 'a?'
caused by:
None of the union options are compatible. For example:
Type
'{ value: { x: number, y: number } }'
could not be converted into
'{| value: Base & {| y: number |} |}'
caused by:
Property 'value' is not compatible.
Type
'{ x: number, y: number }'
could not be converted into
'Base & {| y: number |}'
caused by:
Not all intersection parts are compatible.
Table type '{ x: number, y: number }' not compatible with type 'Base' because the former has extra field 'y'
In cases like this, the best we can do to keep ourselves safe is something like:
-- This will error if we don't match the type
local value: Value = {
x = 1,
y = 2,
}
e(Component, {
value = value,
})
This one is the most dangerous: unused properties will not error.
local function HealthBar(props: {
health: number,
})
-- code
end
e(HealthBar, {
health = 100,
maxHealth = 100,
})
This code will not error, as health = 1
is enough to make this match props
, but the maxHealth
will be completely unused.
React.ReactNode
Strict mode requires that a function have a consistent return value. For example, this will fail in strict mode:
local function ContextualHealthBar(props: {
health: number,
maxHealth: number,
})
if props.health == props.maxHealth then
return nil
end
-- ERROR: We only expect to return nil
return e(HealthBar, {
health = health,
maxHealth = maxHealth,
})
end
The above case of optional returns comes up all the time. Thus, it is helpful to know that createElement
returns a React.ReactNode
. With that in mind, we can add this to the component:
local function ContextualHealthBar(props: {
health: number,
maxHealth: number,
}): React.ReactNode?
...and our code will now validate.
Unlike HTML, where elements have a defined order, in Roblox we must specify a LayoutOrder
for elements governed by automatic layouts, such as UIListLayout
. The obvious way to do this is to write code that looks like this:
local function TitleButtons()
return e(Frame, {}, {
UIListLayout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Minimize = e("TextButton", {
LayoutOrder = 1,
-- etc
}),
Maximize = e("TextButton", {
LayoutOrder = 2,
-- etc
}),
Close = e("TextButton", {
LayoutOrder = 3,
-- etc
}),
})
end
However, this quickly becomes a nightmare as your components get more and more complex, and with different elements being contextual. Updating this code to add new elements means that you must go and update every other value.
Instead, use a function that looks like this:
local function createNextOrder(): () -> number
local layoutOrder = 0
return function()
layoutOrder += 1
return layoutOrder
end
end
return createNextOrder
This function will return another function (a pattern known as "higher order functions") that will return an incrementing number. With this in our toolbelt, we can now update our original code to look like this:
local function TitleButtons()
local nextOrder = createNextOrder()
return e("Frame", {
-- etc
}, {
UIListLayout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Minimize = e("TextButton", {
LayoutOrder = nextOrder(),
-- etc
}),
Maximize = e("TextButton", {
LayoutOrder = nextOrder(),
-- etc
}),
Close = e("TextButton", {
LayoutOrder = nextOrder(),
-- etc
}),
})
end
Now we can update our component definition however we want and not have to care about layout order being correct.
and
to conditionally show componentsIf you want to only show a component when a condition is met, the cleanest way to do that is with and
.
local function Menu()
local storeOpen, setStoreOpen = React.useState(false)
return e("Frame", {
-- etc
}, {
Store = storeOpen and e(Store),
})
end
This works because of two tricks.
The first is that x and y
does not return a boolean all the time. x and y
is defined as returning x
if it's falsy (that is, it is nil
or false
), and y
otherwise.
We can observe this behavior with the following:
print(true and 1) -- 1
print(false and 1) -- false
The second is that false
is a valid React node, it just won't render. Thus, storeOpen and e(Store)
will read as "if the store is open, render a Store component, otherwise render false".
Luau also has the "if expression" syntax, where you can write if condition then resultIfTrue else resultIfFalse
, though I choose to only use this in cases where the and
trick doesn't work, or when I actually have a value to return in both cases.
useToggleState
It is very common to have menus that open up and buttons that close them, or differences in UI when hovering over something. Let's expand our last example:
local function Menu()
local storeOpen, setStoreOpen = React.useState(false)
local storeButtonHovered, setStoreButtonHovered = React.useState(false)
return e("Frame", {
-- etc
}, {
StorePage = storeOpen and e(Store, {
onClose = function()
setStoreOpen(false)
end,
}),
-- later
StoreButton = e("TextButton", {
-- etc
BackgroundColor3 = if storeButtonHovered then Colors.white else Colors.gray,
[React.Event.Activated] = function()
setStoreOpen(true)
end,
[React.Event.MouseEnter] = function()
setStoreButtonHovered(true)
end,
[React.Event.MouseLeave] = function()
setStoreButtonHovered(false)
end,
}),
})
end
This pattern comes up so often that I recommend this custom hook:
local function useToggleState(default: boolean): {
on: boolean,
enable: () -> (),
disable: () -> (),
toggle: () -> (),
}
local toggled, setToggled = React.useState(default)
local enable = React.useCallback(function()
setToggled(true)
end, {})
local disable = React.useCallback(function()
setToggled(false)
end, {})
local toggle = React.useCallback(function()
setToggled(function(currentToggled)
return not currentToggled
end)
end, {})
return {
on = toggled,
enable = enable,
disable = disable,
toggle = toggle,
}
end
return useToggleState
This simply packages all the useful stuff you'll need. Our previous example will now look like:
local function Menu()
local storeOpen = useToggleState(false)
local storeButtonHovered = useToggleState(false)
return e("Frame", {
-- etc
}, {
StorePage = storeOpen.on and e(Store, {
onClose = storeOpen.disable,
}),
-- later
StoreButton = e("TextButton", {
-- etc
BackgroundColor3 = if storeButtonHovered.on then Colors.white else Colors.gray,
[React.Event.Activated] = storeOpen.enable,
[React.Event.MouseEnter] = storeButtonHovered.enable,
[React.Event.MouseLeave] = storeButtonHovered.disable,
}),
})
end
This also has the benefit of avoiding creating more anonymous functions which can help performance, as the identity of anonymous functions changes every render, requiring React to disconnect and reconnect any event you connect it to.
createUniqueKey
Every child in a React component should have a key associated with it. This means that code like this:
return e("Frame", {}, {
e(Button, { --[[ etc ]] }),
e(Button, { --[[ etc ]] }),
e(Button, { --[[ etc ]] }),
})
...is wrong. This is not just because looking at the UI in the explorer will be a pain, but also because if this list of children changes at all, for example:
return e("Frame", {}, {
showMinimize and e(Button, { --[[ etc ]] }),
showMaximize and e(Button, { --[[ etc ]] }),
showClose and e(Button, { --[[ etc ]] }),
})
...that the keys these associated to will change. This can incur costs in completely unmounting and remounting the components, as React will treat them as completely separate objects. Furthermore, you will also lose any internal state these components had.
The above code should instead be written as:
return e("Frame", {}, {
Minimize = showMinimize and e(Button, { --[[ etc ]] }),
Maximize = showMaximize and e(Button, { --[[ etc ]] }),
Close = showClose and e(Button, { --[[ etc ]] }),
})
However, this rule can become tricky when it comes to completely dynamic objects. For example, if we were making a todo list component, we would not want to write it like this:
local function TodoList(props: {
entries: { string },
})
local children = {}
for _, entry in props.entries do
children[entry] = e(TodoEntry, {
text = entry,
})
end
return e("Frame", {}, children)
end
...as putting the same entry twice will only show one of them. We can solve this problem by using the index somehow, but we get the same problem as before where changing the list will waste a lot of computation on tearing down and setting back up the other components.
For this reason, I recommend the following higher order function:
-- If you ever want to use an indice in a React name, use this instead.
local function createUniqueKey(): (string) -> string
local names = {}
return function(name)
if names[name] == nil then
names[name] = 1
return name
else
-- Edge case in case of:
-- uniqueKey("foo") -- foo
-- uniqueKey("foo_2") -- foo_2
-- uniqueKey("foo") -- foo_2 (clash)
while true do
names[name] += 1
local finalName = `{name}_{names[name]}`
if names[finalName] == nil then
return finalName
end
end
end
end
end
This function has the following behavior:
local uniqueKey = createUniqueKey()
uniqueKey("Dog") -- Returns "Dog"
uniqueKey("Cat") -- Returns "Cat"
uniqueKey("Dog") -- Returns "Dog_2"
Thus our component would now look like:
local function TodoList(props: {
entries: { string },
})
local uniqueKey = createUniqueKey()
local children = {}
for _, entry in props.entries do
children[uniqueKey(entry)] = e(TodoEntry, {
text = entry,
})
end
return e("Frame", {}, children)
end
Note that the existing caveat still exists in the form of if we have multiple duplicated names, as changing the amount of those will cause keys to change. However, in my experience this is very rare, and we at least only incur the cost in those cold cases rather than in all list changes.
useClock
I have written a Twitter thread on this before, so I will keep it somewhat brief.
For animations, I use TweenService:GetValue()
, with the alpha being timeSpent / timeToAnimate
.
In order to know that timeSpent
value, I have a useClock
hook.
local function useClock(): React.Binding<number>
local clockBinding, setClockBinding = React.useBinding(0)
React.useEffect(function()
local stepConnection = RunService.PostSimulation:Connect(function(delta)
setClockBinding(clockBinding:getValue() + delta)
end)
return function()
stepConnection:Disconnect()
end
end, {})
return clockBinding
end
This returns a binding to the amount of time since the component was mounted.
As an example, here is code to animate a white screen that fades out:
local function Flash()
local clockBinding = useClock()
return e("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
Size = UDim2.fromScale(1, 1),
-- Note: In strict mode, it is often necessary to type the mapped parameter of a binding
BackgroundTransparency = clockBinding:map(function(clock: number)
return math.clamp(clock / FADE_IN_TIME, 0, 1)
end)
})
end
useEventConnection
And while we're on the topic of custom hooks, here's a very simple one for hooking onto an event and disconnecting from it when you're done.
local function useEventConnection<T...>(
event: RBXScriptSignal<T...>, -- Can also include | Signal.Signal<T...> if you're using a custom signal type
callback: (T...) -> (),
dependencies: { any }
)
local cachedCallback = React.useMemo(function()
return callback
end, dependencies)
React.useEffect(function()
local connection = event:Connect(cachedCallback)
return function()
connection:Disconnect()
end
end, { event, cachedCallback } :: { unknown })
end
return useEventConnection
To be used like:
useEventConnection(humanoid.Died, function()
print(`You died! You did {damage} damage before you did.`)
end, { damage })
DEV mode, activated through _G.__DEV__
, is useful for catching a lot of bugs that Luau cannot, such as calling state setters in your render function, as well as for providing more useful stack traces in general.
I recommend turning it on in Studio, as it carries a non-negligible performance cost. I do this by putting this at the top of my React wally package source:
_G.__DEV__ = game:GetService("RunService"):IsStudio()
In fact, I have the following PowerShell script that I use instead of wally install
to make sure it doesn't go away:
wally install
$reactContents = "_G.__DEV__ = game:GetService('RunService'):IsStudio()`n"
$reactContents = $reactContents + (Get-Content -Path .\Packages\React.lua -Encoding ASCII -Raw)
Set-Content -Path .\Packages\React.lua -Value $reactContents -Encoding ASCII
native
tableIt is very common to wrap basic Roblox instances in a component for the sake of easier styling or other utilities. I have a Pane
component, for instance. Let's create one that looks like this.
local function Pane(props: {
children: React.ReactNode,
})
return e("Frame", {
-- Some nice defaults
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.fromScale(1, 1),
}, props.children)
end
However, we of course want the ability to write in our own properties. Traditionally, people like to do this by extending the properties itself, such that the following code will work:
e(Pane, {
Position = UDim2.fromScale(0.5, 0.5),
})
This would have to be implemented to look like:
local function Pane(props: {
children: React.ReactNode,
[any]: any,
})
local native = table.clone(props)
-- Remove any extra fields
native.children = nil
return e("Frame", join({
-- Some nice defaults
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.fromScale(1, 1),
}, props), props.children)
end
However, I really dislike this approach for a few reasons. One is that every time we add a new property, we must now keep that list of omitted properties up to date--for example, the Pane
component in My Movie has several utilities on top of it for automatically creating layouts, setting aspect ratios, etc. Second, it means that invalid properties will now definitely get through Luau. Third, it means that if Roblox ever adds a property named the same as yours, you now have problems as you try to force it into your component.
For these reasons, I choose to have a native
property instead.
local function Pane(props: {
native: { [any]: any }?,
children: React.ReactNode,
})
return e("Frame", join({
-- Some nice defaults
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.fromScale(1, 1),
}, props.native), props.children)
end
...which would then be used as:
return e(Pane, {
native = {
Position = UDim2.fromScale(0.5, 0.5),
}
})
Let's say I have the following component:
local function Leaderboard()
local entries, setEntries = React.useState({})
React.useEffect(function()
task.spawn(function()
setEntries(ReplicatedStorage.Remotes.GetLeaderboardEntries:InvokeServer())
end)
end, {})
local children = {}
for userId, score in entries do
children[`Player_{userId}`] = e(LeaderboardEntry, {
userId = UserId,
score = score,
})
end
return e("Frame", {
-- etc
}, children)
end
This component can be easily incorporated just through e(Leaderboard)
. However, if we try to create a Hoarcekat story using it, we face a problem where GetLeaderboardEntries
doesn't exist.
For this reason, I like to split my components into two:
local function Leaderboard(props: {
entries: { [number]: number },
})
local children = {}
for userId, score in entries do
children[`Player_{userId}`] = e(LeaderboardEntry, {
userId = userId,
score = score,
})
end
return e("Frame", {
-- etc
}, children)
end
local function LeaderboardConnected()
local entries, setEntries = React.useState({})
React.useEffect(function()
task.spawn(function()
setEntries(ReplicatedStorage.Remotes.GetLeaderboardEntries:InvokeServer())
end)
end, {})
return e(Leaderboard, {
entries = entries,
})
end
As you can see, we now have a Leaderboard
component, which simply accepts data and renders it, and a LeaderboardConnected
component, which simply retrieves that data.
After exporting both, now our Hoarcekat story can look something like:
e(Leaderboard, {
entries = {
[156] = 12345,
[261] = -1000,
}
})
I don't use Rodux anymore. It splits code up far too much, and now that React Context is a thing, I don't see any reason to depend on it anymore. For similar reasons, I no longer use Redux on the web.
React Context is an extremely powerful tool. I like to create contexts specific to the features they own. For example, for currency, I might create a context that looks like:
Context = React.createContext({
coins = 0,
})
...and then expand it later to include things like an easily accessible function for trying to purchase things that would open up a buy menu if you did not have enough coins.
From there, my context provider would do the heavy lifting of keeping this state up to date.
local function CoinsProvider(props: {
children: React.ReactNode,
})
local coins, setCoins = React.useState(0)
-- Just like before, I recommend potentailly making a "connected" provider that does this for easier mocking
useEventConnection(Remotes.UpdateCoins.OnClientEvent, setCoins, {})
return e(Context.Provider, {
value = {
coins = coins,
}
}, props.children)
end
This would then be used by other components in the form of:
local function CoinsCount()
local coinsContext = React.useContext(CoinsContext)
return e("TextLabel", {
-- etc
Text = coinsContext.coins,
})
end
Mocking these contexts is simple, as you can simply wrap your stories with a provider that provides some other value.
If you do this a lot, you might end up with something that looks like:
e(ThemesContext.Provider, {}, {
e(CoinsContext.Provider, {}, {
e(SoundContext.Provider, {}, {
-- Eventually, your code
})
})
})
The code at the end is going to end up being pretty hard to read! I have the following component to help with this:
local function ContextStack(props: {
providers: {
React.ComponentType<{
children: React.ReactNode,
}>
},
children: React.ReactNode,
})
local mostRecent = e(props.providers[#props.providers], {}, props.children)
for providerIndex = #props.providers - 1, 1, -1 do
mostRecent = e(props.providers[providerIndex], {}, mostRecent)
end
return mostRecent
end
The idea being that you can write the previous component to look like:
e(ContextStack, {
providers = {
ThemesContext.Provider,
CoinsContext.Provider,
SoundContext.Provider,
}
})
Luau and Context unfortunately do not mix very cleanly. To start with, the value
type in Context.Provider
does not appear to properly type check:
e(CoinsContext.Provider, {
value = {
-- could be anything!
}
})
Next, in order to make sure you get a useful return from useContext
, you need to make sure the context you are creating is properly typed.
I go overboard by regularly reminding Luau what types I want things to be. My context modules tend to look like this:
export type ContextType = {
coins: number,
}
-- By specifying ContextType like this, we guarantee that we will always fit
local default: ContextType = {
coins = 100,
}
local Context = React.createContext(default)
local function Provider(props: {
children: React.ReactNode,
})
local coins, setCoins = React.useState(0)
useEventConnection(Remotes.UpdateCoins.OnClientEvent, setCoins, {})
-- Again, do this so that we force Luau to show us errors when we get this wrong
local value: ContextType = {
coins = coins,
}
return e(Context.Provider, {
value = value,
}, props.children)
end
return {
Context = Context,
Provider = Provider,
}
createElement
has more than 3 parametersWe know that createElement looks like this:
createElement(
component, -- A string for natives, a component type otherwise
props,
children
)
However, this isn't exactly right. There actually is no children parameter at all! It actually looks like:
createElement(
component,
props,
children...
)
That's right, you can specify more than one children table, and React will merge them together!
This is very useful in the case of dynamic children such as our previous todo list. Let's look at it one more time.
local function TodoList(props: {
entries: { string },
})
local uniqueKey = createUniqueKey()
local entries = {}
for _, entry in props.entries do
entries[uniqueKey(entry)] = e(TodoEntry, {
text = entry,
})
end
return e("Frame", {}, entries)
end
It seems we forgot to add a UIListLayout to this! If we wanted to do that, we could shape it to look like:
entries.UIListLayout = e("UIListLayout")
...but especially as our component gets more complicated, it's disappointing to keep so much of our rendering code separate.
We could instead do something like:
e("Frame", {}, join(
{
UIListLayout = e("UIListLayout"),
},
entries
))
...where join
is a function for immutably merging dictionaries, but we don't need this at all, and can instead write the code to look like:
e("Frame", {}, {
UIListLayout = e("UIListLayout"),
}, entries)
...which I much prefer, as it keeps everything close, and will not need to be reshaped if we add dynamic content to a previously static component.
Web React developers will know the following pattern:
const [value, setValue] = useState("")
return <input
value={value}
onChange={(e) => setValue(e.value)}
/>
If you copy this same code into Roblox with TextBox
it will work. However, I have found that it can perform very badly on low end devices, with React changing the value much later than people are typing. You will also get weird issues if you try to use this to limit text length. Let's suppose we have the following code:
local value, setValue = React.useState("")
return e("TextBox", {
Text = value,
[React.Change.Text] = function(instance)
local text = instance.Text:sub(1, 3)
setValue(text)
end,
})
This seems like it should work to limit text lengths to 3 character, however you will actually find that it does nothing at all.
What's happening is that we are setting the text value to the same thing as it was before, which React (maybe ReactRoblox? I'm not sure) ignores, so while Roblox will update the text as ABCD
, internally we still see it as ABC
, so it never gets updated.
For these reasons, I have this decently large component:
local function TextBox(
props: {
initialText: string?,
onTextChange: (string) -> ()?,
maxLength: number?,
native: { [string]: any }?,
children: React.ReactNode?,
}
)
-- Ref instead of binding/state to allow Roblox's normal updating without re-renders,
-- which are noticably clunky on mobile.
local currentTextRef = React.useRef(props.initialText)
React.useEffect(function()
currentTextRef.current = props.initialText
end, { props.initialText })
local onTextChange = React.useCallback(function(textBox: TextBox)
local text = textBox.Text
if text == currentTextRef.current then
return
end
if props.maxLength ~= nil and #text > props.maxLength then
textBox.Text = text:sub(1, props.maxLength)
return
end
currentTextRef.current = text
if props.onTextChange ~= nil then
props.onTextChange(text)
end
end, { props.onTextChange, props.maxLength or 0 } :: { unknown })
return e(
"TextBox",
join({
Text = props.initialText or "",
[React.Change.Text] = onTextChange,
}, props.native),
props.children
)
end)
The idea of this component is that we rely on Roblox changing the text property, and we update our own internal state through onTextChange
, but we step in forcefully through refs when we need to change it ourselves.
Bindings are a carryover from Roact that represent a 1:1 property mapping. Originally added as a performance feature, people tend to overuse them wherever they can. This is a problem as bindings are unergonomic and limited: they cannot be used to conditionally render components, you can't react to them changing easily, etc.
As a rule of thumb, if you are not changing roughly every frame, you do not need a binding. For example, our useClock
component returns a binding, which makes sense, but a binding for when an object is hovered over would not.
Furthermore, you may be surprised to learn that the performance benefits of binding over state is much more negligible in react-lua than it is in legacy Roact. It's hard to get a good benchmark for this, as React has its own optimizations for delaying state updates, but I've been defaulting to no binding unless I notice performance issues for all of My Movie, and I haven't hit any case thus far where it was noticeable, which I cannot say for legacy Roact.
Do not write code that looks like this:
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
local tree = ReactRoblox.createTree(PlayerGui)
tree:render(e(App))
This will appear to work, however ReactRoblox will take complete ownership over PlayerGui. This means that if the tree unmounts, updates, whatever--it will likely clear anything in there that it does not own. This means that Roblox's mobile controls, which get placed into PlayerGui, will be wiped.
I've had this come up in other ways than just this. I have mounted ReactRoblox trees to player's heads, only to get errors when a TouchTransmitter object that Roblox put inside (by virtue of connecting .Touched
, even though I never do that in my own code...) gets destroyed as a result of the tree unmounting.
As a rule of thumb, once an object is in the data model, you should treat it as being forever taintable. This means that you should be using the following pattern instead:
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
-- Mount to a dummy object...
local tree = ReactRoblox.createTree(Instance.new("Folder"))
-- Then portal to the PlayerGui.
tree:render(ReactRoblox.createPortal(e(App), PlayerGui))