-
Notifications
You must be signed in to change notification settings - Fork 0
Caveats and Limitations
@nrbx/react is a self-contained TypeScript port of the React reconciler for Roblox, with React 19-era APIs layered on top. It has no dependency on @rbxts-js/* or any Lua-vendored runtime. The reconciler core still descends from the React 17-generation architecture, so a number of React 18/19 runtime semantics behave differently here than in the browser.
This page is intentionally candid: if a pattern works in the browser but depends on React 18/19 runtime behaviour, it may not behave the same way here.
The reconciler core is a direct TypeScript port of the React 17 fiber implementation. The public API (hooks, types, entry points) is React 19-flavored, but the scheduling and update machinery underneath is not the React 18/19 concurrent implementation. That means several React 18/19 runtime features are not available in the same way they are in the browser.
- No true Concurrent Mode
- no
createRootconcurrent features - no deep
useDeferredValueintegration - no Suspense-driven data fetching model
- no
- No automatic batching
- state updates are effectively synchronous from the runtime's perspective
-
setState(prev => next)may not behave as expected in the same way as React DOM -
useTransitiondoes not work in the React 18/19 sense withisPendingindicators
- Prefer explicit state updates and simple commit patterns instead of depending on batched transitions.
- Keep state values primitive or object-based and assign them directly when you know the next value.
- For state sequences, use refs when you need to accumulate values before committing.
- For loading or "in-flight" states, use your own boolean flags, such as
isBusyandisLoading, rather thanuseTransition.
import React, { useRef, useState } from "@nrbx/react";
function Counter() {
const [count, setCount] = useState(0);
const nextValue = useRef(0);
function increment() {
nextValue.current += 1;
setCount(nextValue.current);
}
return (
<textbutton onClick={increment}>Count: {count}</textbutton>
);
}Text-as-Children is a convenience feature: plain strings and numbers are automatically wrapped in Roblox text instances when you write JSX such as:
<frame>Hello world</frame>This is helpful, but it comes with costs.
- It creates additional
TextLabelinstances behind the scenes. - Large trees of text content can increase instance count.
- Empty strings and
0are still rendered as text labels. - If you need precise control over layout or styling, explicit
<textlabel>elements are clearer and more predictable.
- For important text, use explicit
textlabelnodes and configure them directly. - Avoid relying on empty text nodes as spacers.
- Use layout constraints or
UIListLayout/UIPaddingrather than empty labels as padding.
function Header() {
return (
<frame Size={new UDim2(0, 240, 0, 48)}>
<textlabel
Size={new UDim2(1, 0, 1, 0)}
Text="Welcome"
BackgroundTransparency={1}
TextXAlignment={Enum.TextXAlignment.Left}
/>
</frame>
);
}Tailwind-style className support is useful, but it is not a full browser Tailwind implementation.
- Not all Tailwind classes are supported.
- Roblox doesn't have CSS positioning, so classes like
absoluteandrelativeare not meaningful. - There are no media queries or responsive breakpoints like
@media. - No
dark:mode behavior. - No
peer-*orgroup-*variants. - Only a curated subset of classes is available.
- Class resolution happens at element creation time, not at compile time, which can affect performance if you generate many dynamic class names.
- Use a curated set of supported classes only.
- Prefer explicit Roblox properties for layout and position rather than trying to mimic CSS layout semantics.
- For dynamic styling, compute a small set of known class combinations or use direct instance props.
function Card() {
return (
<frame
className="flex h-16 w-64 items-center rounded-xl bg-slate-900 px-4"
Size={new UDim2(0, 256, 0, 64)}
>
<textlabel
className="text-sm font-bold text-white"
Text="Inventory"
BackgroundTransparency={1}
/>
</frame>
);
}Not every HTML element has a direct Roblox UI equivalent.
The following are not supported:
canvasvideoaudioiframe-
table,tr,td,th,thead,tbody
Some inputs have no Roblox equivalent or only partial approximations:
-
<input type="file">has no real file-system upload path in Roblox. -
<input type="datetime-local">is approximated with aTextBoxand custom date formatting.
- Use
frame,textlabel,textbox,imagebutton, andscroller-style layouts instead of HTML-only structures. - For file input, build a custom upload flow using a Roblox UI and a server or remote event boundary.
- For date/time input, wire a
TextBoxto a formatter/parser you control.
function DateInput() {
return (
<textbox
Text="2026-08-12"
PlaceholderText="YYYY-MM-DD"
ClearTextOnFocus={false}
TextXAlignment={Enum.TextXAlignment.Left}
/>
);
}React's browser-style SyntheticEvent system does not exist here.
- Event handlers receive raw Roblox input objects, not DOM-like events.
-
event.targetis the RobloxInstance, not a React element. -
event.preventDefault()does not exist. -
event.stopPropagation()does not exist. - Input events carry
InputObject, which behaves differently from DOM events.
- Treat the first callback argument as the Roblox instance and the second as input data.
- Use Roblox-native APIs such as
input.UserInputType,input.KeyCode, and instance property checks instead of browser event semantics. - Build custom event wrappers if you want a higher-level abstraction.
function SearchBox() {
return (
<textbox
onInputBegan={(rbx, input) => {
if (input.UserInputType === Enum.UserInputType.Keyboard) {
print("Key pressed:", input.KeyCode.Name);
}
}}
onInputChanged={(rbx, input) => {
if (input.UserInputType === Enum.UserInputType.Keyboard) {
print("Current key state:", input.UserInputState.Name);
}
}}
/>
);
}The underlying reconciler works at the Roblox instance level, not with a browser-style virtual DOM diff for all properties.
- Some computed values that change frequently, such as
UDim2calculations, may not diff as efficiently as a browser DOM update. - You may need to be more careful about property churn on hot UI elements.
- Readable React code does not automatically imply perfect property diff efficiency in Roblox.
- Memoize expensive derived values when possible.
- Avoid re-creating large
UDim2objects on every render when the values are identical. - Keep property updates stable and deterministic.
- Prefer a small number of frequently-updated elements rather than large dynamic trees.
function Panel({ width, height }: { width: number; height: number }) {
const size = new UDim2(0, width, 0, height);
return <frame Size={size} BackgroundColor3={Color3.fromRGB(20, 20, 30)} />;
}Transforms are not a CSS engine. They are mapped to Roblox properties such as UDim2, Rotation, and size adjustments.
- They do not compose like browser CSS transforms.
- The transform order can differ from CSS stacking behavior.
- Complex transform logic may need to be implemented manually with Roblox UI properties.
- Use
Position,Size, andRotationdirectly. - Prefer compositional layout and explicit UI structure over complex transform chains.
- For animation, model the final state as concrete values rather than trying to mimic CSS keyframes exactly.
function RotatedBadge() {
return (
<frame
Size={new UDim2(0, 160, 0, 60)}
Rotation={12}
BackgroundColor3={Color3.fromRGB(255, 140, 0)}
>
<textlabel
Size={new UDim2(1, 0, 1, 0)}
Text="Featured"
TextColor3={Color3.fromRGB(255, 255, 255)}
BackgroundTransparency={1}
/>
</frame>
);
}Roblox uses UIGradient rather than the browser's CSS gradient engine.
- Only linear gradients are supported in practice.
- Complex CSS gradients such as repeating, conic, or radial gradients are not modeled the same way.
- Some gradient behavior is Roblox-specific and may require property tuning.
- Use
UIGradientand keep the gradient simple. - Make the gradient a small number of stop points rather than trying to replicate many browser gradient styles.
function GradientPanel() {
return (
<frame
Size={new UDim2(0, 260, 0, 120)}
BackgroundColor3={Color3.fromRGB(60, 60, 80)}
>
<uiGradient
Rotation={90}
Color={ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(79, 70, 229)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(14, 165, 233)),
})}
/>
</frame>
);
}Animations are implemented using Roblox's runtime and scheduling model.
- Spring-like animations can use
RunService.Heartbeat, which can affect performance if you animate many elements at once. - Tailwind animation utilities such as
animate-pulseandanimate-spinare simple tween-based patterns, not browser CSS animations. - A large number of simultaneous animations can be expensive in Roblox UI.
- Keep the number of simultaneously animated elements low.
- Prefer targeted animation for active elements instead of animating every component in a large tree.
- Use
TweenServiceor small custom spring logic for important transitions only.
function AnimatedButton() {
const [hovered, setHovered] = useState(false);
return (
<textbutton
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
Size={new UDim2(0, 160, 0, 42)}
BackgroundColor3={hovered ? Color3.fromRGB(59, 130, 246) : Color3.fromRGB(37, 99, 235)}
>
Hover me
</textbutton>
);
}FormData is only a lightweight polyfill and does not model browser form semantics fully.
- It only supports text serialization.
- No file uploads.
- No multipart form encoding.
- You should not rely on web form paths that expect browser file upload flows.
- Serialize form values into plain strings or JSON when sending to a backend.
- For file uploads, use a custom Roblox-native file picker or a server-side upload flow.
- Keep form data model simple and explicit.
function UserForm() {
const [name, setName] = useState("Guest");
function submit() {
const payload = {
name,
};
print("Submit payload:", payload);
}
return (
<textbox Text={name} onInputChanged={(rbx, input) => setName(rbx.Text)} />
);
}@nrbx/react-devtools connects to the standalone React DevTools server (the Node process you run locally), not to the browser extension.
- The bundled backend speaks the standalone DevTools wire protocol from the React 17-era codebase, so the latest DevTools frontend may not support every panel or feature.
- Component tree, prop, and hook inspection work; profiling and some newer panels can be unreliable.
- Run the standalone DevTools server version that pairs with the bundled backend if a panel misbehaves.
- Fall back to Roblox Studio's object tree and
printstatements when DevTools does not surface what you need. - Add debug views and debug overlays to your UI when you need to inspect state.
- Log props and instance trees during development.
useSyncExternalStore is implemented as a lightweight wrapper around useState + useEffect, not the full React 18/19 external-store fiber semantics.
- It is not a complete browser-like subscription system.
-
getServerSnapshotis ignored because there is no SSR in Roblox. - You should not assume the same semantics as web React when integrating with external mutable stores.
- Use your own state + effect subscription patterns for Roblox data sources.
- Create a custom hook wrapper that listens to your store and returns derived state.
function useRobloxStore<T>(source: T, selector: (value: T) => unknown) {
const [value, setValue] = useState(() => selector(source));
useEffect(() => {
setValue(selector(source));
}, [source, selector]);
return value;
}StrictMode is effectively a no-op wrapper in this runtime.
- There is no double-rendering in development.
- You cannot rely on StrictMode-specific warning behavior.
- Side effects that are intentionally double-invoked in browser React will not behave the same way.
- Keep render logic pure and idempotent.
- Place side effects in
useEffectand guard them when necessary. - Avoid assumptions that development mode will mount/unmount twice.
There is no React Server Components model in the Roblox runtime.
- No RSC.
- Everything runs on the client side.
- Data fetching and rendering patterns that rely on server component boundaries are not available.
- Fetch data from remote endpoints or Roblox services in
useEffect. - Use client-side state, service adapters, or remote event/message patterns.
- Keep UI and data-loading logic explicit.
Roblox UI instances have a real memory cost.
- Large React trees with many GUI instances can become heavy.
- Frequent creation and destruction of UI elements can create churn.
- Complex UI with many nested elements can be more expensive than a browser DOM tree of similar complexity.
- Reuse UI instead of recreating it when possible.
- Use
ObjectPool-style patterns for frequently created/destroyed UIs. - Hide or recycle panels instead of mounting and unmounting large trees repeatedly.
- Keep deeply nested UIs minimal and intentionally structured.
The runtime expects roblox-ts 3.x+ and compiles to Lua.
- Build output is Lua, not browser JS.
- Source maps may not be perfectly accurate for debugging.
- Some tooling assumptions from browser React projects do not carry over directly.
- Keep the project on supported
roblox-tsversions. - Favor explicit runtime logging and simple debug panels when investigating issues.
- Expect a smaller subset of browser tooling to map cleanly to Roblox.
Refs are more Roblox-native than web React refs in a few important ways.
-
createRef()returns{ current: T | undefined }. -
useRef()returns{ current: T }and is mutable. - Roblox instances often require explicit lookup steps such as
FindFirstChildor a parent-child traversal pattern when the ref points to a child instance created by React.
- Treat refs as instance handles, not as DOM nodes with browser semantics.
- If React creates a nested instance and you need to reach it, keep a stable parent reference and query child instances explicitly.
- Use refs for values or instance handles, but be intentional about the object graph.
import React, { useRef } from "@nrbx/react";
function Toolbar() {
const panelRef = useRef<Frame>();
function focusPanel() {
const panel = panelRef.current;
if (panel) {
const child = panel.FindFirstChild("Title") as TextLabel | undefined;
if (child) {
print("Found child:", child.Text);
}
}
}
return (
<frame ref={panelRef} Name="Toolbar" Size={new UDim2(0, 200, 0, 60)}>
<textlabel Name="Title" Text="Toolbar" BackgroundTransparency={1} />
</frame>
);
}@nrbx/react is a useful and ergonomic way to build Roblox UIs with a React-like mental model, but it is not a drop-in replacement for browser React 18/19. The runtime is intentionally simpler and more Roblox-native, which means some features are intentionally missing or behave differently.
The best way to get the most out of it is to design around Roblox's actual GUI system:
- prefer explicit Roblox properties over browser-like abstractions
- keep state updates simple and deterministic
- use explicit text and event handling where the platform differs
- treat UI performance and instance count as first-class concerns
If you know these limitations up front, you can write clearer, more stable Roblox UI code without pretending the runtime is a browser React implementation.