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.