Structuring a React NUI resource for FiveM

11 min read6 sections · 4 questions answered
Short answer

Keep the Lua in client/ and server/, the React app in web/ (Vite, TypeScript), and point ui_page at web/build/index.html with web/build/**/* in files. Use a useNuiEvent(action, handler) hook for Lua messages sent as { action, data }, a fetchNui helper for callbacks, debugData to fake Lua messages in the browser, and isEnvBrowser() (!window.invokeNative) to detect development. Build with base: './'.

Most modern FiveM UIs — phones, inventories, tablets — are React apps built with Vite. The community’s reference starting point is Project Error’s React + Lua boilerplate. Understanding its structure pays off even if you start from scratch, because the same four helpers show up in almost every serious NUI.

The layout

Resource structure
my_ui/
  fxmanifest.lua
  client/
    client.lua
    utils.lua          -- SendReactMessage helper
  server/
    server.lua
  web/
    index.html
    vite.config.ts     -- base: './', outDir: 'build'
    src/
      main.tsx
      components/App.tsx
      hooks/useNuiEvent.ts
      providers/VisibilityProvider.tsx
      utils/fetchNui.ts
      utils/debugData.ts
      utils/misc.ts      -- isEnvBrowser
    build/             -- generated
fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

ui_page 'web/build/index.html'

client_script 'client/**/*'
server_script 'server/**/*'

files {
    'web/build/index.html',
    'web/build/**/*',
}

Messages: Lua → React

client/utils.lua
function SendReactMessage(action, data)
    SendNUIMessage({ action = action, data = data })
end
components/App.tsx
import { useState } from 'react';
import { useNuiEvent } from '../hooks/useNuiEvent';

export default function App() {
  const [balance, setBalance] = useState(0);
  useNuiEvent<number>('setBalance', setBalance);
  return <div className="card">${balance}</div>;
}

useNuiEvent adds one message listener per action and removes it when the component unmounts. The { action, data } convention keeps routing simple as the UI grows. Basics: NUI basics.

Callbacks: React → Lua

utils/fetchNui.ts (simplified)
import { isEnvBrowser } from './misc';

export async function fetchNui<T = unknown>(event: string, data?: unknown, mock?: T): Promise<T> {
  if (isEnvBrowser() && mock !== undefined) return mock;
  const resource = (window as any).GetParentResourceName ? (window as any).GetParentResourceName() : 'nui-frame-app';
  const res = await fetch(`https://${resource}/${event}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=UTF-8' },
    body: JSON.stringify(data),
  });
  return res.json();
}

The Lua side registers each event with RegisterNuiCallback — details and pitfalls in NUI callbacks.

Developing in the browser

main.tsx — fake Lua messages in dev
import { debugData } from './utils/debugData';

debugData([
  { action: 'setVisible', data: true },
  { action: 'setBalance', data: 12500 },
]);

debugData only runs in development mode outside the game (isEnvBrowser() checks for window.invokeNative, which exists only inside FiveM). npm run dev gives hot reload in Chrome; npm run build produces web/build for the game.

Visibility and focus

A visibility provider listens for a setVisible action, renders nothing when hidden, and on Escape calls a hideFrame callback so Lua can release focus with SetNuiFocus(false, false). Focus rules: NUI focus and cursor.

Growing the structure

  • One folder per screen (features/bank, features/garage) with its own components and hooks.
  • A small store (Zustand, Jotai or context) for shared state; keep fast-changing values local.
  • Type your message payloads once and share them between useNuiEvent and fetchNui calls.
  • Keep the Lua side thin: validate on the server, send UI-ready data to React.

Performance habits for React UIs are in NUI performance.

Common questions

What is the best way to start a React NUI for FiveM?

Start from a React + Vite + TypeScript boilerplate such as Project Error’s, which includes useNuiEvent, fetchNui and browser debugging helpers.

How does React receive data from Lua?

Lua calls SendNUIMessage({ action, data }); a useNuiEvent(action, handler) hook passes data to your component.

How do I test the UI without the game?

Run the Vite dev server and use debugData to dispatch fake Lua messages; mock callback responses in fetchNui.

Why is my built React UI blank in game?

Usually absolute asset paths. Set Vite base: './' and make sure web/build/**/* is in files.

Want a loading screen you never have to debug?

Build it in the browser, export once, and edit it whenever — no HTML, no re-uploads.

Start free

More guides