fivem-nui-react 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mustafa Burak Güneş
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -10,27 +10,46 @@ npm install fivem-nui-react
10
10
 
11
11
  ## Features
12
12
 
13
- - **useNuiEvent** - Listen for NUI messages from Lua
14
- - **fetchNui** - Send requests to Lua client and receive responses
15
- - **useNuiCallback** - Hook for NUI requests with loading/error states
16
- - **isEnvBrowser** - Check if running in browser (debug mode)
17
- - **Mock data support** - Test your UI in browser with simulated responses
13
+ ```tsx
14
+ // Listen for NUI messages from FiveM client
15
+ useNuiEvent("eventName", (data) => {});
18
16
 
19
- ## API Reference
17
+ // Send request and get response
18
+ const data = await fetchNui("eventName", { payload });
20
19
 
21
- ### useNuiEvent
20
+ // Hook with loading/error states and callback
21
+ const [fetch, { loading, error }] = useNuiCallback("eventName", (data) => {});
22
22
 
23
- Listen for NUI messages sent from Lua.
23
+ // Send data without expecting response
24
+ const [send, { loading, error }] = useSendNui("eventName");
24
25
 
25
- ```tsx
26
- useNuiEvent<T>(action: string, handler: (data: T) => void, options?: UseNuiEventOptions<T>): void
26
+ // Check if running in browser (debug mode)
27
+ if (isEnvBrowser())
27
28
  ```
28
29
 
29
- **Parameters:**
30
+ ```lua
31
+ -- Send message from FiveM client to UI
32
+ SendNUIMessage({ action = "eventName", data = { key = "value" } })
30
33
 
31
- - `action` - The action name to listen for
32
- - `handler` - Callback function executed when message is received
33
- - `options` - Optional mock data configuration for browser testing
34
+ -- Register callback for UI requests
35
+ RegisterNUICallback("eventName", function(data, cb)
36
+ cb({ response = "data" })
37
+ end)
38
+ ```
39
+
40
+ ## API
41
+
42
+ ### useNuiEvent
43
+
44
+ Listen for NUI messages sent from FiveM client.
45
+
46
+ ```tsx
47
+ useNuiEvent<T>(
48
+ action: string,
49
+ handler: (data: T) => void,
50
+ options?: { mockData?: T; mockDelay?: number }
51
+ ): void
52
+ ```
34
53
 
35
54
  **Example:**
36
55
 
@@ -40,11 +59,6 @@ import { useNuiEvent } from "fivem-nui-react";
40
59
  function App() {
41
60
  const [visible, setVisible] = useState(false);
42
61
 
43
- useNuiEvent<{ show: boolean }>("toggleUI", (data) => {
44
- setVisible(data.show);
45
- });
46
-
47
- // With mock data for browser testing
48
62
  useNuiEvent<{ show: boolean }>("toggleUI", (data) => setVisible(data.show), {
49
63
  mockData: { show: true },
50
64
  mockDelay: 1000,
@@ -54,7 +68,7 @@ function App() {
54
68
  }
55
69
  ```
56
70
 
57
- **Fivem client side:**
71
+ **FiveM Client (Lua):**
58
72
 
59
73
  ```lua
60
74
  SendNUIMessage({
@@ -67,53 +81,53 @@ SendNUIMessage({
67
81
 
68
82
  ### fetchNui
69
83
 
70
- Send a request to the Fivem client and receive a response.
84
+ Send a request to the FiveM client and receive a response.
71
85
 
72
86
  ```tsx
73
- fetchNui<T, D>(eventName: string, data?: D, options?: FetchNuiOptions<T>): Promise<T>
87
+ fetchNui<T, D>(
88
+ eventName: string,
89
+ data?: D,
90
+ options?: { mockData?: T; mockDelay?: number; signal?: AbortSignal }
91
+ ): Promise<T>
74
92
  ```
75
93
 
76
- **Parameters:**
77
-
78
- - `eventName` - The NUI callback event name
79
- - `data` - Data to send to Lua
80
- - `options` - Optional mock data configuration for browser testing
81
-
82
94
  **Example:**
83
95
 
84
96
  ```tsx
85
97
  import { fetchNui } from "fivem-nui-react";
86
98
 
