Skip to main content

Client/UI Communication

True Life uses NUI events for communication between the FiveM client and the React UI.

Sending Events from Client to UI

Use sendHudEvent from the client:

// src/modules/needs/needs.service.ts
"use client";

import { sendHudEvent } from "@core/hud";

export class NeedsService {
@ClientOnly
updateUI(needs: NeedsState): void {
sendHudEvent("needs:update", {
hunger: needs.hunger,
thirst: needs.thirst,
health: needs.health,
armor: needs.armor,
});
}
}

Receiving Events in UI

Use useNuiEvent hook in React components:

// src/modules/needs/huds/status/Panel.tsx
"use web";

import { useNuiEvent, useAppDispatch } from "@ui/hooks";
import { updateNeeds } from "@modules/needs/huds/status/state/slice";

interface NeedsPayload {
hunger: number;
thirst: number;
health: number;
armor: number;
}

export default function StatusPanel() {
const dispatch = useAppDispatch();

useNuiEvent<NeedsPayload>("needs:update", (data) => {
dispatch(updateNeeds(data));
});

// ... render UI
}

Sending Events from UI to Client

Use NUI callbacks (fetch to NUI resource):

// UI component
async function handleButtonClick() {
try {
const response = await fetch("https://true_life/action", {
method: "POST",
body: JSON.stringify({ action: "withdraw", amount: 100 }),
});
const result = await response.json();
console.log("Result:", result);
} catch (error) {
console.error("NUI callback failed:", error);
}
}

Register the callback on the client:

// src/runtime/client/main.ts
RegisterNuiCallbackType("action");
on("__cfx_nui:action", (data: unknown, cb: (response: unknown) => void) => {
// Handle the action
const result = processAction(data);
cb(result);
});

Event Naming Conventions

Use consistent naming patterns:

PatternExampleDescription
{module}:updateneeds:updateState update
{module}:shownotification:showShow something
{module}:hideinventory:hideHide something
{module}:togglemenu:toggleToggle visibility
{module}:{action}banking:transferSpecific action

Type-Safe Events

Define event payload types:

// src/modules/needs/types.ts
export interface NeedsUpdatePayload {
hunger: number;
thirst: number;
health: number;
armor: number;
}

export interface NotificationPayload {
message: string;
type: "info" | "success" | "warning" | "error";
duration?: number;
}

Use them in both client and UI:

// Client
import type { NeedsUpdatePayload } from "@modules/needs/types";

sendHudEvent("needs:update", {
hunger: 100,
thirst: 100,
health: 100,
armor: 0,
} satisfies NeedsUpdatePayload);
// UI
import type { NeedsUpdatePayload } from "@modules/needs/types";

useNuiEvent<NeedsUpdatePayload>("needs:update", (data) => {
dispatch(updateNeeds(data));
});

Page Open/Close Events

For full-screen pages, control visibility:

// Client - open a page
sendHudEvent("page:open", { page: "atm-interface" });

// Client - close current page
sendHudEvent("page:close", {});
// UI - PageShell listens for these events
useNuiEvent<{ page: string }>("page:open", ({ page }) => {
dispatch(openPage(page));
});

useNuiEvent("page:close", () => {
dispatch(closePage());
});

HUD Visibility

Control HUD visibility:

// Hide all HUDs
sendHudEvent("hud:hide", {});

// Show all HUDs
sendHudEvent("hud:show", {});

// Hide specific HUD
sendHudEvent("hud:hide", { id: "status" });

Complete Example

Client Service

// src/modules/notifications/notification.service.ts
"use client";

import { sendHudEvent } from "@core/hud";

interface NotificationData {
id: string;
message: string;
type: "info" | "success" | "warning" | "error";
duration: number;
}

export class NotificationService {
private notificationId = 0;

@ClientOnly
show(message: string, type: NotificationData["type"] = "info", duration = 5000): void {
const notification: NotificationData = {
id: `notif-${++this.notificationId}`,
message,
type,
duration,
};

sendHudEvent("notification:show", notification);
}

@ClientOnly
clear(): void {
sendHudEvent("notification:clear", {});
}
}

UI Component

// src/modules/notifications/huds/notifications/Panel.tsx
"use web";

import { useState, useRef, useEffect, useCallback } from "react";
import { useNuiEvent } from "@ui/hooks";
import { motion, AnimatePresence } from "motion/react";

interface Notification {
id: string;
message: string;
type: "info" | "success" | "warning" | "error";
duration: number;
}

export default function NotificationsPanel() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const timeoutIds = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());

// Clear a specific timeout by notification id
const clearNotificationTimeout = useCallback((id: string) => {
const timeoutId = timeoutIds.current.get(id);
if (timeoutId) {
clearTimeout(timeoutId);
timeoutIds.current.delete(id);
}
}, []);

// Clear all pending timeouts
const clearAllTimeouts = useCallback(() => {
for (const timeoutId of timeoutIds.current.values()) {
clearTimeout(timeoutId);
}
timeoutIds.current.clear();
}, []);

// Cleanup all timeouts on unmount
useEffect(() => {
return () => {
clearAllTimeouts();
};
}, [clearAllTimeouts]);

useNuiEvent<Notification>("notification:show", (notification) => {
setNotifications((prev) => [...prev, notification]);

// Auto-remove after duration, tracking the timeout ID
const timeoutId = setTimeout(() => {
timeoutIds.current.delete(notification.id);
setNotifications((prev) => prev.filter((n) => n.id !== notification.id));
}, notification.duration);

timeoutIds.current.set(notification.id, timeoutId);
});

useNuiEvent("notification:clear", () => {
clearAllTimeouts();
setNotifications([]);
});

return (
<div className="flex flex-col gap-2">
<AnimatePresence>
{notifications.map((notification) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
className={`rounded-lg p-4 ${getTypeStyles(notification.type)}`}
>
{notification.message}
</motion.div>
))}
</AnimatePresence>
</div>
);
}

function getTypeStyles(type: Notification["type"]): string {
const styles = {
info: "bg-blue-500/90 text-white",
success: "bg-green-500/90 text-white",
warning: "bg-yellow-500/90 text-black",
error: "bg-red-500/90 text-white",
};
return styles[type];
}

Best Practices

  1. Type your payloads - Share types between client and UI
  2. Use consistent naming - Follow the {module}:{action} pattern
  3. Handle errors - Wrap NUI callbacks in try/catch
  4. Debounce frequent events - Don't spam the UI
  5. Clean up subscriptions - useNuiEvent handles this automatically
  6. Keep payloads small - Only send necessary data