How To: React + Roblox

You can just think of context as a way to avoid passing props too many times, such that multiple components can read and update the state, which will cause everyone that is consuming this context to reload to the new state.

A context would just have a state holding some data, and you just export out this data so your components can read them, and you export out the functions to update the data, so you can control what the context data changes to. A simple example is a page context. To follow along with this example, create three files. The first file is a file called PageContext.luau which is our context file

-- At PageContext.luau file
-- !! Make sure to require relevant react packages. I am omitting from this file since our paths can differ a lot
-- Pages we want to navigate our context to
export type Pages = "Home" | "Shop" | "Game" | "Settings"
export type PageContext = {
	-- This is the active page our context holds
	page: Pages,

	-- This is a function to change our page to the page we pass in the parameter
	changePage: (page: Pages) -> (),
}

-- We can either set the default value to nil, or we can provide a default value to the context
-- When you provide default value, it means two things
-- The first time the context runs, this default value is used
-- Whenever you try to access the context outside of its provider, you can use this value
-- Although IDEALLY, you don't want to access the value outside the providers
-- So in react.js it is a common practice to set the default value to undefined or nil
-- If you don't want to set it to undefined, thats okay too, just pass some default value instead
local page_context: react.Context<PageContext?> = createContext(nil :: PageContext?)

local function provider(props: react.ElementProps<any>): react.ReactNode
	-- Page is the current context state, set_page is the useState function to change the state
	-- Note that set_page can only recieve Pages as the parameter
	-- We are setting the default value of the page to `Home`
	local page, set_page = useState("Home" :: Pages)

	-- Attach our state and the handler as a value
	-- We will then pass this to the context.value
	local value: PageContext = {
		page = page,
		changePage = set_page,
	}

	-- Note that e is just local e = react.createElement;
	-- We are basically wrapping all the children components inside the Provider
	-- This way all the children components can access the provider's values.
	-- By default, react always passes a prop called `children`. Which is what we are using here
	return e(page_context.Provider, {
		value = value,
	}, props.children)
end

local function usePage(): Pages
	local context = useContext(page_context)
	if context == nil then
		return error(`Attempted to access Page context from outside the provider`)
	end

	return context.page
end

local function useChangePage(): (Pages) -> ()
	local context = useContext(page_context)
	if context == nil then
		return error(`Attempted to access Page context from outside the provider`)
	end

	return context.changePage
end

return {
	provider = provider,
	usePage = usePage,
	useChangePage = useChangePage,
}

Now, we need to wrap our App into the providers. It is also a common practice to have all the context wrapped in at the top of our app. Here is the App.luau file which is our main client file essentially. For our demo purpose, this file is going to be very simple

-- At App.luau file
-- !! Make sure to require relevant react packages. I am omitting from this file since our paths can differ a lot
-- I am also importing Page Context module in this file as well
local function App()
	-- Consume the page here so we can conditionally render our pages
	-- Annnoying luau bug here as well since Pages datatype gets coerced into strings
	-- Which is why we must manually assign the pageContext.Pages type for now.... sigh...
	local active_page: pageContext.Pages = pageContext.usePage()

	-- Now, we can conditionally render each of our pages based on what the active page is
	return e("ScreenGui", {}, {
		Home = active_page == "Home" and e(HomePage),
		Settings = active_page == "Settings" and e(SettingsPage),
		Game = active_page == "Game" and e(GamePage),
		Shop = active_page == "Shop" and e(ShopPage),
	})
end

-- This is the function that you will mount on the react-roblox
-- I will not be going over the implementation detail of that since that is covered in
-- tutorial in this forum to begin with
local function main(): react.ReactNode
	return e(pageContext.provider, {}, App)
end

You can also require this Page Context file anywhere else as well. And it works great because module scripts are cached. So lets say inside the Settings.luau file we want a button that is going to get us back to home page. Well, we can just require the Page context file, and when a button is clicked, use the changePage function and pass in Home. A simple example can look like this

-- At SettingsPage.luau file
-- !! Make sure to require relevant react packages. I am omitting from this file since our paths can differ a lot
-- I am also importing Page Context module in this file as well
export type SettingsPageProps = {}
local function SettingsPage(props: SettingsPageProps): react.ReactNode
	local changePage = pageContext.useChangePage()

	-- Settings papge is rendered in the App file earlier when current page is set to "settings"
	-- So when this page is being rendered in our UI, we know that the current page is settings
	return e("Frame", {
		Size = UDim2.fromScale(1, 1),
	}, {
		-- We have a go back home button here
		HomeButton = e("TextButton", {
			Size = UDim2.new(0, 200, 0, 50),
			Text = "Home",

			-- When a button is activated, we simply change the page context's state to Home
			-- Then the page context will force rerender on all of the components that are
			-- consuming this Context. Then the app component will render Home page instead
			[react.Event.Activated] = function()
				changePage("Home")
			end,
		}),
	})
