> ## Content Index
> Fetch the complete content index at: https://noctis.id/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# Cara Membuat NUI FiveM dengan React dan TypeScript
- URL: https://noctis.id/blog/fivem-nui-react-typescript/
- Published: 2026-10-18T21:36:24.000Z
- Updated: 2026-10-18T21:36:24.000Z
- Description: Bangun UI FiveM (NUI) modern dengan React, TypeScript, dan Vite: struktur resource, komunikasi Lua ke UI dan sebaliknya, fokus input, mode development di browser, sampai build untuk production.
- Author: Noctis
- Tags: FiveM, FiveM Development

Hampir semua antarmuka yang kamu lihat di server FiveM modern, seperti HUD, menu telepon, inventory, sampai layar pembuatan karakter, dibangun dengan **NUI**. NUI pada dasarnya adalah halaman web (HTML, CSS, JavaScript) yang dirender di atas game oleh browser Chromium bawaan FiveM.

Karena isinya halaman web, kamu bebas memakai framework frontend favorit. Di tutorial ini kita membangun NUI dengan **React + TypeScript + Vite**, kombinasi yang populer karena cepat saat development dan rapi untuk proyek besar. Kalau kamu belum nyaman dengan scripting Lua di FiveM, baca dulu [belajar Lua FiveM dari nol](https://noctis.id/blog/tutorial-lua-fivem/).

## Gambaran alur komunikasi

Sebelum menulis kode, pahami dulu tiga jalur komunikasinya:

1. **Lua → UI**: script client memanggil `SendNUIMessage(table)`. Di sisi browser, data itu diterima sebagai event `message` pada `window`.
2. **UI → Lua**: UI mengirim HTTP POST ke `https://nama-resource/nama-callback`. Script client menangkapnya dengan `RegisterNUICallback`.
3. **Fokus input**: secara default NUI tidak menerima klik atau keyboard. Script client harus memanggil `SetNuiFocus(hasFocus, hasCursor)`.

Perlu diingat, NUI hanya berbicara dengan **client script**. Kalau UI perlu data dari server (misalnya saldo bank), alurnya UI → client Lua → server → client Lua → UI. Detail API resminya ada di dokumentasi NUI di [docs.fivem.net](https://docs.fivem.net/docs/scripting-manual/nui-development/?ref=noctis.id).

## Struktur resource

Kita akan membuat resource bernama `noctis-menu` dengan struktur berikut:

```text
noctis-menu/
├── fxmanifest.lua
├── client.lua
└── web/
    ├── index.html
    ├── package.json
    ├── tsconfig.json
    ├── vite.config.ts
    └── src/
        ├── main.tsx
        ├── App.tsx
        └── nui.ts
```

Folder `web/` adalah proyek Vite biasa. Hasil build-nya masuk ke `web/dist/`, dan folder `dist` itulah yang dibaca FiveM.

## Langkah 1: Buat proyek Vite

Di dalam folder resource, jalankan:

```bash
npm create vite@latest web -- --template react-ts
cd web
npm install
```

Lalu ubah `vite.config.ts`. Hal terpenting di sini adalah `base: './'`, karena NUI memuat file dari path relatif resource, bukan dari root domain:

```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  base: './',
  build: {
    outDir: 'dist',
    emptyOutDir: true,
  },
});
```

Tanpa `base: './'`, hasil build akan mencari aset di `/assets/...` dan UI kamu tampil kosong di game, padahal normal di browser.

## Langkah 2: fxmanifest.lua

```lua
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

ui_page 'web/dist/index.html'

files {
    'web/dist/index.html',
    'web/dist/assets/*',
}

client_script 'client.lua'
```

Semua file yang dipakai UI wajib tercantum di `files`, kalau tidak, file itu tidak dikirim ke client. Wildcard `assets/*` memudahkan karena nama file hasil build Vite berisi hash yang berubah setiap build.

## Langkah 3: Helper komunikasi di TypeScript

Buat `src/nui.ts` untuk membungkus komunikasi agar komponen React tetap bersih:

```ts
// Ada di dalam game, tidak ada di browser biasa.
declare function GetParentResourceName(): string;

export const isBrowser = !(window as any).invokeNative;

export async function fetchNui<T = unknown>(event: string, data: unknown = {}): Promise<T> {
  if (isBrowser) {
    console.log('[mock] fetchNui', event, data);
    return {} as T;
  }
  const res = await fetch(`https://${GetParentResourceName()}/${event}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=UTF-8' },
    body: JSON.stringify(data),
  });
  return res.json();
}

export type NuiMessage<T = unknown> = { action: string; data: T };
```

Pengecekan `invokeNative` adalah trik umum di komunitas untuk membedakan apakah UI dibuka di dalam game atau di browser biasa saat development. Dengan begitu kamu bisa mengembangkan UI di `npm run dev` tanpa harus restart resource setiap mengubah CSS.

## Langkah 4: Komponen React

Contoh sederhana: menu yang muncul saat menerima pesan `open`, menampilkan nama pemain, dan bisa ditutup dengan tombol atau Escape.

```tsx
import { useEffect, useState } from 'react';
import { fetchNui, isBrowser, NuiMessage } from './nui';

type MenuData = { name: string; job: string };

