-
Notifications
You must be signed in to change notification settings - Fork 0
Migration Guide
This guide helps you move an existing @rbxts/react v17 project to @nrbx/react v19 with the least friction.
@nrbx/react keeps the React-style development model you already know, but adds a React 19-style runtime, JSX wrapper, className styling, text-as-children support, and a few compatibility changes that are worth handling intentionally.
The biggest changes are:
- Package rename:
@rbxts/react→@nrbx/react - New JSX factory:
React.createElementwrapper with text/className/event support - String children auto-create
TextLabels -
Event={{ Activated: ... }}is still accepted, butonClick={...}is preferred - Styling moves from raw Roblox props to
className/tw()/defineConfig - HTML element aliases like
<div>,<span>,<button>,<h1>are available - New React 19 APIs are available (
useId,useTransition,useDeferredValue,useActionState, etc.) - Error boundaries, forms, motion, gradients, and class components are built in
Before:
{
"dependencies": {
"@rbxts/react": "^17.3.7"
}
}After:
{
"dependencies": {
"@nrbx/react": "^19.0.0"
}
}Install:
npm install @nrbx/react@^19.0.0If you also use the Roblox renderer, install or update it as needed:
npm install @nrbx/react-robloxThe import changes are straightforward:
Before:
import React from "@rbxts/react";After:
import React from "@nrbx/react";You should also update any named imports:
import React, { useState, useMemo, useEffect } from "@nrbx/react";If you imported a project helper or utility from the old package namespace, check whether it was moved or renamed.
@nrbx/react uses a React.createElement wrapper that understands Roblox-friendly props, text children, event props, and className.
If you previously configured custom JSX output, make sure it points at the new runtime.
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.createFragment"
}
}If your project has a custom tsconfig or a roblox-ts config that was pointing at the legacy package, update the JSX configuration to match the new runtime:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.createFragment",
"paths": {
"@nrbx/react": ["node_modules/@nrbx/react/src"]
}
}
}If you previously used a custom factory or custom JSX transform, check any jsxInject / jsxFactory references and replace them with the @nrbx/react equivalent.
The main migration is moving from the old Roblox-ish Event={{ ... }} pattern toward more React-like event props such as onClick, onChange, and similar onXxx props.
<textbutton
Text="Click"
onClick={() => print("hi")}
className="bg-blue-500"
/>Before:
<textbutton
Text="Click"
Event={{ Activated: () => print("hi") }}
BackgroundColor3={Color3.fromRGB(0, 100, 255)}
/>
After:
<textbutton
Text="Click"
onClick={() => print("hi")}
className="bg-blue-500"
/>| Old pattern | Preferred v19 pattern | Notes |
|---|---|---|
Event={{ Activated: handler }} |
onClick={handler} |
Use the React-style event name when available |
Event={{ MouseButton1Click: handler }} |
onClick={handler} |
onClick is preferred for button-like interactions |
Event={{ TextChanged: handler }} |
onChange={handler} |
Common React-style form naming |
Event={{ FocusLost: handler }} |
onBlur={handler} |
More React-like semantics |
Event={{ InputBegan: handler }} |
onMouseDown={handler} or other specific handlers |
Some event names are mapped by element behavior |
Important:
-
Event={{ ... }}still works for compatibility. -
onXxxis preferred for readability and consistency. - If you still use
Event, it is not an immediate blocker — just treat it as a migration path, not the target pattern.
@nrbx/react supports plain text children automatically. If you pass a string or number as a child to a container, it is turned into a TextLabel automatically instead of requiring explicit wrapping.
Before:
<frame>
<textlabel Text="Hello" BackgroundTransparency={1} />
</frame>After:
<frame>
Hello
</frame>This is especially useful for labels, headings, and nested text content:
<div className="p-4">
<h1 className="text-2xl font-bold">Welcome</h1>
{"Ready to play"}
</div>Text-capable elements such as TextLabel, TextButton, and TextBox receive the text directly as their Text property.
The new style system is built around className and utility classes rather than manually setting BackgroundColor3, TextColor3, BorderSizePixel, and similar props everywhere.
Before:
<textbutton
Text="Save"
BackgroundColor3={Color3.fromRGB(59, 130, 246)}
TextColor3={Color3.fromRGB(255, 255, 255)}
BorderSizePixel={0}
Size={new UDim2(0, 140, 0, 40)}
/>After:
<textbutton
Text="Save"
className="bg-blue-500 text-white border-0 rounded-md"
Size={new UDim2(0, 140, 0, 40)}
/>import React, { tw, defineConfig } from "@nrbx/react";
defineConfig({
colors: {
brand: { 500: "#3b82f6", 600: "#2563eb" },
},
});
const buttonClass = tw("rounded-md bg-brand-500 px-4 py-2 text-white");
return <button className={buttonClass}>Save</button>;This is the recommended direction if you want to keep styling consistent and readable.
@nrbx/react provides familiar HTML element aliases such as:
divspan-
h1,h2,h3 buttoninputlabelimg
This makes migration easier when you are used to web-like markup.
Example:
<div className="flex flex-col gap-2 p-4">
<h1 className="text-2xl font-bold">Profile</h1>
<span className="text-sm text-gray-500">Welcome back.</span>
<button className="bg-blue-500 text-white px-4 py-2 rounded">Open</button>
</div>If you prefer to keep direct Roblox control names, you can still do that; the new HTML aliases are simply a more familiar option.
The component model is familiar, but the runtime has updated semantics in a few places.
Before:
const Greeting = (props: { name: string }) => {
return <textlabel Text={`Hello, ${props.name}`} />;
};After:
const Greeting = ({ name }: { name: string }) => {
return <textlabel Text={`Hello, ${name}`} />;
};@nrbx/react supports class components with full lifecycle support.
class Counter extends React.Component<{}, { value: number }> {
state = { value: 0 };
render() {
return (
<button className="bg-blue-500" onClick={() => this.setState({ value: this.state.value + 1 })}>
{this.state.value}
</button>
);
}
}A breaking change to be aware of:
-
refobjects now look like{ current: T } - they are no longer raw Roblox object refs in the same way they were in older code
Before:
const myTextLabel = useRef<TextLabel>();After:
const myTextLabel = React.useRef<TextLabel | undefined>(undefined);In practice, this means reading the value from myTextLabel.current instead of treating the ref as the instance object itself.
@nrbx/react includes React 19 APIs and helpers that are not available in older @rbxts/react versions.
Examples include:
useIduseTransitionuseDeferredValueuseActionStateuseFormStatususeOptimistic-
use(for promise/context patterns) startTransition
Example:
import React, { useId, useTransition } from "@nrbx/react";
function SubmitButton() {
const id = useId();
const [isPending, startTransition] = useTransition();
return (
<button
id={id}
className="bg-blue-500 text-white px-4 py-2 rounded"
onClick={() => {
startTransition(() => {
print("transitioning");
});
}}
>
{isPending ? "Saving..." : "Save"}
</button>
);
}@nrbx/react includes built-in error boundary support to catch rendering failures in child trees.
class AppErrorBoundary extends React.Component {
static getDerivedStateFromError(error: unknown) {
print("render error", error);
return { hasError: true };
}
render() {
return this.props.children;
}
}The new form APIs make server-action style patterns easier, especially for user input and optimistic updates.
import React, { useActionState, useFormStatus } from "@nrbx/react";
function SubmitForm() {
const [state, formAction] = useActionState(async () => {
print("submitted");
return "Saved";
}, "Idle");
return (
<form action={formAction}>
<button className="bg-green-500 text-white px-4 py-2 rounded">{state}</button>
</form>
);
}@nrbx/react includes motion/animation support in the runtime, making UI transitions easier without writing custom tween code for every interaction.
<div className="transition-all duration-200 hover:scale-105">Hello</div>You can use gradient backgrounds and rich visual styling through the config and className layer:
<div className="bg-gradient-to-r from-blue-500 to-purple-500" />Solution:
- Keep it working temporarily during the migration
- Convert to
onXxx={handler}as you touch each component
Solution:
- Replace raw style props with
className - Use
tw()for repeated styles - Centralize shared rules with
defineConfig()
Solution:
- Remember that plain string children are now auto-wrapped for container elements
- Use
Textfor explicit text values on text-capable controls
Solution:
- Read from
ref.current - Type the ref as
TextLabel | undefined,Frame | undefined, etc.
Solution:
- Ensure
tsconfig.jsonusesjsxFactory: "React.createElement" - Recheck any custom project transforms or old
roblox-tssettings
Solution:
- Confirm you are using the new runtime and JSX wrapper
- Check that you are passing the
classNameprop to the component tree after the migration
You do not have to rewrite everything in one pass. The new runtime allows a gradual migration path.
Example:
function Toolbar() {
return (
<frame Size={new UDim2(1, 0, 0, 56)} BackgroundColor3={Color3.fromRGB(17, 24, 39)}>
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
Event={{
Activated: () => print("Still works during migration"),
}}
>
Save
</button>
</frame>
);
}As you touch a component, convert it to the new pattern:
function Toolbar() {
return (
<div className="h-14 w-full bg-slate-900">
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={() => print("Preferred")}>
Save
</button>
</div>
);
}This lets teams migrate component by component without a big-bang rewrite.
The following are the key compatibility changes in v19:
-
Event={{ ... }}still works, butonXxxis preferred -
React.createPortal()andReact.flushSync()are now in@nrbx/react-roblox(the renderer), matching React's own split betweenreactandreact-dom. Import them asimport { createPortal, flushSync } from "@nrbx/react-roblox". -
React.useState(),React.useEffect(), and all other hooks are available on the default import (import React from "@nrbx/react"→React.useState(...)). BothReact.useState()andimport { useState } from "@nrbx/react"work. - Ref objects are
{ current: T }rather than raw Roblox object refs - Some internal types and runtime expectations changed
- Styling is now driven by
className/ config rather than direct prop-by-prop Roblox assignments - JSX output relies on the new
React.createElementwrapper
These are manageable, and most migrations are a matter of updating your style and event patterns rather than rewriting your entire app.
If you want the least painful upgrade path, do this in order:
- Update
package.jsonand install@nrbx/react - Update
import React from "@rbxts/react"to@nrbx/react - Fix your JSX factory /
tsconfigsetup - Replace major
Event={{ ... }}handlers withonXxx={...} - Introduce
classNameandtw()for styling - Convert repeated raw Roblox props to utility classes
- Adopt HTML elements and text-as-children where helpful
- Optionally move to new hooks, forms, motion, and error boundaries
@nrbx/react v19 is a modernized, React-19-style API surface. The migration is most often a package rename plus a style and event cleanup, not a complete rewrite.
The most important decisions are:
- update imports
- update
jsxFactory - prefer
onXxxevents overEvent={{ ... }} - adopt
className/tw()styling - migrate gradually as you touch components
If you follow those steps, most projects move over cleanly and gain a more ergonomic React-like development experience.