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:
| Pattern | Example | Description |
|---|---|---|
{module}:update | needs:update | State update |
{module}:show | notification:show | Show something |
{module}:hide | inventory:hide | Hide something |
{module}:toggle | menu:toggle | Toggle visibility |
{module}:{action} | banking:transfer | Specific 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
- Type your payloads - Share types between client and UI
- Use consistent naming - Follow the
{module}:{action}pattern - Handle errors - Wrap NUI callbacks in try/catch
- Debounce frequent events - Don't spam the UI
- Clean up subscriptions -
useNuiEventhandles this automatically - Keep payloads small - Only send necessary data