games / fivem
NUI callbacks : communication JS vers Lua
Explication
Ce que vous allez apprendre
- Enregistrer un callback NUI côté Lua avec
RegisterNUICallback - Déclencher ce callback depuis JavaScript avec
fetch() - Comprendre pourquoi
cb(...)doit toujours être appelé, même avec un objet vide - Récupérer
GetParentResourceName()pour cibler la bonne ressource depuis le JS - Revalider côté serveur toute action métier déclenchée par un clic dans l'interface
Dans quel contexte ?
Un joueur clique sur "Acheter" dans le menu de boutique construit en leçon 6 : ce clic, purement visuel côté JavaScript, doit déclencher une vraie transaction côté Lua (retirer de l'argent, ajouter l'objet). Sans NUI callback, la page web resterait une simple façade sans aucun effet réel sur le jeu — cette leçon relie enfin l'interface à la logique du script.
| Élément | Rôle |
|---|---|
RegisterNUICallback(name, fn) | Déclare un point d'entrée Lua appelable depuis le JS |
fetch(...) | Déclenche ce callback depuis la page web |
cb(response) | Réponse OBLIGATOIRE, sinon le fetch reste bloqué |
Piège fréquent
Un RegisterNUICallback qui ne finit jamais par appeler cb(...) laisse la promesse JavaScript associée bloquée indéfiniment. Le symptôme typique côté joueur : l'interface semble "figée" après un clic, sans aucun message d'erreur visible pour comprendre pourquoi.
Le sens manquant de la communication NUI
La leçon précédente a montré comment le Lua envoie des données vers la page web (SendNUIMessage). Mais une interface interactive a aussi besoin du sens inverse : quand un joueur clique sur "Acheter" dans la page web, il faut que ce clic déclenche une action réelle côté Lua (retirer de l'argent, ajouter un objet). C'est le rôle des NUI callbacks.
Pourquoi passer par fetch()
JavaScript utilise ici fetch(), la fonction standard du web pour envoyer une requête HTTP — exactement comme un site web interrogerait un serveur classique. FiveM intercepte ces requêtes particulières (adressées à GetParentResourceName(), qui identifie la ressource courante) et les redirige vers le code Lua correspondant, enregistré au préalable avec RegisterNUICallback. Il n'y a donc pas de vraie requête réseau qui sort de la machine : c'est un mécanisme purement local entre la page et le script Lua qui l'héberge.
Le piège qui bloque tout : oublier de répondre
C'est le point le plus important à retenir de cette leçon. Le JavaScript, via fetch(), attend une réponse. Si le code Lua enregistré avec RegisterNUICallback ne répond jamais explicitement en appelant cb(...), la promesse JavaScript côté page reste bloquée indéfiniment en attente — un bug fréquent chez les débutants, qui se traduit par une interface qui semble "figée" après un clic, sans message d'erreur clair pour comprendre pourquoi.
Ne jamais faire confiance au clic
Un clic dans l'interface ne doit jamais, à lui seul, valider une action métier sensible. Exactement comme pour les events réseau classiques (vu en leçon 4 et détaillé dans la leçon Sécurité), le code déclenché par un callback NUI ne fait que relayer une intention côté serveur, qui reste seul juge de la validité réelle de l'action (prix, quantité, autorisations).
Commandes & code
NUI callbacks : communication JS vers Lua
La communication inverse (JS vers Lua) passe par des NUI callbacks, appelés via fetch().
-- CLIENT : enregistre un callback NUI appelable depuis le JS
RegisterNUICallback('buyItem', function(data, cb)
TriggerServerEvent('technologik:buyItem', data.itemName, 1)
cb({ status = 'ok' }) -- répondre est OBLIGATOIRE, sinon le fetch JS reste bloqué en attente
end)
RegisterNUICallback('closeShop', function(data, cb)
SetNuiFocus(false, false) -- rend le contrôle au jeu
cb({})
end)// html/ui.js
function buyItem(itemName) {
fetch(`https://${GetParentResourceName()}/buyItem`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify({ itemName }),
})
.then(resp => resp.json())
.then(result => {
if (result.status === 'ok') {
console.log('Achat confirmé');
}
})
.catch(err => console.error('Erreur NUI callback:', err));
}
document.getElementById('close-btn').addEventListener('click', () => {
fetch(`https://${GetParentResourceName()}/closeShop`, {
method: 'POST',
body: JSON.stringify({}),
});
document.getElementById('shop').classList.add('hidden');
});
// Fermer avec Échap, comme la plupart des menus FiveM
window.addEventListener('keyup', (e) => {
if (e.key === 'Escape') {
document.getElementById('close-btn').click();
}
});// html/ui.js : brancher les clics sur les items dynamiquement générés
function renderShopItems(items) {
const list = document.getElementById('shop-items');
list.innerHTML = '';
items.forEach(item => {
const li = document.createElement('li');
li.textContent = `${item.name} - $${item.price}`;
li.addEventListener('click', () => buyItem(item.name));
list.appendChild(li);
});
}Résumé
GetParentResourceName()retourne le nom de la ressource, utilisé comme host dans lefetch.- Un
RegisterNUICallbackDOIT appelercb(...), même avec un objet vide, sinon le JS reste en attente. - Toute action métier (achat, argent) doit être re-validée côté serveur, jamais décidée en JS.
Exercices pratiques
Mission : le clic 'Acheter' qui fige la boutique
Objectif : Corriger un callback NUI qui ne répond jamais et déplacer la logique d'achat côté serveur.
Contexte
Après avoir cliqué une seule fois sur "Acheter" dans le menu boutique, l'interface entière semble figée : plus aucun clic ne réagit, sans message d'erreur dans la console F8. Le code en cause :
RegisterNUICallback('buyItem', function(data, cb)
RemoveMoney(source, data.price)
AddItem(source, data.itemName, 1)
end)Le prix (data.price) est directement envoyé par le JavaScript, calculé côté page web.