export default function App() {
  const [visible, setVisible] = useState(isBrowser);
  const [info, setInfo] = useState<MenuData>({ name: 'Dev Mode', job: 'unemployed' });

  useEffect(() => {
    const onMessage = (e: MessageEvent<NuiMessage<MenuData>>) => {
      const { action, data } = e.data;
      if (action === 'open') { setInfo(data); setVisible(true); }
      if (action === 'close') setVisible(false);
    };
    const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') close(); };

    window.addEventListener('message', onMessage);
    window.addEventListener('keydown', onKey);
    return () => {
      window.removeEventListener('message', onMessage);
      window.removeEventListener('keydown', onKey);
    };
  }, []);

  function close() {
    setVisible(false);
    fetchNui('close');
  }

  if (!visible) return null;

  return (
    <div className="menu">
      <h1>Halo, {info.name}</h1>
      <p>Pekerjaan: {info.job}</p>
      <button onClick={() => fetchNui('claimPaycheck')}>Ambil gaji</button>
      <button onClick={close}>Tutup</button>
    </div>
  );
}
```

Satu hal penting soal CSS: **background halaman harus transparan**. Template Vite memberi warna background di `index.css`. Hapus itu dan pastikan `html, body { background: transparent; }`. Kalau tidak, layar game akan tertutup warna solid.

## Langkah 5: client.lua

```lua
local open = false

local function setOpen(state)
    open = state
    SetNuiFocus(state, state)
end

RegisterCommand('menu', function()
    if open then return end
    setOpen(true)
    SendNUIMessage({
        action = 'open',
        data = { name = GetPlayerName(PlayerId()), job = 'mechanic' },
    })
end, false)

RegisterNUICallback('close', function(_, cb)
    setOpen(false)
    cb({ ok = true })
end)

RegisterNUICallback('claimPaycheck', function(_, cb)
    -- Jangan proses uang di client. Teruskan ke server untuk divalidasi.
    TriggerServerEvent('noctis-menu:server:claimPaycheck')
    cb({ ok = true })
end)

AddEventHandler('onResourceStop', function(name)
    if name == GetCurrentResourceName() and open then
        SetNuiFocus(false, false)
    end
end)
```

Ada tiga kebiasaan baik di kode ini:

- **Selalu panggil `cb()`** di setiap `RegisterNUICallback`. Kalau lupa, promise `fetch` di UI tidak pernah selesai.
- **Lepaskan fokus saat resource berhenti.** Kalau resource di-restart ketika menu terbuka, pemain bisa terjebak dengan kursor aktif dan tidak bisa bergerak.
- **Logika penting tetap di server.** UI bisa dimanipulasi lewat devtools NUI, jadi anggap semua data dari UI tidak tepercaya.

## Langkah 6: Build dan jalankan

```bash
cd web
npm run build
```

Pastikan folder `web/dist` terisi, lalu tambahkan `ensure noctis-menu` di server.cfg dan restart resource. Di game, ketik `/menu`.

Saat deploy ke server production, kamu cukup mengunggah `fxmanifest.lua`, `client.lua`, dan `web/dist/`. Folder `node_modules` dan source TypeScript tidak perlu ada di server, dan sebaiknya memang tidak ada karena ukurannya besar dan bisa memperlambat proses scan resource.

## Debugging NUI

- **Devtools NUI**: FiveM menyediakan devtools Chromium untuk NUI. Di client, buka console F8 dan jalankan `nui_devtools` (atau lewat menu sesuai versi client). Di sana kamu bisa melihat error JavaScript dan request ke callback.
- **UI kosong di game tapi normal di browser**: hampir selalu masalah `base` di Vite atau file yang tidak tercantum di `files`.
- **Klik tidak berfungsi**: `SetNuiFocus` belum dipanggil dengan `true, true`.
- **Request callback gagal**: nama resource di URL salah. Gunakan `GetParentResourceName()`, jangan ditulis manual, agar tetap benar meski folder resource diganti namanya.

## Tips performa

NUI berjalan di proses browser terpisah, tapi tetap memakan resource di PC pemain. Beberapa praktik yang aman:

- Jangan render UI yang tidak terlihat. Return `null` seperti contoh di atas lebih ringan daripada menyembunyikannya dengan CSS.
- Untuk HUD yang update terus (misalnya kecepatan kendaraan), kirim data dengan interval wajar, bukan setiap frame dari loop `Wait(0)`.
- Kompres gambar dan hindari font atau library besar yang tidak terpakai. Ukuran bundle berpengaruh ke waktu loading pemain.
- Gunakan state management sederhana dulu (useState/useReducer). Library tambahan baru dipasang kalau memang dibutuhkan.

Kalau UI butuh data yang berubah dari banyak sumber, misalnya status pemain yang juga dibaca script lain, pertimbangkan menyimpan data itu di [state bags](https://noctis.id/blog/fivem-state-bags/) lalu meneruskan perubahannya ke NUI.

## Langkah selanjutnya

- Sambungkan tombol "Ambil gaji" ke sistem job sungguhan dengan mengikuti [tutorial job custom QBCore](https://noctis.id/blog/qbcore-custom-job/).
- Pakai komponen ox\_lib untuk notifikasi dan progress bar alih-alih membangunnya dari nol. Lihat [cara install ox\_lib](https://noctis.id/blog/install-ox-inventory-ox-lib-qbcore/).
- Kalau kamu baru mulai membangun kota, baca panduan pilar [cara membuat server FiveM sendiri dari nol](https://noctis.id/blog/cara-membuat-server-fivem/).

## Siap deploy ke server sungguhan?

Workflow build-lalu-upload seperti ini paling nyaman dijalankan di server dengan akses root penuh, supaya kamu bisa memasang Node.js, git, dan CI sendiri. Paket [hosting FiveM Noctis](https://noctis.id/id/fivem-hosting/?ref=noctis.id) berjalan di vDedicated AMD Ryzen dengan DDR5 dan NVMe di Singapore, lengkap dengan anti-DDoS. Bandingkan spesifikasinya di halaman [Noctis Cloud](https://noctis.id/id/cloud/?ref=noctis.id).