87
- interface PlayerData {
88
- name: string;
89
- level: number;
90
- }
99
+ const player = await fetchNui<PlayerData>(
100
+ "getPlayerData",
101
+ { id: 1 },
102
+ {
103
+ mockData: { name: "John", level: 10 },
104
+ mockDelay: 500,
105
+ },
106
+ );
107
+ ```
91
108
 
92
- async function getPlayer() {
93
- const player = await fetchNui<PlayerData>("getPlayerData", { id: 1 });
94
- console.log(player.name, player.level);
95
- }
109
+ **With AbortController:**
96
110
 
97
- // With mock data for browser testing
98
- async function getPlayerMocked() {
99
- const player = await fetchNui<PlayerData>(
100
- "getPlayerData",
101
- { id: 1 },
102
- {
103
- mockData: { name: "John", level: 10 },
104
- mockDelay: 500,
105
- },
106
- );
107
- console.log(player.name, player.level);
108
- }
111
+ ```tsx
112
+ const controller = new AbortController();
113
+
114
+ const player = await fetchNui<PlayerData>(
115
+ "getPlayerData",
116
+ { id: 1 },
117
+ {
118
+ signal: controller.signal,
119
+ },
120
+ );
121
+
122
+ // Cancel the request
123
+ controller.abort();
109
124
  ```
110
125
 
111
- **Lua side:**
126
+ **FiveM Client (Lua):**
112
127
 
113
128
  ```lua
114
129
  RegisterNUICallback("getPlayerData", function(data, cb)
115
130
  local playerId = data.id
116
- -- Fetch player data...
117
131
  cb({ name = "John", level = 10 })
118
132
  end)
119
133
  ```
@@ -122,48 +136,28 @@ end)
122
136
 
123
137
  ### useNuiCallback
124
138
 
125
- A hook that wraps `fetchNui` with loading and error state management.
139
+ Hook for NUI requests with loading/error states. Calls the callback with response data.
126
140
 
127
141
  ```tsx
128
142
  useNuiCallback<T, D>(
129
143
  eventName: string,
130
144
  callback: (data: T) => void,
131
- options?: UseNuiCallbackOptions<T>
132
- ): [fetchFn: (data?: D) => Promise<T>, state: { loading: boolean, error: Error | null }]
145
+ options?: { mockData?: T; mockDelay?: number }
146
+ ): [(data?: D) => Promise<T>, { loading: boolean; error: Error | null }]
133
147
  ```
134
148
 
135
- **Parameters:**
136
-
137
- - `eventName` - The NUI callback event name
138
- - `callback` - Callback function executed when response is received
139
- - `options` - Optional mock data configuration for browser testing
140
-
141
- **Returns:**
142
-
143
- - `fetchFn` - Function to trigger the request
144
- - `state.loading` - Boolean indicating if request is in progress
145
- - `state.error` - Error object if request failed
146
-
147
149
  **Example:**
148
150
 
149
151
  ```tsx
150
152
  import { useNuiCallback } from "fivem-nui-react";
151
153
 
