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:
- Lua → UI: script client memanggil
SendNUIMessage(table). Di sisi browser, data itu diterima sebagai eventmessagepadawindow. - UI → Lua: UI mengirim HTTP POST ke
https://nama-resource/nama-callback. Script client menangkapnya denganRegisterNUICallback. - 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.tsFolder 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 installLalu 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 setiapRegisterNUICallback. Kalau lupa, promisefetchdi 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 buildPastikan 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
basedi Vite atau file yang tidak tercantum difiles. - Klik tidak berfungsi:
SetNuiFocusbelum dipanggil dengantrue, 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
nullseperti 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
- Sambungkan tombol "Ambil gaji" ke sistem job sungguhan dengan mengikuti tutorial job custom QBCore.
- Pakai komponen ox_lib untuk notifikasi dan progress bar alih-alih membangunnya dari nol. Lihat cara install ox_lib.
- Kalau kamu baru mulai membangun kota, baca panduan pilar cara membuat server FiveM sendiri dari nol.
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.