# Structuring a React NUI resource for FiveM

> How a React + Vite + TypeScript NUI resource is organised: the Project Error boilerplate layout, useNuiEvent, fetchNui, debugData and isEnvBrowser, a visibility provider, the fxmanifest for web/build, dev vs production builds, and scaling the structure for bigger UIs.

Source: https://fam-tools.com/guides/fivem-nui-react-boilerplate-structure · FAM Tools · published 2026-09-11 · updated 2026-09-11 · about 11 min

**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: './'`.

## Key points

- Lua sends `{ action, data }`; React subscribes with `useNuiEvent`.
- `fetchNui` wraps callbacks; mock responses in the browser.
- `isEnvBrowser()` = `!window.invokeNative`.
- Vite `base: './'`, output to `web/build`.
- Only the built files ship — never `node_modules`.

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*

```text
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*

```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*

```lua
function SendReactMessage(action, data)
    SendNUIMessage({ action = action, data = data })
end
```

*components/App.tsx*

```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](https://fam-tools.com/guides/fivem-nui-basics).

## Callbacks: React → Lua

*utils/fetchNui.ts (simplified)*

```ts
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](https://fam-tools.com/guides/fivem-nui-callbacks-fetch).

## Developing in the browser

*main.tsx — fake Lua messages in dev*

```ts
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](https://fam-tools.com/guides/fivem-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](https://fam-tools.com/guides/fivem-nui-performance).

## 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`.

## Related

- https://fam-tools.com/guides/fivem-nui-basics
- https://fam-tools.com/guides/fivem-nui-callbacks-fetch
- https://fam-tools.com/guides/fivem-nui-assets-paths
- https://fam-tools.com/guides/fivem-loading-screen-react-vite