end

Context are also commonly used in roblox to listen for remote events or Signal changes through server/client. So when multiple components needs to listen to remote event changes, you would listen to the remote events inside the context. However, if only a single component or a child of a parent component needs to consume a state, then it isn’t necessary to use context. So make sure you aren’t overusing context either.

5 Likes

Hi Theo, can UILabs work with react-lua? I just did some testing and it turns out the functional ui broken when I attempted to view it from the UI Labs Console

1 Like

image


I’m good at breaking things (I have no clue what I did)

5 Likes

You should post your code, most likely you are doing something very wrong. Errors like those are very uncommon.

You are most likely using react.createElement wrong since this usually occurs when you try to create something that isn’t a valid react component.

2 Likes

Solved partially in DMs;

Make sure you’re not accidentally inserting nested tables into your react element’s children field

1 Like

So in ReactJS apparently the render() function gets called everytime it gets triggered.

e.g.

  • Parent render()
    • Child render()
    • Child render()

Something like that.

Maybe it only does that when the props are changed, not sure.

 

It doesn’t re-create the HTML Elements which is good, I’d assume the same on the Roblox one. But some things become annoying.

If you have very heavy computations inside each render() it may cause Components to act very laggy. Unlike when one had the ability to do :Init()

Not sure if there’s a workaround for that, without adding a prop that is checking whether it was “initiated”

If your component needs to perform expensive calculation, you should be memoizing the results so it does not recalculate it every time it is being rerendered. This is possible using hooks such as react-lua/useMemo or react-lua/useCallback.

You can learn about them and how to use them here: react/useMemo and react/useCallback

2 Likes

I actually don’t know if react-lua specifically also reserves less memory for Luau State.

Compared to things that aren’t made with React. Can be hard to figure out with gcinfo(), maybe the Memory thing in F9 showed me a better value.

Hello all, new to react & react-lua. Was looking for some guidance on solving my problem regarding responsive design.

Im current using a context object to handle breakpoints for a responsive UILayout.

My context setup:

Which is then passed to my component tree:

And then used in the following component:

I feel like I’m using context wrong / using providers wrong…
Any guidance appreciated!

Hi,

Few glaring issues I see are as follows:

  • Separate connections into individual useEffect. It is typically good idea to only have one connection per useEffect. This is the recommend approach by official react.js docs
  • Each useEffect must have dependencies. This is EXTREMELY important. React will NOT default to empty array if you don’t pass any dependencies. If you don’t have any dependencies, you ideally should just have something like
useEffect(function () end, {})

In your case you have something like this

useEffect(function () end)

This is very bad because your useeffect will run every frame. You do not want this at all.

  • Ensure that the value’s datatype matches with whatever you have while you are creating context. Im not really use what Breakpoint returns so make sure it matches properly.

  • You want to ensure you have error handling on useDevice. This is because if you try to access device outside of the provider, you want it to throw error. This is another standard react.js way of using Context.

  • You have an error on 2 lines above line 29 on 2nd image. You NEVER CALL THE FUNCTIONAL COMPONENT. You ALWAYS do react.createElement(App). NOT App().

  • It is a good practice/habit to creating Keys for all of your functional components. This is to ensure you dont have 1) type errors, 2) unexpected rerenders especially on lists. So what I mean is do this

return e(Something, {}, {
  App = e(App)
})

not this

return e(Something, {}, {
  e(App)
})
  • Since you have already created a Provider function for your Breakpoints, you want to use it like this
local BreakpointProvider = require(breakpointcontext path).Provider
return e(LayoutContext.Provider, {}, {
  e(BreakpointProvider, {}, {
    App = e(App)
  })
})

These are the only surface level issues I have seen, let me know if you have more questions or etc.

2 Likes

Great take, esp explaining that UI is a visual concept…does not make sense for it to be purely code…and anyways, you can already purely code UI right now…so i dont get how this is such an amazing selling point. You can make your own resusable UI in a module script. It truly is pointless lol.

I’m having a problem with react, I’m trying to create useState, and it gives me an error

I’ve searched everything and found nothing, if someone could tell me the problem, code below:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local React = require(ReplicatedStorage.Packages.react)