152
- interface PlayerData {
153
- name: string;
154
- level: number;
155
- }
156
-
157
154
  function PlayerInfo() {
158
155
  const [player, setPlayer] = useState<PlayerData | null>(null);
159
156
 
160
157
  const [fetchPlayer, { loading, error }] = useNuiCallback<PlayerData>(
161
158
  "getPlayerData",
162
159
  (data) => setPlayer(data),
163
- {
164
- mockData: { name: "John", level: 10 },
165
- mockDelay: 500,
166
- },
160
+ { mockData: { name: "John", level: 10 }, mockDelay: 500 },
167
161
  );
168
162
 
169
163
  useEffect(() => {
@@ -172,96 +166,125 @@ function PlayerInfo() {
172
166
 
173
167
  if (loading) return <div>Loading...</div>;
174
168
  if (error) return <div>Error: {error.message}</div>;
175
- if (!player) return null;
176
169
 
177
- return (
178
- <div>
179
- <p>Name: {player.name}</p>
180
- <p>Level: {player.level}</p>
181
- </div>
182
- );
170
+ return <div>{player?.name}</div>;
183
171
  }
184
172
  ```
185
173
 
174
+ **FiveM Client (Lua):**
175
+
176
+ ```lua
177
+ RegisterNUICallback("getPlayerData", function(data, cb)
178
+ local playerId = data.id
179
+ cb({ name = "John", level = 10 })
180
+ end)
181
+ ```
182
+
186
183
  ---
187
184
 
188
- ### isEnvBrowser
185
+ ### useSendNui
189
186
 
190
- Check if running in browser (outside FiveM).
187
+ Hook for sending data to FiveM client without expecting a response.
191
188
 
192
189
  ```tsx
193
- isEnvBrowser(): boolean
190
+ useSendNui<D>(
191
+ eventName: string,
192
+ options?: { mockDelay?: number }
193
+ ): [(data?: D) => Promise<void>, { loading: boolean; error: Error | null }]
194
194
  ```
195
195
 
196
196
  **Example:**
197
197
 
198
+ ```tsx
199
+ import { useSendNui } from "fivem-nui-react";
200
+
201
+ function CloseButton() {
202
+ const [closeUI, { loading }] = useSendNui<{ reason: string }>("closeUI");
203
+
204
+ return (
205
+ <button
206
+ onClick={() => closeUI({ reason: "user_clicked" })}
207
+ disabled={loading}
208
+ >
209
+ Close
210
+ </button>
211
+ );
212
+ }
213
+ ```
214
+
215
+ **FiveM Client (Lua):**
216
+
217
+ ```lua
218
+ RegisterNUICallback("closeUI", function(data, cb)
219
+ SetNuiFocus(false, false)
220
+ cb("ok")
221
+ end)
222
+ ```
223
+
224
+ ---
225
+
226
+ ### isEnvBrowser
227
+
228
+ Check if running in browser (outside FiveM).
229
+
198
230
  ```tsx
199
231
  import { isEnvBrowser } from "fivem-nui-react";
200
232
 
201
233
  if (isEnvBrowser()) {
202
234
  console.log("Running in browser - debug mode");
203
- } else {
204
- console.log("Running in FiveM");
205
235
  }
206
236
  ```
207
237
 
208
238
  ---
209
239
 
210
- ## Browser Testing (Mock Data)
240
+ ## Browser Testing
211
241
 
212
- All hooks and functions support mock data for testing your UI in a regular browser without FiveM.
242
+ All hooks support mock data for testing in browser without FiveM:
213
243
 
214
- When `isEnvBrowser()` returns `true`:
244
+ | Option | Description | Default |
245
+ | ----------- | ------------------------------------------------------ | ------- |
246
+ | `mockData` | Data to return in browser mode | - |
247
+ | `mockDelay` | Delay in ms before returning | 500 |
248
+ | `signal` | AbortSignal for request cancellation (`fetchNui` only) | - |
215
249
 
216
- - `useNuiEvent` will trigger the handler with `mockData` after `mockDelay` milliseconds
217
- - `fetchNui` will return `mockData` after `mockDelay` milliseconds
218
- - `useNuiCallback` will call the callback with `mockData` after `mockDelay` milliseconds
250
+ When `isEnvBrowser()` is `true`:
219
251
 
220
- **Options:**
221
-
222
- - `mockData` - The data to return in browser mode
223
- - `mockDelay` - Delay in milliseconds before returning data (default: 500ms)
252
+ - `useNuiEvent` triggers handler with `mockData` after delay
253
+ - `fetchNui` returns `mockData` after delay
254
+ - `useNuiCallback` calls callback with `mockData` after delay
255
+ - `useSendNui` simulates delay only
224
256
 
225
257
  ---
226
258
 
227
- ## TypeScript Support
259
+ ## TypeScript
228
260
 
229
- All functions are fully typed with generics:
261
+ All functions are fully typed:
230
262
 
231
263
  ```tsx
232
- // Define your types
233
- interface ShowUIData {
234
- visible: boolean;
235
- title: string;
264
+ interface PlayerData {
265
+ name: string;
266
+ level: number;
236
267
  }
237
268
 
238
269
  interface PlayerRequest {
239
270
  id: number;
240
271
  }
241
272
 
242
- interface PlayerResponse {
243
- name: string;
244
- level: number;
245
- }
246
-
247
- // Use with type parameters
248
- useNuiEvent<ShowUIData>("showUI", (data) => {
249
- // data is typed as ShowUIData
250
- console.log(data.visible, data.title);
251
- });
252
-
253
- const player = await fetchNui<PlayerResponse, PlayerRequest>("getPlayer", {
273
+ // Typed response
274
+ const player = await fetchNui<PlayerData, PlayerRequest>("getPlayer", {
254
275
  id: 1,
255
276
  });
256
- // player is typed as PlayerResponse
257
277
 
258
- const [fetchPlayer, { loading }] = useNuiCallback<
259
- PlayerResponse,
260
- PlayerRequest
261
- >("getPlayer", (data) => console.log(data.name));
278
+ // Typed callback
279
+ const [fetchPlayer, { loading }] = useNuiCallback<PlayerData, PlayerRequest>(
280
+ "getPlayer",
281
+ (data) => console.log(data.name),
282
+ );
262
283
  ```
263
284
 
264
- ---
285
+ ## Note
286
+
287
+ Most of the code in this project was written by AI and then manually reviewed and edited.
265
288
 
266
289
  ## License
267
290
 
package/dist/index.d.mts CHANGED
@@ -10,6 +10,7 @@ interface UseNuiEventOptions<T = unknown> {
10
10
  interface FetchNuiOptions<T = unknown> {
11
11
  mockData?: T;
12
12
  mockDelay?: number;
13
+ signal?: AbortSignal;
13
14
  }
14
15
  /**
15
16
  * Checks if running in browser debug mode
@@ -30,7 +31,7 @@ declare function useNuiEvent<T = unknown>(action: string, handler: NuiEventHandl
30
31
  * Sends a request to NUI callback (fivem client)
31
32
  * @param eventName - The callback event name
32
33
  * @param data - Data to send
33
- * @param options - Options for mock data in browser mode
34
+ * @param options - Options for mock data and abort signal in browser mode
34
35
  * @returns Promise<T> - Response from fivem client
35
36
  * @example
36
37
  * const result = await fetchNui("getPlayerData", { id: 1 }, {
@@ -70,5 +71,26 @@ type UseNuiCallbackReturn<T, D> = [
70
71
  * }, [fetchPlayer]);
71
72
  */
72
73
  declare function useNuiCallback<T = unknown, D = unknown>(eventName: string, callback: (data: T) => void, options?: UseNuiCallbackOptions<T>): UseNuiCallbackReturn<T, D>;
74
+ interface UseSendNuiOptions {
75
+ mockDelay?: number;
76
+ }
77
+ interface UseSendNuiState {
78
+ loading: boolean;
79
+ error: Error | null;
80
+ }
81
+ type UseSendNuiReturn<D> = [(data?: D) => Promise<void>, UseSendNuiState];
82
+ /**
83
+ * React hook for sending data to NUI callback without expecting a response
84
+ * @param eventName - The callback event name
85
+ * @param options - Options for mock delay in browser mode
86
+ * @returns [sendFunction, { loading, error }]
87
+ * @example
88
+ * const [closeUI, { loading }] = useSendNui("closeUI");
89
+ *
90
+ * const handleClose = () => {
91
+ * closeUI({ reason: "user_clicked" });
92
+ * };
93
+ */
94
+ declare function useSendNui<D = unknown>(eventName: string, options?: UseSendNuiOptions): UseSendNuiReturn<D>;
73
95
 
74
- export { type FetchNuiOptions, type NuiEventHandler, type NuiMessageEvent, type UseNuiCallbackOptions, type UseNuiCallbackReturn, type UseNuiCallbackState, type UseNuiEventOptions, fetchNui, isEnvBrowser, useNuiCallback, useNuiEvent };
96
+ export { type FetchNuiOptions, type NuiEventHandler, type NuiMessageEvent, type UseNuiCallbackOptions, type UseNuiCallbackReturn, type UseNuiCallbackState, type UseNuiEventOptions, type UseSendNuiOptions, type UseSendNuiReturn, type UseSendNuiState, fetchNui, isEnvBrowser, useNuiCallback, useNuiEvent, useSendNui };
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface UseNuiEventOptions<T = unknown> {
10
10
  interface FetchNuiOptions<T = unknown> {
11
11
  mockData?: T;
12
12
  mockDelay?: number;
13
+ signal?: AbortSignal;
13
14
  }
14
15
  /**
15
16
  * Checks if running in browser debug mode
@@ -30,7 +31,7 @@ declare function useNuiEvent<T = unknown>(action: string, handler: NuiEventHandl
30
31
  * Sends a request to NUI callback (fivem client)
31
32
  * @param eventName - The callback event name
32
33
  * @param data - Data to send
33
- * @param options - Options for mock data in browser mode
34
+ * @param options - Options for mock data and abort signal in browser mode
34
35
  * @returns Promise<T> - Response from fivem client
35
36
  * @example
36
37
  * const result = await fetchNui("getPlayerData", { id: 1 }, {
@@ -70,5 +71,26 @@ type UseNuiCallbackReturn<T, D> = [
70
71
  * }, [fetchPlayer]);
71
72
  */
72
73
  declare function useNuiCallback<T = unknown, D = unknown>(eventName: string, callback: (data: T) => void, options?: UseNuiCallbackOptions<T>): UseNuiCallbackReturn<T, D>;
74
+ interface UseSendNuiOptions {
75
+ mockDelay?: number;
76
+ }
77
+ interface UseSendNuiState {
78
+ loading: boolean;
79
+ error: Error | null;
80
+ }
81
+ type UseSendNuiReturn<D> = [(data?: D) => Promise<void>, UseSendNuiState];
82
+ /**
83
+ * React hook for sending data to NUI callback without expecting a response
84
+ * @param eventName - The callback event name
85
+ * @param options - Options for mock delay in browser mode
86
+ * @returns [sendFunction, { loading, error }]
87
+ * @example
88
+ * const [closeUI, { loading }] = useSendNui("closeUI");
89
+ *
90
+ * const handleClose = () => {
91
+ * closeUI({ reason: "user_clicked" });
92
+ * };
93
+ */
94
+ declare function useSendNui<D = unknown>(eventName: string, options?: UseSendNuiOptions): UseSendNuiReturn<D>;
73
95
 
74
- export { type FetchNuiOptions, type NuiEventHandler, type NuiMessageEvent, type UseNuiCallbackOptions, type UseNuiCallbackReturn, type UseNuiCallbackState, type UseNuiEventOptions, fetchNui, isEnvBrowser, useNuiCallback, useNuiEvent };
96
+ export { type FetchNuiOptions, type NuiEventHandler, type NuiMessageEvent, type UseNuiCallbackOptions, type UseNuiCallbackReturn, type UseNuiCallbackState, type UseNuiEventOptions, type UseSendNuiOptions, type UseSendNuiReturn, type UseSendNuiState, fetchNui, isEnvBrowser, useNuiCallback, useNuiEvent, useSendNui };
package/dist/index.js CHANGED
@@ -22,7 +22,8 @@ __export(index_exports, {
22
22
  fetchNui: () => fetchNui,
23
23
  isEnvBrowser: () => isEnvBrowser,
24
24
  useNuiCallback: () => useNuiCallback,
25
- useNuiEvent: () => useNuiEvent
25
+ useNuiEvent: () => useNuiEvent,
26
+ useSendNui: () => useSendNui
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
28
29
  var import_react = require("react");
@@ -65,9 +66,21 @@ async function fetchNui(eventName, data, options) {
65
66
  headers: {
66
67
  "Content-Type": "application/json; charset=UTF-8"
67
68
  },
68
- body: JSON.stringify(data ?? {})
69
+ body: JSON.stringify(data ?? {}),
70
+ signal: options == null ? void 0 : options.signal
69
71
  });
70
- return response.json();
72
+ if (!response.ok) {
73
+ throw new Error(`NUI request failed: ${eventName} (${response.status})`);
74
+ }
75
+ const text = await response.text();
76
+ if (!text || text.length === 0) {
77
+ return null;
78
+ }
79
+ try {
80
+ return JSON.parse(text);
81
+ } catch {
82
+ return null;
83
+ }
71
84
  }
72
85
  function useNuiCallback(eventName, callback, options) {
73
86
  const [state, setState] = (0, import_react.useState)({
@@ -75,20 +88,46 @@ function useNuiCallback(eventName, callback, options) {
75
88
  error: null
76
89
  });
77
90
  const callbackRef = (0, import_react.useRef)(callback);
91
+ const mountedRef = (0, import_react.useRef)(true);
92
+ const abortControllerRef = (0, import_react.useRef)(null);
78
93
  (0, import_react.useEffect)(() => {
79
94
  callbackRef.current = callback;
80
95
  }, [callback]);
96
+ (0, import_react.useEffect)(() => {
97
+ mountedRef.current = true;
98
+ return () => {
99
+ var _a;
100
+ mountedRef.current = false;
101
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
102
+ };
103
+ }, []);
81
104
  const fetch2 = (0, import_react.useCallback)(
82
105
  async (data) => {
83
- setState({ loading: true, error: null });
106
+ var _a;
107
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
108
+ const controller = new AbortController();
109
+ abortControllerRef.current = controller;
110
+ if (mountedRef.current) {
111
+ setState({ loading: true, error: null });
112
+ }
84
113
  try {
85
- const result = await fetchNui(eventName, data, options);
86
- callbackRef.current(result);
87
- setState({ loading: false, error: null });
114
+ const result = await fetchNui(eventName, data, {
115
+ ...options,
116
+ signal: controller.signal
117
+ });
118
+ if (!controller.signal.aborted && mountedRef.current) {
119
+ callbackRef.current(result);
120
+ setState({ loading: false, error: null });
121
+ }
88
122
  return result;
89
123
  } catch (err) {
124
+ if (controller.signal.aborted) {
125
+ throw err;
126
+ }
90
127
  const error = err instanceof Error ? err : new Error(String(err));
91
- setState({ loading: false, error });
128
+ if (mountedRef.current) {
129
+ setState({ loading: false, error });
130
+ }
92
131
  throw error;
93
132
  }
94
133
  },
@@ -96,10 +135,65 @@ function useNuiCallback(eventName, callback, options) {
96
135
  );
97
136
  return [fetch2, state];
98
137
  }
138
+ function useSendNui(eventName, options) {
139
+ const [state, setState] = (0, import_react.useState)({
140
+ loading: false,
141
+ error: null
142
+ });
143
+ const mountedRef = (0, import_react.useRef)(true);
144
+ const abortControllerRef = (0, import_react.useRef)(null);
145
+ (0, import_react.useEffect)(() => {
146
+ mountedRef.current = true;
147
+ return () => {
148
+ var _a;
149
+ mountedRef.current = false;
150
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
151
+ };
152
+ }, []);
153
+ const send = (0, import_react.useCallback)(
154
+ async (data) => {
155
+ var _a;
156
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
157
+ const controller = new AbortController();
158
+ abortControllerRef.current = controller;
159
+ if (mountedRef.current) {
160
+ setState({ loading: true, error: null });
161
+ }
162
+ try {
163
+ if (isEnvBrowser()) {
164
+ const delay = (options == null ? void 0 : options.mockDelay) ?? 500;
165
+ await new Promise((resolve) => setTimeout(resolve, delay));
166
+ } else {
167
+ await fetch(`https://${resourceName}/${eventName}`, {
168
+ method: "POST",
169
+ headers: {
170
+ "Content-Type": "application/json; charset=UTF-8"
171
+ },
172
+ body: JSON.stringify(data ?? {}),
173
+ signal: controller.signal
174
+ });
175
+ }
176
+ if (!controller.signal.aborted && mountedRef.current) {
177
+ setState({ loading: false, error: null });
178
+ }
179
+ } catch (err) {
180
+ if (controller.signal.aborted) return;
181
+ const error = err instanceof Error ? err : new Error(String(err));
182
+ if (mountedRef.current) {
183
+ setState({ loading: false, error });
184
+ }
185
+ throw error;
186
+ }
187
+ },
188
+ [eventName, options]
189
+ );
190
+ return [send, state];
191
+ }
99
192
  // Annotate the CommonJS export names for ESM import in node:
