FiveM NUI com React e Vite: setup, callbacks e desenvolvimento no navegador

Construa uma NUI FiveM com React e Vite: ui_page e files em fxmanifest, base './', SendNUIMessage, RegisterNUICallback, SetNuiFocus e desenvolvendo no navegador.

HTML simples funciona para uma NUI pequena, mas uma interface real é mais fácil com React. A parte que faz as pessoas tropeçarem não é React, é a cola: o manifesto, os caminhos de saída da build, e como as mensagens vão e vêm.

Este artigo configura uma NUI React e Vite que carrega em FiveM, fala com Lua em ambas as direções, e pode ser construída em uma guia normal do navegador.

Layout do projeto

Coloque a interface em uma pasta web dentro do recurso e a crie com Vite:

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

O resultado:

text
my_script/
  fxmanifest.lua
  client/main.lua
  web/
    package.json
    vite.config.js
    src/
    dist/        (created by the build)

vite.config: base './'

FiveM carrega sua página de um endereço nui://, não de uma raiz de servidor web. Os caminhos de ativos padrão do Vite começam com /, que depois apontam para lugar nenhum e deixam você com uma página em branco. Torne-os relativos:

js
// web/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

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

Então construa:

bash
npm run build

fxmanifest.lua

Aponte ui_page para o index.html construído e envie a pasta dist inteira para o jogador com files:

lua
fx_version 'cerulean'
game 'gta5'

ui_page 'web/dist/index.html'

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

client_script 'client/main.lua'

Apenas os arquivos listados em files são enviados aos jogadores. Se uma imagem ou fonte está faltando no jogo, o glob de arquivo não o incluiu. Mantenha node_modules e src fora da lista: os jogadores não precisam deles.

Lua para a UI: SendNUIMessage

O script do cliente envia uma mensagem e dá foco à interface:

lua
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) dá o teclado e mouse para a página. Sempre o devolva, ou o cursor fica preso: veja NUI focus stuck cursor.

Em React, ouça a mensagem:

jsx
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>
}

Uma página que não renderiza nada quando escondida é o padrão usual, já que a página NUI em si permanece carregada.

UI para Lua: fetch e RegisterNUICallback

Para a outra direção, a página POST para um URL em seu recurso. GetParentResourceName() é uma função que FiveM fornece à página e retorna o nome do recurso:

js
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()
}
jsx
<button onClick={() => nuiPost('close')}>Close</button>

Em Lua, manipule-o. O callback deve ser chamado, ou a solicitação fica pendente:

lua
RegisterNUICallback('close', function(data, cb)
    setOpen(false)   -- gives focus back
    cb({ ok = true })
end)

data é o corpo JSON decodificado e cb responde o fetch. Qualquer coisa que você passar para cb volta como a resposta.

Desenvolva no navegador

Reconstruir e reiniciar o recurso para cada mudança de CSS é lento. Execute Vite e trabalhe em uma guia normal do navegador:

bash
npm run dev

Fora do jogo, window.GetParentResourceName não existe e ninguém envia mensagens para você, então simule ambas:

js
export const isBrowser = !window.invokeNative

// in main.jsx, in dev only
if (isBrowser) {
  setTimeout(() => {
    window.postMessage({ action: 'setVisible', visible: true, data: { name: 'Mic' } }, '*')
  }, 500)
}

A função nuiPost acima já volta a um nome de recurso padrão. No navegador, a solicitação simplesmente falha, então capture-a ou pule a chamada quando isBrowser é verdadeiro. Use um fundo escuro ou quadriculado em dev, já que o jogo real mostra o mundo atrás da página.

Iterando dentro do jogo

Para uma verificação final no jogo, construa e reinicie apenas o recurso:

bash
npm run build

então ensure my_script no console do servidor. Com vite build --watch, os arquivos reconstruem toda vez que você salva, e você apenas reinicia o recurso.

Problemas comuns

  • Página em branco. Faltando base: './', ou files não inclui web/dist/**.
  • Nada acontece ao clicar. O nome RegisterNUICallback não corresponde à URL de fetch, ou cb nunca é chamado.
  • Cursor travado após fechar. Faltando SetNuiFocus(false, false) em cada caminho de fechamento, incluindo um reinício de recurso.
  • Fontes e imagens faltando no jogo. Eles são referenciados por caminho absoluto, ou não estão em dist.
  • Mensagem nunca chega. A página não foi carregada ainda, ou os nomes de action diferem entre Lua e o ouvinte.

Lista de verificação

Sintoma Solução
UI em branco no jogo, funciona no navegador base: './' em vite.config, depois npm run build
Arquivos faltando para jogadores files { 'web/dist/**' } em fxmanifest.lua
Botão não faz nada Corresponda o nome RegisterNUICallback e chame cb
Não consigo alcançar o jogo da página fetch para https://${GetParentResourceName()}/name
Cursor não volta SetNuiFocus(false, false) ao fechar
Desenvolvendo sem o jogo Execute vite, simule postMessage e GetParentResourceName

Respostas rápidas

Por que minha NUI React é uma página em branco em FiveM?

Geralmente a build usa caminhos de ativos absolutos. Defina base: './' em vite.config, reconstrua, e certifique-se de que files em fxmanifest.lua inclui a pasta inteira web/dist.

Como a UI envia dados de volta ao jogo?

Com um fetch POST para https://${GetParentResourceName()}/eventName, manipulado em Lua por RegisterNUICallback('eventName', ...), que deve chamar seu callback.

Posso desenvolver a NUI sem iniciar FiveM?

Sim. Execute vite no navegador, detecte que você está fora do jogo, e envie mensagens falsas com window.postMessage para simular o que Lua enviaria.

Scripts sem esse problema

Mic PhoneUm celular dobrável que abre em tablet e chega ao celular de verdade do jogador.Ver script →Clothing DesignerCrie roupas dentro do FiveM — pincel, camadas, importação de imagens e IA — e vista na hora.Ver script →Arcade MachinesSete jogos de fliperama jogáveis em máquinas de verdade, com rankings e apostas.Ver script →

Continue lendo