NUI en FiveM con React y Vite: configuración, callbacks y desarrollo en navegador
Construye una NUI en FiveM con React y Vite: ui_page y archivos en fxmanifest, base './', SendNUIMessage, RegisterNUICallback, SetNuiFocus y desarrollo en el navegador.
HTML simple funciona para una NUI pequeña, pero una interfaz real es más fácil con React. La parte que confunde a la gente no es React, es el pegamento: el manifiesto, las rutas de salida de compilación, y cómo van y vienen los mensajes.
Este artículo configura una NUI de React y Vite que se carga en FiveM, habla con Lua en ambas direcciones, y puede compilarse en una pestaña de navegador normal.
Diseño del proyecto
Pon la interfaz en una carpeta web dentro del recurso y créala con Vite:
cd my_script
npm create vite@latest web -- --template react
cd web
npm installEl resultado:
my_script/
fxmanifest.lua
client/main.lua
web/
package.json
vite.config.js
src/
dist/ (created by the build)vite.config: base './'
FiveM carga tu página desde una dirección nui://, no desde la raíz de un servidor web. Las rutas de activos predeterminadas de Vite comienzan con /, que luego apuntan a nada y te dejan con una página en blanco. Hazlas relativas:
// web/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: './',
build: { outDir: 'dist' },
})Luego compila:
npm run buildfxmanifest.lua
Apunta ui_page al index.html compilado y envía la carpeta dist completa al jugador con files:
fx_version 'cerulean'
game 'gta5'
ui_page 'web/dist/index.html'
files {
'web/dist/index.html',
'web/dist/**',
}
client_script 'client/main.lua'Solo los archivos listados en files se envían a los jugadores. Si una imagen o fuente falta en el juego, el glob de archivo no la incluyó. Mantén node_modules y src fuera de la lista: los jugadores no los necesitan.
Lua a la UI: SendNUIMessage
El script del cliente envía un mensaje y da foco a la interfaz:
local open = false
local function setOpen(state)
open = state
SetNuiFocus(state, state)
SendNUIMessage({ action = 'setVisible', visible = state, data = { name = 'Mic' } })
end
RegisterCommand('myui', function()
setOpen(not open)
end, false)SetNuiFocus(hasFocus, hasCursor) da el teclado y el ratón a la página. Siempre devuélvelo, o el cursor se queda atrapado: ver NUI focus cursor atrapado.
En React, escucha el mensaje:
import { useEffect, useState } from 'react'
export default function App() {
const [visible, setVisible] = useState(false)
const [name, setName] = useState('')
useEffect(() => {
const onMessage = (event) => {
const { action, visible, data } = event.data
if (action === 'setVisible') {
setVisible(visible)
if (data) setName(data.name)
}
}
window.addEventListener('message', onMessage)
return () => window.removeEventListener('message', onMessage)
}, [])
if (!visible) return null
return <div className="panel">Hello {name}</div>
}Una página que no renderiza nada cuando está oculta es el patrón usual, ya que la página NUI en sí permanece cargada.
UI a Lua: fetch y RegisterNUICallback
Para la otra dirección, la página POSTea a una URL en tu recurso. GetParentResourceName() es una función que FiveM proporciona a la página y devuelve el nombre del recurso:
export async function nuiPost(eventName, data = {}) {
const resource = window.GetParentResourceName ? window.GetParentResourceName() : 'my_script'
const resp = await fetch(`https://${resource}/${eventName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(data),
})
return resp.json()
}<button onClick={() => nuiPost('close')}>Close</button>En Lua, manéjalo. El callback debe ser llamado, o la solicitud se queda pendiente:
RegisterNUICallback('close', function(data, cb)
setOpen(false) -- gives focus back
cb({ ok = true })
end)data es el cuerpo JSON decodificado y cb responde al fetch. Cualquier cosa que pases a cb vuelve como la respuesta.
Desarrolla en el navegador
Recompilar y reiniciar el recurso para cada cambio de CSS es lento. Ejecuta Vite y trabaja en una pestaña de navegador normal:
npm run devFuera del juego, window.GetParentResourceName no existe y nadie te envía mensajes, así que simula ambos:
export const isBrowser = !window.invokeNative
// in main.jsx, in dev only
if (isBrowser) {
setTimeout(() => {
window.postMessage({ action: 'setVisible', visible: true, data: { name: 'Mic' } }, '*')
}, 500)
}La función nuiPost de arriba ya vuelve a un nombre de recurso predeterminado. En el navegador, la solicitud simplemente falla, así que atrápala u omite la llamada cuando isBrowser es verdadero. Usa un fondo oscuro o a cuadros en desarrollo, ya que el juego real muestra el mundo detrás de la página.
Iterando dentro del juego
Para una verificación final en el juego, compila y reinicia solo el recurso:
npm run buildluego ensure my_script en la consola del servidor. Con vite build --watch, los archivos se recompilan cada vez que guardas, y solo reinicia el recurso.
Problemas comunes
- Página en blanco. Faltante
base: './', ofilesno incluyeweb/dist/**. - Nada sucede al hacer clic. El nombre
RegisterNUICallbackno coincide con la URL de fetch, ocbnunca se llama. - Cursor atrapado después de cerrar. Faltante
SetNuiFocus(false, false)en cada ruta de cierre, incluyendo un reinicio de recurso. - Fuentes e imágenes faltantes en el juego. Se hace referencia a ellas por ruta absoluta, o no están en
dist. - El mensaje nunca llega. La página aún no se había cargado, o los nombres
actiondifieren entre Lua y el escucha.
Checklist
| Síntoma | Solución |
|---|---|
| UI en blanco en el juego, funciona en navegador | base: './' en vite.config, luego npm run build |
| Archivos faltantes para jugadores | files { 'web/dist/**' } en fxmanifest.lua |
| El botón no hace nada | Coincide el nombre RegisterNUICallback y llama cb |
| No puedo alcanzar el juego desde la página | fetch a https://${GetParentResourceName()}/name |
| El cursor no vuelve | SetNuiFocus(false, false) al cerrar |
| Desarrollando sin el juego | Ejecuta vite, simula postMessage y GetParentResourceName |
Respuestas rápidas
¿Por qué mi NUI de React es una página en blanco en FiveM?
Normalmente la compilación usa rutas de activos absolutas. Establece base: './' en vite.config, recompila, y asegúrate de que files en fxmanifest.lua incluye la carpeta web/dist completa.
¿Cómo envía la UI datos de vuelta al juego?
Con un POST de fetch a https://${GetParentResourceName()}/eventName, manejado en Lua por RegisterNUICallback('eventName', ...), que debe llamar su callback.
¿Puedo desarrollar la NUI sin iniciar FiveM?
Sí. Ejecuta vite en el navegador, detecta que estás fuera del juego, y envía mensajes falsos con window.postMessage para simular lo que Lua enviaría.
Scripts que evitan este problema
Mic PhoneUn móvil plegable que se abre en tablet y llega al móvil real del jugador.Ver script →
Clothing DesignerDiseña ropa dentro de FiveM — pincel, capas, imágenes e IA — y póntela.Ver script →
Arcade MachinesSiete juegos arcade jugables en máquinas reales, con clasificaciones y apuestas.Ver script →