How To: React + Roblox

I can’t provide a clean way.

But I’ve used one before, but it was pre-made by Roblox.

local function PseudoFuncComp(props)
  local expanded, setExpanded = React.useState(false)

  props.propsRef = setExpanded

  -- return React Element or something
end

Then you can use setExpanded(true) to expand it, and use expanded to read the value and implement the rest of the logic.

e.g. using Visible = true if expanded is true, and then you can modify the style of your Expandable Element, based on whether it’s expanded or not.

 

You can put references to that State inside the props.propsRef, while it sounds hacky, it works. You can’t do that in ReactJS, that’s why it’s hacky.

In ReactJS you’d have to use forwardRef. The ReactJS Community doesn’t seem to have a better way without Context or forwardRef.

 

Everything that checks for expanded, should automatically re-render. Just note when you use tables as your states, that I’ve encountered a Roblox Developer Framework Component called “Tabs”, where I had to use an unique Identifier for the state, because the Tabs used tables as their info, and I had to put an unique Identifier inside the tables so I could universally get it, independent from table address.

Using something like a react spring is very handy for animating because you can just describe your UI effects declaratively

3 Likes

I am providing a quick demo on what it could potentially look like since it could benefit people trying out react and react-spring. I hope this helps @Vundeq @HealthyKarl

To follow along with this example, create two files, the first one will contain our button component which when we click, we want to add the fade out effect. When the button is first added to whatever component we add to, we want to add the fade in effect

-- Button.luau file
-- !! Make sure to require relevant react, and react-spring packages. I am omitting from this file since our paths can differ a lot
--[[
      The idea is that in our parent component, where we use this Button, we want to
      control the state of this button. We will provide whether it should be in
      mounting, unmounting or active state

      mounting = We want to play the fade in effect
      unmounting = We want to play the fade out effect
      active = We want it to stay static on screen
]]
export type Transition = "mounting" | "unmounting" | "active"
export type ButtonProps = {
	children: react.ReactNode?,
	transition: Transition,

	event: {
		-- What to do when the button is clicked
		onClick: () -> (),

		-- What to do when the button is on mounting state
		afterMounting: () -> (),
		-- What to do when the button is on unmounting state
		afterUnmounting: () -> (),
	},
}

return function(props: ButtonProps): react.ReactNode
	-- See the React-spring library on how to use springs
	-- We want to use imperative style since we want to control when to run springs
	local style, api = useSpring(function()
		return {
			to = { transparency = 0 },
			from = { transparency = 1 },
			config = {}, --See react spring configs to play around with animating values
		}
	end)

	--[[
    When doing imperative springs, it is important to do it inside useEffect
    The idea is that whenever the props we pass to the button changes, we want to run this
    function we pass to the useEffect

    If props is mounting, we want to play the mounting spring animation and then invoke the callback afterMounting function
    If props is unmounting, we want to play the unmounting spring animation and then invoke the callback afterUnmounting function
  ]]
	useEffect(function()
		if props.transition == "mounting" then
			api.start({ to = { transparency = 0 } }):andThen(function()
				props.event.afterMounting()
			end)
		elseif props.transition == "unmounting" then
			api.start({ to = { transparency = 1 } }):andThen(function()
				props.event.afterUnmounting()
			end)
		end
	end, { props.transition }) -- ALWAYS REMEMBER to add useEffect dependencies! Otherwise we can change prop as many times and we will not see any changes

	return e("TextButton", {
		Size = UDim2.new(0, 200, 0, 50),
		BackgroundColor3 = Color3.fromRGB(255, 255, 255),
		BackgroundTransparency = style.transparency, -- This is where we tell react how to get the transparency values
		TextTransparency = style.transparency, -- same as above
		[react.Event.Activated] = function() -- run the onClick function when we click on button
			props.event.onClick()
		end,
	}, props.children) -- We are just passing children in case we want to add other effects like UICorner, etc
end

The convenient thing about springs is that they are Promise based. Which means when an animation is done animating, (in our case the fade in/out effect), we can pass some callback function as props to invoke when it is finished animating.

Then in somewhere else, (for this example, I am doing it in a story), we can control the behaviour of this button we just created.

-- At button.story.luau file
-- !! Make sure to require relevant react, and react-spring packages. I am omitting from this file since our paths can differ a lot
local function Story()
	local render_button, set_render_button = useState(true)
	local transition_state: Transition, set_transition_state = useState("mounting" :: Transition)

	return e(HoarcekatStory, {}, { -- this is just a component I use to render story items, it is not necessary to understand this for this demo
        -- render the button we created now
		One = render_button and e(Button, {
			transition = transition_state,
			event = {
				onClick = function()
					set_transition_state("unmounting")
				end,
				afterMounting = function()
					set_render_button(true)
					set_transition_state("active")
				end,
				afterUnmounting = function()
					set_render_button(false)

					-- !! THIS IS FOR DEMO ONLY (so I can loop it)
					task.delay(2, function()
						set_render_button(true)
						set_transition_state("mounting")
					end)
				end,
			},
		}),
	})
end
4 Likes

is there any way i can get the it just as a module in an rbxm, i dont use wally

I don’t know if there is an easy way to be honest. But you could use something like Studio Wally. This has a roblox plugin which you can directly install. In order for this to work properly you apparently also need to install this another plugin called Rojo Boatly

Kinda complicated but unfortunately I don’t know any other way.

1 Like

jsdotlua hosts the library as an rbxm file on their release page Tags · jsdotlua/react-lua · GitHub

If you want something more official, it’s also available as a toolbox model on the Roblox account here https://create.roblox.com/store/asset/15621638430/ReactLua

I’ve only tried it with the jsdotlua release myself so I would suggest using that.

i was talking about an animation library not react itself

How exactly does React.useContext work (in terms of changing it during game)? I looked at the reactjs docs and sort of understand, but the syntax is too different for me to gain a full grasp of it

Your getting a lot of hate for this but I agree. Nothing was as painful as trying to modify the modern Roact UI corescripts compared to the ones they replaced.
I gave up after 4 hours of no progress

2 Likes

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.

2 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

image


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

1 Like

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.

1 Like

Solved partially in DMs;

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