← All posts FiveM

Cara Membuat NUI FiveM dengan React dan TypeScript

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.

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.

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.

Struktur resource

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

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:

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:

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

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:

// 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.

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

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

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 lalu meneruskan perubahannya ke NUI.

Langkah selanjutnya

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 berjalan di vDedicated AMD Ryzen dengan DDR5 dan NVMe di Singapore, lengkap dengan anti-DDoS. Bandingkan spesifikasinya di halaman Noctis Cloud.

start here

Building something with Noctis?

Tell us what you need. A real person on our team reads every message.