What is React?
React-lua is a Roblox-compatible Lua port of the well known React UI library that is widely used in web and mobile.
React-lua is maintained by Roblox. Currently most studio plugins and the Roblox Universal App (the desktop console and mobile app where you browse for your favorite Roblox games) are all written in react-lua.
In addition, we also maintain many libraries that extend or support react-lua. These libraries, including react-lua, are transpiled from from their JavaScript counterparts using js-to-lua.
Why should I use React?
Roblox already has plenty of awesome built-in tools for building UIs for your experiences. If youâre just starting out and your UI consists of just a few static buttons and dialogs, then sticking with just these tools is a great choice!! If youâre wanting to apply more structure to the ever growing complexity of your UI or just looking to learn something new, consider trying out React!
React is an industry standard for many web and mobile apps these days and modern React has never been easier to use. It will certainly be a top choice for any professional developer with prior web development experience.
Who is this guide for?
This guide is for experienced off-platform React devs looking to build UIs on Roblox and also for existing Roblox devs looking to up-level their UI/UX code. This guide assumes you are already familiar with building UI in Roblox.
Since react-lua is a port of React, all React resources apply. This guide highlights the key differences and is also a complete guide for those unfamiliar with React. For further reading, we suggest diving into the official React docs. There are also docs for react-lua.
Source Code
Source code for all examples are available on GitHub at react-lua-tutorial. Note that examples in the repository are written in Luau and are fully compatible with untyped Lua code. Type annotations have been omitted from the examples here for simplicity. I still highly recommended that you leverage typed Luau for all your Roblox scripts!
Getting Started
Installing the react-lua module in your Roblox project
If you are using Rojo and the Wally package management tool, react-lua is available on Wally. This is the recommended way to install React. Or, you can download an .rbxm file that can be imported into an existing project here. In either case, react-lua will be added to your ReplicatedStorage
folder.
Setting up React
Typical UI development in Roblox usually entails building a tree of GUIObjects
inside the StarterGui service. React-lua is an entirely code driven UI workflow so instead, we define a single entry point inside StarterPlayer > StarterPlayerScripts where we will declare React code that will ultimately construct the tree.
After following the installation steps above, you should be able to require the React ModuleScript in your LocalScripts to build your UI with React. Note that the package names may have different casing depending on which installation method you chose.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local React = require(ReplicatedStorage.Shared.Packages.react)
local ReactRoblox = require(ReplicatedStorage.Shared.Packages:FindFirstChild("react-roblox"))
MyTestFrame
First letâs define a very basic UI element to test that everything works:
local function MyTestFrame()
return React.createElement("Frame", {
Size = UDim2.new(0,100,0,100),
})
end
To actually render the UI element, we need to âmountâ the React âcomponent tree.â
local handle = Instance.new("ScreenGui",Players.LocalPlayer.PlayerGui)
local root = ReactRoblox.createRoot(handle)
root:render(React.createElement(MyTestFrame, {}, {}))
Hit Play to see that everything works as expected!
The render
function is ultimately what converts your React tree into something you can see on screen. The render
function call will be omitted in the rest of the exampes in this article.
Whatâs happening here?
React-lua internally stores a âtreeâ representing your UI.
React.createElement("Frame", {...}, {...})
defines a node in the tree.
- The first argument in this case is the
GUIObject
that the node represents. It could also be user-defined React component or function - The second argument is a list of properties that modify the appearance or behavior of the
GUIObject
- The final argument is a list of children nodes
Once the tree is defined, MyTestFrame
in the example above, we need to âmountâ so that it will ultimately render a Roblox UI for us. This means converting it into a GUIObject
tree in the datamodel. When calling root:render
with our component, react-lua will do the following things:
- Construct or update itâs internal representation of the UI tree.
- Derive the state of each component in the tree.
- Construct or update the matching Roblox
GUIObject
representation of the UI (rendering). You can see the tree in the explorer widget in the screenshot above.
This is identical to the component-lifecycle of React.
Styling
React properties are matched to GUIObject
styling properties by. For example, to create a cornflower blue 108x108 square in the center of the screen, we want to set the Size
, Position
, AnchorPoint
and BackgroundColor3
property.
The React styling property always matches the instance property name you are trying to set. Only styling related properties can be set this way. Please see the âAdvanced Stylingâ section below for more examples!
Component Trees
Of course you canât have a cute frame without rounded corners! The third argument in React.createElement
is for child elements. These will be children both in the React tree and the represented datamodel tree. Since Roblox uses child objects like UICorner
to style they parent GUIObjects
, we just need to instantiate a UICorner
child element to round out the corners of the parent Frame!
local function MyMostCuteTestFrame()
return React.createElement("Frame", {
Position = UDim2.new(0.5,0,0.5,0),
AnchorPoint = Vector2.new(0.5,0.5),
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(193,179,142)
}, {
React.createElement("UICorner", {
CornerRadius = UDim.new(0,30)
}),
})
end
Building component trees are a key concept of React so you should get comfortable with it!
Properties
In addition to built in properties like the styling properties above, you can also define custom properties for your custom React components.
local function MyCustomTextLabel(props)
return React.createElement("TextLabel", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(193,179,142),
TextColor3 = Color3.fromRGB(127,127,255),
Text = props.MyText,
})
end
Now you can instantiate your MyCustomTextLabel
with whatever text you want!
root:render(React.createElement(MyCustomTextLabel, { MyText = "I love Giraffes!" }, {}))
Reusing Components
Properties can be used for just about anything including styling properties, text to display, callbacks (see next example) and even React components!
local function MyCustomFrame(props)
return React.createElement("Frame", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(193,179,142),
}, { Child = props.child })
end
local function MyCustomFrameWithContents()
return React.createElement(MyCustomFrame, {
child = React.createElement("TextLabel", {
Text = "Hello from inside the frame!",
BackgroundTransparency = 1,
AutomaticSize = Enum.AutomaticSize.XY,
})
})
end
Note that weâre using our own MyCustomFrame
component inside the MyCustomFrameWithContents
component! The above is a very basic example of how you might build your own reuseable components.
Interacting
React can listen to events from the Roblox objects it represents. For example, to do something when a button is clicked, we can listen to the Activated
event.
local function MyBasicButton()
return React.createElement("TextButton", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(0,255,0),
Text = "Click me!",
[React.Event.Activated] = function()
print("You clicked me!")
end
})
end
Property change events are handled separately with the React.Change
key. For example, to listen to changes in the CanvasPosition
property of a ScrollingFrame
:
local function ColorfulScrollingFrame()
return React.createElement("ScrollingFrame", {
Size = UDim2.new(0,200,0,200),
BackgroundColor3 = Color3.fromRGB(255,255,255),
[React.Change.CanvasPosition] = function(instance: ScrollingFrame)
print("you scrolled me to " .. tostring(instance.CanvasPosition))
end
}, {
React.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
}),
React.createElement("Frame", {
Size = UDim2.new(0,200,0,150),
BackgroundColor3 = Color3.fromRGB(255,0,0),
}),
React.createElement("Frame", {
Size = UDim2.new(0,200,0,150),
BackgroundColor3 = Color3.fromRGB(0,255,0),
}),
React.createElement("Frame", {
Size = UDim2.new(0,200,0,150),
BackgroundColor3 = Color3.fromRGB(0,0,255),
}),
})
end
A common pattern is to pass in a callback as a property to the React component so that information can be passed up the hierarchy.
local function MyClickableButton(props)
return React.createElement("TextButton", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(0,0,255),
Text = props.Text,
[React.Event.Activated] = function()
props.OnClick(tostring(os.date("%x %X")))
end
})
end
...
root:render(React.createElement(Tutorial.MyClickableButton, {
OnClick = function(time: string)
print("Clickaroo! It is " .. time)
end
}))
Connecting with your Game
You can pass data into your React UI using properties when you call root:render
. We saw a simple example of this earlier with MyCustomTextLabel
and for a game it might look something like:
root:render(React.createElement(SnackTime, {
NumPlayers = #game:GetService("Players"):GetPlayers(),
PetsEnabled = true,
GameMode = "Fruits & Vegetables"
}, {}))
If you need to update the properties, you can just call root:render
again. To pull UI interactions back to your game, you will use events as outlined in the previous section. You can also pass in a callback as a property here which allows you to separate your UI code and your âside-effectingâ game logic. We saw this in the MyClickableButton
example.
Using properties is just one option. Another option is fetching the data directly inside of a useEffect
call or connecting to signals. We will learn more about this in the âEffectsâ section.
Inspecting and Debugging your React GUI
React ultimately renders a GUIObject
tree in the Roblox DataModel. You can inspect this tree by finding it in the Explorer window under Players > [Local Player Name] > PlayerGui. In the MyCuteTestFrame
example, we can see react-lua has created a Frame
instance for us and carried over all our desired styling properties. Frame
is the closest Roblox equivalent to a div
element in web BTW :D.
You can also update this tree in the properties widget. However changes here are only for testing and will likely be overridden the next time react-lua re-renders its component tree.
Spicing things Up
State useState
To build dynamic UIs, you will need components with states that change from user interaction. This can be accomplished with the useState
hook:
local function MyColorfulClickableSquare()
local color, setColor = React.useState(Color3.new(255,255,255))
return React.createElement("TextButton", {
Size = UDim2.new(0,200,0,200),
BackgroundColor3 = color,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
Text = "Click me to change color!",
[React.Event.Activated] = function()
local newColor = Color3.fromRGB(math.random(0,255), math.random(0,255), math.random(0,255))
print("changing color to " .. tostring(newColor) .. "!")
setColor(newColor)
end
})
end
The useState
function takes an initial value and returns the state value and a setter. In this example, the BackgroundColor3
property of TextButton
is set to the color
state variable, which is initialized to white. The second variable, setColor
, is the function that we use to set change value of color
, which is exactly what we do inside the [React.Event.Activated] event.
React carefully manages its component state so you must set the state through the state setter function returned by useState
. Just setting the color
variable directly in the above example wonât persist the state!
Note, if you capture the state value in a lambda (say), it will become stale when the setter gets called. The state value is just a regular lua variable and will not automatically update after its been captured. To fix this, you will need to capture it within hooks like useMemo
or useCallback
and add the state variable to the list of dependencies. See the âUnderstanding Dependenciesâ section below.
Ok, letâs take it a step further and compose with MyClickableButton
that we defined earlier.
local function MyCounter()
local count, setCount = React.useState(0)
return React.createElement("Frame", {
Size = UDim2.new(0,100,0,200),
}, {
Layout = React.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
}),
Label = React.createElement("TextLabel", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(0,255,0),
LayoutOrder = 1,
Text = "I've been clicked " .. tostring(count) .. " times"
}),
Button = React.createElement(MyClickableButton, {
Size = UDim2.new(0,100,0,100),
Text = "Click me to increment!",
LayoutOrder = 2,
OnClick = function()
setCount(count + 1)
end
})
})
end
Effects useEffect
Ultimately, your react-lua UI code will need to connect to the world outside of the React component tree. The useEffect
hook allows you to run effectful code in a controlled way.
local function MyClock()
local text, setText = React.useState("")
React.useEffect(function()
print("connecting to Heartbeat")
local timer = game:GetService("RunService").Heartbeat:Connect(function()
setText(os.date("%x %X"))
end)
return function()
print("disconnecting")
timer:Disconnect()
end
end, {})
return React.createElement("TextLabel", {
Size = UDim2.new(0,200,0,200),
Text = text,
})
end
The function pass into useEffect
will get run once (and only once) when the node first gets created in the react tree. Here we connect to the Heartbeat
event of RunService
inside the useEffect
hook to update text
to the current time each frame. When we destroy this component (for example, if itâs part of a UI dialog we just closed) we no longer want to be connected to this event. The method we pass into the useEffect
hook allows you to optionally return a cleanup routine. So we return a function that calls the Disconnect
method of the RBXScriptConnection
object that is returned by the Connect
method.
Another common event you might want to listen to is the InputEnded
event of UserInputService
which will allow you to capture keyboard input.
React.useEffect(function()
print("connecting to keyboard signal")
local UserInputService = game:GetService("UserInputService")
local signal: RBXScriptConnection = UserInputService.InputEnded:Connect(function(input: InputObject, gameProcessedEvent: boolean)
if input.UserInputType == Enum.UserInputType.Keyboard then
setText("last key pressed: " .. UserInputService:GetStringForKeyCode(input.KeyCode))
end
end)
return function()
print("disconnecting from keyboard signal")
signal:Disconnect()
end
end, {}) -- empty dependency array means this effect only runs once
Now you might ask, why do I need to wrap code around useEffect
? React is a âdeclarativeâ UI model but its implementation is âimperativeâ. This means your code, which declares how your UI looks/behaves, may be executed multiple times and these executions may produce unpredictable âside-effectsâ. The useEffect
hook precisely allows us to execute âeffectfulâ code predictably within the declarative model.
local function UnderstandingUseEffectExample()
local flavor, setFlavor = React.useState("vanilla")
print("I might get run a lot!")
React.useEffect(function()
print("I only run once!")
setFlavor("guava")
end, {}) -- don't forget the {}
return React.createElement("TextLabel", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(255,216,1),
Text = flavor,
})
end
If you run just this example, youâll see âI might get run a lot!â print twice. In more complex react trees, this may get run many many times. Everytime a property or state changes (for example setFlavor
gets called), React will rerun the code for that component and all of its descendants. This is called a rerender. Each call to React.createElement
declares a node in the component tree. On re-renders, any node in the tree that shares the same location and type from the last time the tree was rendered will be considered the âsameâ node in the tree allowing state to be persisted. In this case, useEffect
only runs the first time the node gets created in the tree.
Of course, sometimes, your effectful code might be dependent on other parts of your component (for example, fetching a URL thatâs passed in as a property). In the above example, we pass in {}
as the second argument to useEffect
indicating there are no dependencies to the effect thus ensuring the effect only gets run once. DO NOT FORGET TO ADD THE {}
. The next 2 sections will dive deeper into dependencies.
Memoization useMemo
Whereas effects are intended for controlled interactions with the world outside of React, sometimes, weâd also like to control code execution for optimization reasons. Remember that your functional component code may get run (rendered) many more times than the number of instances of that component in your tree. We can use the useMemo
hook to cache expensive computations so that they run only once.
local function MyEggCounter(props)
local numberEggs = React.useMemo(function()
print("Counting eggs! I'm only going to do this once!")
local numberEggs = 0
for i = 1, props.numberEggsToCount do
numberEggs = numberEggs + 1
end
return numberEggs
end, {props.numberEggsToCount})
return React.createElement("TextLabel", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(0,255,0),
Text = "I counted " .. numberEggs .. " eggs!"
})
end
...
root:render(React.createElement(Tutorial.MyEggCounter, {
numberEggsToCount = 9511245
}))
Superficially, useMemo
will take a function, run it, and return the value that the function produces. The next time your component node is rendered, it will use the previously computed value! However, the computed numberEggs
value is dependent on props.numberEggsToCount
. We pass this argument inside an array to the second argument of useMemo
which indicates it is a dependency. Now, each time props.numberEggsToCount
changes (and only when it changes), we will recompute numberEggs
.
Understanding Dependencies
While in most cases, you can and should just stick any values captured inside the function passed into useMemo
, itâs still important to understand whatâs going on here. When a node is rendered, React will store each of the dependencies in the dependency array. On each subsequent render, it will compare the values on the current render and the previous render. If these values are different, then the dependencies have âchangedâ and the computation or effect will be run again.
Lua has both value types and reference types. In particular, tables are reference types and modifying a table will not change the reference to the table. Thus the following does not work.
local function MyBoopMachine()
local boops, setBoops = React.useState({
nose = false,
ears = false,
tail = false,
})
local whatToBoop = React.useMemo(function()
if not boops.nose then
return "nose"
elseif not boops.ears then
return "ears"
elseif not boops.tail then
return "tail"
else
return "all booped!"
end
end, {boops})
return React.createElement("TextButton", {
Size = UDim2.new(0,200,0,200),
BackgroundColor3 = Color3.fromRGB(255,255,0),
Text = "boop " .. whatToBoop,
[React.Event.Activated] = function()
boops[whatToBoop] = true -- this will not update the `boops` table reference, so `whatToBoop` will not be updated
setBoops(boops)
end
})
end
To fix this issue, we need to rebuild the table
[React.Event.Activated] = function()
local boopCopy = {
nose = boops.nose,
ears = boops.ears,
tail = boops.tail,
}
boopCopy[whatToBoop] = true
setBoops(boopCopy)
end
Table operations like this are very common and often cumbersome to do manually. Instead, you can use the join
function avaliable in many lua libraies including Cryo and Dash (which are also available on Wally).
local boopCopy = join(boops, { [whatToBoop] = true })
If no dependency argument is passed in (i.e. itâs nil
), then the function will be run every time the component code is run. This is almost never what you want to do.
local function MyInfiniteLoop()
local myState, setMyState = React.useState({ content = "every new react developer be like" })
React.useEffect(function()
print("I'm stuck in an infinite loop!")
setMyState({ content = "This is fine" })
end) -- no explicit {} causes the effect to run every time the component code is executed!
return React.createElement("TextLabel", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(0,255,0),
Text = myState.content
})
end
In the above example, the setText
method prompts a state change which prompts react to run the component function again. Since useEffect
has nil
dependency, it will run the effect again, calling the setText
function again, sending your program into an infinite loop.
Note that react can not tell that the state has not actually changed due to tables being compared by reference. Since the state here is a table, itâs reference changes each time call { content = "This is fine" }
. Lua primitives like numbers and strings are compared by value so using a string directly as the state would not trigger the state change the second time setMyState({ content = "This is fine" })
is called (but you still shouldnât do it).
References useRef
The useRef
hook allows you to create âreferencesâ that can be assigned to anything you like in a React-compatible way. A common use case is to interact with child components. Functional components do not have class methods like the older style of class components. Instead, we can use the useImperativeHandle
hook to bind a method to the functional componentâs state modifiers.
local TextBoxWithSetter = function(props)
local text, setText = React.useState("")
-- binds `setText` to a function to the ref that can be called from the parent component
React.useImperativeHandle(props.setTextRef, function()
return {
setText = setText
}
end, {})
return React.createElement("TextBox", {
Size = UDim2.new(0,200,0,200),
Text = text,
})
end
-- same as MyClock except uses TextBoxWithSetter
-- this is totally silly and you can see how it might be useful for more complex components
local function MySillyClock()
local ref = React.useRef()
React.useEffect(function()
local timer = game:GetService("RunService").Heartbeat:Connect(function()
ref.current.setText(os.date("%x %X"))
end)
return function()
timer:Disconnect()
end
end, {})
return React.createElement(TextBoxWithSetter, {
setTextRef = ref
})
end
This is a powerful tool and breaks the unidirectional data flow of the reactive UI paradigm which will make it harder to reason about your code. Accessing the underlying GUIObject
that your React tree represents is especially discouraged but sometimes this is necessary to interact with its dynamic properties.
-- TW flashing colors
local function ColorJumper()
local ref = React.useRef()
React.useEffect(function()
local timer = game:GetService("RunService").Heartbeat:Connect(function()
-- ref.current is the ScrollingFrame, it will be nil on the first render because the ref hasn't been set yet
if ref.current then
ref.current.CanvasPosition = Vector2.new(0, math.random(0, 800))
end
end)
return function()
timer:Disconnect()
end
end, {})
return React.createElement("ScrollingFrame", {
Size = UDim2.new(0,200,0,200),
-- assign the ScrollingFrame instance to ref
ref = ref
}, {
React.createElement("Frame", {
Size = UDim2.new(1,0,0,200),
BackgroundColor3 = Color3.fromRGB(255,194,132),
}),
React.createElement("Frame", {
Size = UDim2.new(1,0,0,200),
Position = UDim2.new(0,0,0,200)
BackgroundColor3 = Color3.fromRGB(255,251,149),
}),
...
})
end
In the example above, we use the magical ref
reserved property . When React sees this property, it assigns the element being created itself to ref
, which in this case it is the ScrollingFrame
instance .
Some of you may be familiar with the forwardRef
method which allows to capture this magical ref
inside the code for the functional component being created (simply props.ref
will not work). Donât ever use forwardRef
. Instead, just pass in the ref as a property that is not named ref
like we did in the first example.
More Hooks
All React hooks are avaliable in react-lua. React-lua also has a few additional hooks which you can learn about in the react-lua docs.
A common pattern is to define your own custom hooks.
local function useToggleState(default: boolean): {
enabled: boolean,
enable: () -> (),
disable: () -> (),
}
local enabled, setEnabled = React.useState(default)
local enable = React.useCallback(function()
setEnabled(true)
end, {})
local disable = React.useCallback(function()
setEnabled(false)
end, {})
return {
enabled = enabled,
enable = enable,
disable = disable,
}
end
thanks Kampfkarren for the example above!
Advanced Styling
Styling primitives in react-lua + Roblox are different than the ones used by react-dom or react-native. In particular, Roblox has certain GUIObjects
that apply styling to their parents. Ones you will want to use frequently include:
- UIListLayout for applying layouts
- UICorner for rounding corners
- UIPadding for padding content
- UIStroke for outlining
Thus in react-lua, we create a child
local function MyReallyReallyCuteFrame(props: MyReallyReallyCuteFrameProps)
return React.createElement("Frame", {
Size = UDim2.new(0,200,0,200),
AnchorPoint = Vector2.new(0.5,0.5),
Position = UDim2.new(0.5,0,0.5,0),
BackgroundColor3 = Color3.fromRGB(255,194,132),
}, {
React.createElement("UIPadding", {
PaddingTop = UDim.new(0,20),
PaddingBottom = UDim.new(0,20),
PaddingLeft = UDim.new(0,20),
PaddingRight = UDim.new(0,20),
}),
React.createElement("UICorner", {
CornerRadius = UDim.new(0,20)
}),
React.createElement("UIStroke", {
Thickness = 8,
Color = Color3.fromRGB(255,106,106),
}),
Content = React.createElement("Frame", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(255,251,149),
})
})
end
Adding a UIListLayout
will position all children in order based on their respective sizes. This is super useful for managing layout and itâs common to have your UI tree composed of multiple Frames
with UIListLayouts
.
local function MyLayoutExample()
return React.createElement("Frame", {
Size = UDim2.new(0,200,0,400),
}, {
React.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
}),
React.createElement("Frame", {
Size = UDim2.new(1,0,0.5,0),
}, {
React.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
}),
React.createElement("Frame", {
Size = UDim2.new(0.5,0,1,0),
BackgroundColor3 = Color3.fromRGB(255,194,132),
}),
React.createElement("Frame", {
Size = UDim2.new(0.5,0,1,0),
BackgroundColor3 = Color3.fromRGB(194,255,132),
}),
}),
React.createElement("Frame", {
Size = UDim2.new(1,0,0.5,0),
BackgroundColor3 = Color3.fromRGB(255,251,149),
}),
})
end
Note, if you are using explicit non integer table keys, you will also need to set the LayoutOrder
property.
Typing your Props
When youâre writing Lua scripts in Roblox, youâre actually writing Luau! Luau is a superset of the Lua language that Roblox develops and maintains. Luau adds strict types to Lua which can really improve the safety and legibility of your code! To enable strict type checking, just add --!strict
at the top of your scripts!
The entirety of react-lua is written in strictly typed Luau. You can and should use types in your own component props. In many of the above examples, we pass a nebulous props
argument to the fuctional components we defined. We can add types to declare what we expect these props to be.
export type MyTypedComponentProps = {
content: string, -- required
fontSize: number? -- optional!
}
local function MyTypedComponent(props: MyTypedComponentProps)
local newFontSize = props.fontSize or 24
local newContent = "the following content " .. props.content .. " is size " .. tostring(newFontSize)
return React.createElement("TextLabel", {
Size = UDim2.new(0,100,0,100),
BackgroundColor3 = Color3.fromRGB(255,194,132),
Text = newContent,
TextSize = newFontSize,
})
end
If we had, for example, forgotten to give a defaut value to the optional fontSize
before using it, we would have gotten an error!
A full tutorial on how to adopt Luau strict type checking is out of scope of this article.
Class Components
With the latest version of React, class components are discouraged. Functional components are largely simpler and easier to use and are sufficient 99.9% of the time.
Class components may offer some advantages in certain situations. Since the lua does not support native classes, the syntax for declaring class components is as follows:
local MyComponent = React.Component:extend("MyComponent")
function MyComponent:render()
return React.createElement("TextLabel", {Text = self.props.text})
end
function MyComponent:componentDidMount()
print("rendered with text " .. self.props.text .. " by the way, you could have just done this with useEffect")
end
Similarly, if you want to declare a PureComponent:
local MyPureComponent = React.PureComponent:extend("MyPureComponent")
Roact Migration
Some of you may be familiar with Roact which is an earlier manual port of the react programming paradigm. Roact is no longer being maintained and does not support functional components. For the most part, migrating is just a matter of replacing require(...Roact)
with require(...React)
. See the react-lua documentation for guidance on items that require more attention. Note that sometimes react-lua is still referred to as Roact (and indeed the repository is named roact-alignment). Itâs a nominal difference. Just make sure you arenât using the old Roact!!
Contributing to react-lua
JS.Lua hosts the community maintained fork of react-lua and is also the source from which the React Wally packages are built from. You may open issues and pull requests here. This is forked from the Roblox maintained React-lua repo which is currently not accepting community contributions. This is primarily due to 2 reasons:
- React-lua attempts to closely follow the upstream React code and therefore there are strict contribution guidelines that still need to defined
- Robloxâs internal CI/CD tools for lua development are not publicly available at this time and therefore changes can not be automatically verified.
In there future, things may change and there may be more collaboration and coherency here, but what we have right now is what we have. There is so much potential for a vibrant, diverse and collaborative open source ecosystem around Lua and Roblox!!