100
193
  0 && (module.exports = {
101
194
  fetchNui,
102
195
  isEnvBrowser,
103
196
  useNuiCallback,
104
- useNuiEvent
197
+ useNuiEvent,
198
+ useSendNui
105
199
  });
package/dist/index.mjs CHANGED
@@ -39,9 +39,21 @@ async function fetchNui(eventName, data, options) {
39
39
  headers: {
40
40
  "Content-Type": "application/json; charset=UTF-8"
41
41
  },
42
- body: JSON.stringify(data ?? {})
42
+ body: JSON.stringify(data ?? {}),
43
+ signal: options == null ? void 0 : options.signal
43
44
  });
44
- return response.json();
45
+ if (!response.ok) {
46
+ throw new Error(`NUI request failed: ${eventName} (${response.status})`);
47
+ }
48
+ const text = await response.text();
49
+ if (!text || text.length === 0) {
50
+ return null;
51
+ }
52
+ try {
53
+ return JSON.parse(text);
54
+ } catch {
55
+ return null;
56
+ }
45
57
  }
46
58
  function useNuiCallback(eventName, callback, options) {
47
59
  const [state, setState] = useState({
@@ -49,20 +61,46 @@ function useNuiCallback(eventName, callback, options) {
49
61
  error: null
50
62
  });
51
63
  const callbackRef = useRef(callback);
64
+ const mountedRef = useRef(true);
65
+ const abortControllerRef = useRef(null);
52
66
  useEffect(() => {
53
67
  callbackRef.current = callback;
54
68
  }, [callback]);
69
+ useEffect(() => {
70
+ mountedRef.current = true;
71
+ return () => {
72
+ var _a;
73
+ mountedRef.current = false;
74
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
75
+ };
76
+ }, []);
55
77
  const fetch2 = useCallback(
56
78
  async (data) => {
57
- setState({ loading: true, error: null });
79
+ var _a;
80
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
81
+ const controller = new AbortController();
82
+ abortControllerRef.current = controller;
83
+ if (mountedRef.current) {
84
+ setState({ loading: true, error: null });
85
+ }
58
86
  try {
59
- const result = await fetchNui(eventName, data, options);
60
- callbackRef.current(result);
61
- setState({ loading: false, error: null });
87
+ const result = await fetchNui(eventName, data, {
88
+ ...options,
89
+ signal: controller.signal
90
+ });
91
+ if (!controller.signal.aborted && mountedRef.current) {
92
+ callbackRef.current(result);
93
+ setState({ loading: false, error: null });
94
+ }
62
95
  return result;
63
96
  } catch (err) {
97
+ if (controller.signal.aborted) {
98
+ throw err;
99
+ }
64
100
  const error = err instanceof Error ? err : new Error(String(err));
65
- setState({ loading: false, error });
101
+ if (mountedRef.current) {
102
+ setState({ loading: false, error });
103
+ }
66
104
  throw error;
67
105
  }
68
106
  },
@@ -70,9 +108,64 @@ function useNuiCallback(eventName, callback, options) {
70
108
  );
71
109
  return [fetch2, state];
72
110
  }
111
+ function useSendNui(eventName, options) {
112
+ const [state, setState] = useState({
113
+ loading: false,
114
+ error: null
115
+ });
116
+ const mountedRef = useRef(true);
117
+ const abortControllerRef = useRef(null);
118
+ useEffect(() => {
119
+ mountedRef.current = true;
120
+ return () => {
121
+ var _a;
122
+ mountedRef.current = false;
123
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
124
+ };
125
+ }, []);
126
+ const send = useCallback(
127
+ async (data) => {
128
+ var _a;
129
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
130
+ const controller = new AbortController();
131
+ abortControllerRef.current = controller;
132
+ if (mountedRef.current) {
133
+ setState({ loading: true, error: null });
134
+ }
135
+ try {
136
+ if (isEnvBrowser()) {
137
+ const delay = (options == null ? void 0 : options.mockDelay) ?? 500;
138
+ await new Promise((resolve) => setTimeout(resolve, delay));
139
+ } else {
140
+ await fetch(`https://${resourceName}/${eventName}`, {
141
+ method: "POST",
142
+ headers: {
143
+ "Content-Type": "application/json; charset=UTF-8"
144
+ },
145
+ body: JSON.stringify(data ?? {}),
146
+ signal: controller.signal
147
+ });
148
+ }
149
+ if (!controller.signal.aborted && mountedRef.current) {
150
+ setState({ loading: false, error: null });
151
+ }
152
+ } catch (err) {
153
+ if (controller.signal.aborted) return;
154
+ const error = err instanceof Error ? err : new Error(String(err));
155
+ if (mountedRef.current) {
156
+ setState({ loading: false, error });
157
+ }
158
+ throw error;
159
+ }
160
+ },
161
+ [eventName, options]
162
+ );
163
+ return [send, state];
164
+ }
73
165
  export {
74
166
  fetchNui,
75
167
  isEnvBrowser,
76
168
  useNuiCallback,
77
- useNuiEvent
169
+ useNuiEvent,
170
+ useSendNui
78
171
  };
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "fivem-nui-react",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "React hooks and utilities for FiveM NUI development",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/TGIANN/fivem-nui-react.git"
8
+ },
9
+ "homepage": "https://github.com/TGIANN/fivem-nui-react#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/TGIANN/fivem-nui-react/issues"
12
+ },
5
13
  "main": "dist/index.js",
6
14
  "module": "dist/index.mjs",
7
15
  "types": "dist/index.d.ts",
@@ -26,7 +34,7 @@
26
34
  "hooks",
27
35
  "cfx"
28
36
  ],
29
- "author": "",
37
+ "author": "TGIANN (tgiann.com)",
30
38
  "license": "MIT",
31
39
  "type": "commonjs",
32
40
  "peerDependencies": {