export type Props = {
  Name:string,
  Position:UDim2?,
  ImageId:string,
  HoverImageId:string,
  LayoutOrder:number,
}


function CreateButton(Props:Props)
  local hover, SetHover  = React:useState({state = false})

  local ImageId = Props.ImageId
  local HoverImageId = Props.HoverImageId

  local DefaultColor = Color3.fromHex("363636")
  local HoverColor = Color3.fromRGB(255,255,255)
  return React.createElement("ImageButton",{
    LayoutOrder = Props.LayoutOrder,
    Name = `{Props.Name}Button`,
    Size = UDim2.fromOffset(498,62),
    Position = Props.Position or UDim2.fromScale(0,0),
    AnchorPoint = Vector2.new(0,0),
    BackgroundTransparency = 1,
    Image = ImageId,
    ScaleType = Enum.ScaleType.Fit,
    [React.Event.MouseEnter] = function()
      SetHover({state = true})
    end,
    [React.Event.MouseLeave] = function()
      SetHover({state = false})
    end,
  },{ React.createElement("UIAspectRatioConstraint",{AspectRatio = 8.04}),
      React.createElement("TextLabel",{
        Name = `{Props.Name}Text`,
        AnchorPoint = Vector2.new(0.5,0.5),
        Position = UDim2.fromScale(0.5,0.5),
        Size = UDim2.fromScale(0.8,0.8),
        BackgroundTransparency = 1,
        Text = Props.Name,
        TextColor3 = DefaultColor,
        TextScaled = true,
        Font = Enum.Font.SourceSansSemibold,
      })
    })
end


return CreateButton

If anyone can help, I’d be very grateful

A little late so hopefully you fixed by now but the issue was that you are using colon syntax for useState when it should be using dot syntax.

That is, it should be

React.useState({}) -- NOT React:useState({})

For faster react-lua help, join the Roblox OSS discord. There is a react channel which is pretty active and great place to ask for questions.

it’s a terrible take… the point of a ui library is to optimize the way you create UI. coding Ui is the standard outside of Roblox at least, and think of this. your method is just using plain html and css while a ui library is using js to better control your UI and the way it behaves, which leads to cleaner code. there’s a significantly higher learning curve but it’s more beneficial in the end. although i do agree react kinda sucks for roblox and its not as efficient as libraries like vide or lumin ui

As someone who’s been developing on the platform for several years now, I was also weary about using a UI library, as I shared similar opinions to those who are against using it (“it’s useless!”)

Switching to React was the best decision I’ve ever made and I couldn’t imagine making a game without it now.

React makes managing massive projects 1000x easier, and it does all the hard work for you.

I use Roblox-TS which makes using React even more pleasant. I would highly suggest anyone who is weary about using React to atleast give it a go - and if your current workflow is working, don’t use it! Use whatever you’re comfortable with.

1 Like

Im someone who wants to give a UI library a shot, however I’ve got no idea how its better than actually creating the UI visually since you get to see what it looks like and stuff. Where as when you code it you don’t get to visually see it unless you imagine it. Simply put, I just want to know how you actually use it, like the process of using it.

This was one of my main worries too, being unable to actually visually see what I’m making.

You don’t need to use the library to design your UIs - I design my UI in studio as you usually would, and then manually translate it into code. Doing this allows me to see what I’m making and not have to worry about the programmed version looking bad, if anything, the programmed version always comes out tidier due to reusing components, like a button

1 Like

Hi!

If you wish to visualize the user interfaces you code, I recommend this Roblox UI Lab Plugin.

UI Lab is a powerful, user-interface friendly storybook development plugin which allows you to preview the user interface on Roblox Studio while you code on Visual Studio Code.

To get started, you’ll need to write your own story and use the plugin to display user interface using your story, learn more on it’s own official documentation about usage. Happy coding!

4 Likes

I honestly wish there was a more straightforward visualizer, because I am honestly just not able to comprehend storybook-based visualizers. If there was one where you select a script, and it would visualize that script’s react code, that would be a godsend.

Is this just me being incompetent, and being unable to wrap my head around that stuff? Yes. Would having a more straightforward visualizer still help? Also yes.

1 Like

Trust me, a storybook-based plugin is not as hard-to-use as you think!

The reason why the process is not straightforward is because the plugin does not know which part of the UI to render, even if you select the entire script.

I suggest that you create a new script, ending with the extension .story, as these scripts are only detected by the plugin.

The plugin will then try create a element from that script to render, so your script should have the component which returns the elements (You can even duplicate the original UI if you want!).

The exact steps? Might vary, so I suggest reading the documentation I linked above!

1 Like