Bridge & NUI Communication
Connect your React UI directly to FiveM game logic with two-way messaging, NUI callbacks, and phone window controls.
Communication Architecture
Plain text
┌────────────────────────────────────────────────────────┐
│ 5M_Phone │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Custom App Iframe (React / Vite UI) │ │
│ │ │ │
│ │ appFetch() ──────► NUI Callback ──► Lua Client │ │
│ │ onPhoneMessage() ◄ SendAppMessage ◄ Lua Client │ │
│ │ requestCloseApp() ─► Parent Window ─► Closes UI │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘1. Web UI Calling Lua (appFetch)
To fetch game data or trigger server actions from your React UI, use appFetch:
In React (App.tsx):
TypeScript
import { appFetch } from './lib/phoneBridge';
type PlayerSnapshot = {
ok: boolean;
name: string;
serverId: number;
coords: { x: number; y: number; z: number };
};
export function PlayerStatus() {
const [player, setPlayer] = useState<PlayerSnapshot | null>(null);
const loadData = async () => {
try {
const data = await appFetch<PlayerSnapshot>('getPlayerSnapshot');
setPlayer(data);
} catch (err) {
console.error('Failed to query player data:', err);
}
};
return (
<button onClick={loadData}>
Refresh Player Data
</button>
);
}In Lua (client/main.lua):
Lua
RegisterNUICallback('getPlayerSnapshot', function(data, cb)
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
cb({
ok = true,
name = GetPlayerName(PlayerId()) or 'Player',
serverId = GetPlayerServerId(PlayerId()),
coords = { x = coords.x, y = coords.y, z = coords.z },
})
end)2. Lua Pushing Live Events to UI (SendAppMessage)
To stream live data (speedometer, GPS compass heading, dispatch alerts, incoming transactions) from Lua into the open app:
In Lua (client/main.lua):
Lua
-- Push live compass heading every 200ms
CreateThread(function()
local lastHeading = nil
while true do
Wait(200)
local heading = math.floor(GetEntityHeading(PlayerPedId()) + 0.5)
if heading ~= lastHeading then
lastHeading = heading
pcall(function()
exports['5M_Phone']:SendAppMessage('finals-demo', 'heading', {
degrees = heading
})
end)
end
end
end)In React (App.tsx):
TypeScript
import { onPhoneMessage } from './lib/phoneBridge';
export function Compass() {
const [heading, setHeading] = useState<number>(0);
useEffect(() => {
const unsubscribe = onPhoneMessage((eventName, payload) => {
if (eventName === 'heading') {
const data = payload as { degrees: number };
setHeading(data.degrees);
}
});
return () => unsubscribe();
}, []);
return <div>Compass: {heading}°</div>;
}3. Returning to Home Screen (requestCloseApp)
When the user taps a "Back" or "Close" button inside your custom app, call requestCloseApp() to return to the phone home screen:
TypeScript
import { requestCloseApp } from './lib/phoneBridge';
export function Header() {
return (
<header className="flex items-center justify-between p-4">
<button
onClick={requestCloseApp}
className="text-sm font-semibold text-blue-500 hover:text-blue-400"
>
‹ Home
</button>
<h1 className="font-bold">My Custom App</h1>
</header>
);
}The helper sends a postMessage { source: '5M_DeveloperApp', type: 'close', appId } to the parent window, smoothly transitioning the phone UI back to the app grid.
