@usefy/use-init 0.0.24

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/README.md ADDED
@@ -0,0 +1,610 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/geon0529/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">@usefy/use-init</h1>
6
+
7
+ <p align="center">
8
+ <strong>A powerful React hook for one-time initialization with async support, retry, timeout, and conditional execution</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@usefy/use-init">
13
+ <img src="https://img.shields.io/npm/v/@usefy/use-init.svg?style=flat-square&color=007acc" alt="npm version" />
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/@usefy/use-init">
16
+ <img src="https://img.shields.io/npm/dm/@usefy/use-init.svg?style=flat-square&color=007acc" alt="npm downloads" />
17
+ </a>
18
+ <a href="https://bundlephobia.com/package/@usefy/use-init">
19
+ <img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-init?style=flat-square&color=007acc" alt="bundle size" />
20
+ </a>
21
+ <a href="https://github.com/geon0529/usefy/blob/master/LICENSE">
22
+ <img src="https://img.shields.io/npm/l/@usefy/use-init.svg?style=flat-square&color=007acc" alt="license" />
23
+ </a>
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="#installation">Installation</a> •
28
+ <a href="#quick-start">Quick Start</a> •
29
+ <a href="#api-reference">API Reference</a> •
30
+ <a href="#examples">Examples</a> •
31
+ <a href="#license">License</a>
32
+ </p>
33
+
34
+ <p align="center">
35
+ <a href="https://geon0529.github.io/usefy/?path=/docs/hooks-useinit--docs" target="_blank" rel="noopener noreferrer">
36
+ <strong>📚 View Storybook Demo</strong>
37
+ </a>
38
+ </p>
39
+
40
+ ---
41
+
42
+ ## Overview
43
+
44
+ `@usefy/use-init` is a React hook for executing initialization logic exactly once when a component mounts. It supports synchronous and asynchronous callbacks, automatic retry on failure, timeout handling, conditional execution, and cleanup functions. Perfect for initializing services, loading configuration, setting up subscriptions, and any one-time setup tasks.
45
+
46
+ **Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
47
+
48
+ ### Why use-init?
49
+
50
+ - **Zero Dependencies** — Pure React implementation with no external dependencies
51
+ - **TypeScript First** — Full type safety with exported interfaces
52
+ - **One-Time Execution** — Guarantees initialization runs only once per mount
53
+ - **Async Support** — Handles both synchronous and asynchronous initialization callbacks
54
+ - **Cleanup Functions** — Optional cleanup function support for resource management
55
+ - **Retry Logic** — Automatic retry with configurable attempts and delays
56
+ - **Timeout Handling** — Built-in timeout support with custom error handling
57
+ - **Conditional Execution** — Run initialization only when conditions are met
58
+ - **State Tracking** — Track initialization status, loading state, and errors
59
+ - **Manual Reinitialize** — Trigger re-initialization programmatically
60
+ - **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
61
+ - **Well Tested** — Comprehensive test coverage with Vitest
62
+
63
+ ---
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ # npm
69
+ npm install @usefy/use-init
70
+
71
+ # yarn
72
+ yarn add @usefy/use-init
73
+
74
+ # pnpm
75
+ pnpm add @usefy/use-init
76
+ ```
77
+
78
+ ### Peer Dependencies
79
+
80
+ This package requires React 18 or 19:
81
+
82
+ ```json
83
+ {
84
+ "peerDependencies": {
85
+ "react": "^18.0.0 || ^19.0.0"
86
+ }
87
+ }
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Quick Start
93
+
94
+ ```tsx
95
+ import { useInit } from "@usefy/use-init";
96
+
97
+ function MyComponent() {
98
+ const { isInitialized, isInitializing, error } = useInit(async () => {
99
+ await loadConfiguration();
100
+ console.log("Component initialized!");
101
+ });
102
+
103
+ if (isInitializing) return <div>Loading...</div>;
104
+ if (error) return <div>Error: {error.message}</div>;
105
+ if (!isInitialized) return null;
106
+
107
+ return <div>Ready!</div>;
108
+ }
109
+ ```
110
+
111
+ ---
112
+
113
+ ## API Reference
114
+
115
+ ### `useInit(callback, options?)`
116
+
117
+ A hook that executes initialization logic exactly once when the component mounts (or when conditions are met).
118
+
119
+ #### Parameters
120
+
121
+ | Parameter | Type | Default | Description |
122
+ | ---------- | ---------------- | ------- | ---------------------------------------- |
123
+ | `callback` | `InitCallback` | — | The initialization function to run |
124
+ | `options` | `UseInitOptions` | `{}` | Configuration options for initialization |
125
+
126
+ #### Callback Type
127
+
128
+ The callback can be:
129
+
130
+ - **Synchronous**: `() => void`
131
+ - **Asynchronous**: `() => Promise<void>`
132
+ - **With cleanup**: `() => void | CleanupFn` or `() => Promise<void | CleanupFn>`
133
+
134
+ Where `CleanupFn` is `() => void` - a function that will be called when the component unmounts or before re-initialization.
135
+
136
+ #### Options
137
+
138
+ | Option | Type | Default | Description |
139
+ | ------------ | --------- | ------- | --------------------------------------------------- |
140
+ | `when` | `boolean` | `true` | Only run initialization when this condition is true |
141
+ | `retry` | `number` | `0` | Number of retry attempts on failure |
142
+ | `retryDelay` | `number` | `1000` | Delay between retry attempts in milliseconds |
143
+ | `timeout` | `number` | — | Timeout for initialization in milliseconds |
144
+
145
+ #### Returns `UseInitResult`
146
+
147
+ | Property | Type | Description |
148
+ | ---------------- | --------------- | -------------------------------------------------------------- |
149
+ | `isInitialized` | `boolean` | Whether initialization has completed successfully |
150
+ | `isInitializing` | `boolean` | Whether initialization is currently in progress |
151
+ | `error` | `Error \| null` | Error that occurred during initialization, if any |
152
+ | `reinitialize` | `() => void` | Manually trigger re-initialization (respects `when` condition) |
153
+
154
+ ---
155
+
156
+ ## Examples
157
+
158
+ ### Basic Synchronous Initialization
159
+
160
+ ```tsx
161
+ import { useInit } from "@usefy/use-init";
162
+
163
+ function BasicComponent() {
164
+ useInit(() => {
165
+ console.log("Component initialized!");
166
+ initializeAnalytics();
167
+ });
168
+
169
+ return <div>My Component</div>;
170
+ }
171
+ ```
172
+
173
+ ### Async Initialization with Status Tracking
174
+
175
+ ```tsx
176
+ import { useInit } from "@usefy/use-init";
177
+
178
+ function DataLoader() {
179
+ const [data, setData] = useState(null);
180
+ const { isInitialized, isInitializing, error } = useInit(async () => {
181
+ const response = await fetch("/api/data");
182
+ const result = await response.json();
183
+ setData(result);
184
+ });
185
+
186
+ if (isInitializing) return <div>Loading data...</div>;
187
+ if (error) return <div>Error: {error.message}</div>;
188
+ if (!isInitialized) return null;
189
+
190
+ return <div>{JSON.stringify(data)}</div>;
191
+ }
192
+ ```
193
+
194
+ ### With Cleanup Function
195
+
196
+ ```tsx
197
+ import { useInit } from "@usefy/use-init";
198
+
199
+ function SubscriptionComponent() {
200
+ useInit(() => {
201
+ const subscription = eventBus.subscribe("event", handleEvent);
202
+
203
+ // Return cleanup function
204
+ return () => {
205
+ subscription.unsubscribe();
206
+ };
207
+ });
208
+
209
+ return <div>Subscribed to events</div>;
210
+ }
211
+ ```
212
+
213
+ ### Conditional Initialization
214
+
215
+ ```tsx
216
+ import { useInit } from "@usefy/use-init";
217
+
218
+ function ConditionalComponent({ isEnabled }: { isEnabled: boolean }) {
219
+ const { isInitialized } = useInit(
220
+ () => {
221
+ initializeFeature();
222
+ },
223
+ { when: isEnabled }
224
+ );
225
+
226
+ if (!isEnabled) return <div>Feature disabled</div>;
227
+ if (!isInitialized) return <div>Initializing...</div>;
228
+
229
+ return <div>Feature ready!</div>;
230
+ }
231
+ ```
232
+
233
+ ### With Retry Logic
234
+
235
+ ```tsx
236
+ import { useInit } from "@usefy/use-init";
237
+
238
+ function ResilientComponent() {
239
+ const { isInitialized, error, reinitialize } = useInit(
240
+ async () => {
241
+ await connectToServer();
242
+ },
243
+ {
244
+ retry: 3,
245
+ retryDelay: 1000, // Wait 1 second between retries
246
+ }
247
+ );
248
+
249
+ if (error) {
250
+ return (
251
+ <div>
252
+ <p>Failed to connect: {error.message}</p>
253
+ <button onClick={reinitialize}>Retry</button>
254
+ </div>
255
+ );
256
+ }
257
+
258
+ if (!isInitialized) return <div>Connecting...</div>;
259
+
260
+ return <div>Connected!</div>;
261
+ }
262
+ ```
263
+
264
+ ### With Timeout
265
+
266
+ ```tsx
267
+ import { useInit } from "@usefy/use-init";
268
+
269
+ function TimeoutComponent() {
270
+ const { isInitialized, error } = useInit(
271
+ async () => {
272
+ await slowOperation();
273
+ },
274
+ {
275
+ timeout: 5000, // Fail after 5 seconds
276
+ }
277
+ );
278
+
279
+ if (error) {
280
+ return <div>Timeout: {error.message}</div>;
281
+ }
282
+
283
+ if (!isInitialized) return <div>Processing...</div>;
284
+
285
+ return <div>Completed!</div>;
286
+ }
287
+ ```
288
+
289
+ ### Combined Options: Retry + Timeout + Conditional
290
+
291
+ ```tsx
292
+ import { useInit } from "@usefy/use-init";
293
+
294
+ function AdvancedComponent({ shouldInit }: { shouldInit: boolean }) {
295
+ const { isInitialized, isInitializing, error, reinitialize } = useInit(
296
+ async () => {
297
+ await initializeService();
298
+ },
299
+ {
300
+ when: shouldInit,
301
+ retry: 2,
302
+ retryDelay: 2000,
303
+ timeout: 10000,
304
+ }
305
+ );
306
+
307
+ if (!shouldInit) return <div>Waiting for condition...</div>;
308
+ if (isInitializing) return <div>Initializing (attempt in progress)...</div>;
309
+ if (error) {
310
+ return (
311
+ <div>
312
+ <p>Error: {error.message}</p>
313
+ <button onClick={reinitialize}>Try Again</button>
314
+ </div>
315
+ );
316
+ }
317
+ if (!isInitialized) return <div>Not initialized</div>;
318
+
319
+ return <div>Service initialized successfully!</div>;
320
+ }
321
+ ```
322
+
323
+ ### Manual Re-initialization
324
+
325
+ ```tsx
326
+ import { useInit } from "@usefy/use-init";
327
+
328
+ function RefreshableComponent() {
329
+ const [refreshKey, setRefreshKey] = useState(0);
330
+ const { isInitialized, reinitialize } = useInit(async () => {
331
+ await loadData();
332
+ });
333
+
334
+ const handleRefresh = () => {
335
+ setRefreshKey((k) => k + 1);
336
+ reinitialize();
337
+ };
338
+
339
+ return (
340
+ <div>
341
+ <button onClick={handleRefresh}>Refresh Data</button>
342
+ {isInitialized && <div>Data loaded (key: {refreshKey})</div>}
343
+ </div>
344
+ );
345
+ }
346
+ ```
347
+
348
+ ### Async Cleanup Function
349
+
350
+ ```tsx
351
+ import { useInit } from "@usefy/use-init";
352
+
353
+ function AsyncCleanupComponent() {
354
+ useInit(async () => {
355
+ const connection = await establishConnection();
356
+
357
+ // Return async cleanup function
358
+ return async () => {
359
+ await connection.close();
360
+ console.log("Connection closed");
361
+ };
362
+ });
363
+
364
+ return <div>Connected</div>;
365
+ }
366
+ ```
367
+
368
+ ### Initializing Multiple Services
369
+
370
+ ```tsx
371
+ import { useInit } from "@usefy/use-init";
372
+
373
+ function MultiServiceComponent() {
374
+ const analytics = useInit(() => {
375
+ initializeAnalytics();
376
+ return () => analyticsService.shutdown();
377
+ });
378
+
379
+ const logging = useInit(async () => {
380
+ await initializeLogging();
381
+ return () => loggingService.disconnect();
382
+ });
383
+
384
+ const config = useInit(async () => {
385
+ const config = await loadConfig();
386
+ return config;
387
+ });
388
+
389
+ const allReady =
390
+ analytics.isInitialized && logging.isInitialized && config.isInitialized;
391
+
392
+ if (!allReady) return <div>Initializing services...</div>;
393
+
394
+ return <div>All services ready!</div>;
395
+ }
396
+ ```
397
+
398
+ ---
399
+
400
+ ## TypeScript
401
+
402
+ This hook is written in TypeScript with full type safety.
403
+
404
+ ```tsx
405
+ import {
406
+ useInit,
407
+ type UseInitOptions,
408
+ type UseInitResult,
409
+ } from "@usefy/use-init";
410
+
411
+ // Basic usage with type inference
412
+ const { isInitialized } = useInit(() => {
413
+ console.log("Init");
414
+ });
415
+
416
+ // With options
417
+ const options: UseInitOptions = {
418
+ when: true,
419
+ retry: 3,
420
+ retryDelay: 1000,
421
+ timeout: 5000,
422
+ };
423
+
424
+ const result: UseInitResult = useInit(async () => {
425
+ await initialize();
426
+ }, options);
427
+
428
+ // Cleanup function types
429
+ useInit(() => {
430
+ const resource = createResource();
431
+ return () => {
432
+ // TypeScript knows this is a cleanup function
433
+ resource.cleanup();
434
+ };
435
+ });
436
+ ```
437
+
438
+ ---
439
+
440
+ ## Behavior Details
441
+
442
+ ### One-Time Execution
443
+
444
+ The hook guarantees that initialization runs only once per component mount. Even if the `callback` reference changes, initialization will not run again unless:
445
+
446
+ - The component unmounts and remounts
447
+ - `reinitialize()` is called manually
448
+ - The `when` condition changes from `false` to `true` (after initial mount)
449
+
450
+ ### Conditional Execution (`when`)
451
+
452
+ When `when` is `false`:
453
+
454
+ - Initialization does not run
455
+ - If `when` changes from `false` to `true`, initialization will run
456
+ - If initialization was already successful, it will not run again even if `when` becomes `true` again
457
+
458
+ ### Retry Logic
459
+
460
+ When `retry` is set to `n`, the hook will attempt initialization up to `n + 1` times (initial attempt + `n` retries). Between attempts, it waits for `retryDelay` milliseconds.
461
+
462
+ ### Timeout
463
+
464
+ When `timeout` is set:
465
+
466
+ - For async callbacks, a race condition is created between the callback and timeout
467
+ - If timeout expires first, an `InitTimeoutError` is thrown
468
+ - For sync callbacks, timeout is cleared immediately after execution
469
+
470
+ ### Cleanup Functions
471
+
472
+ If the callback returns a cleanup function:
473
+
474
+ - It is called when the component unmounts
475
+ - It is called before re-initialization (when `reinitialize()` is called)
476
+ - It can be synchronous or asynchronous
477
+ - Only one cleanup function is stored at a time
478
+
479
+ ### Error Handling
480
+
481
+ - Errors during initialization are caught and stored in the `error` property
482
+ - If retry is enabled, errors trigger retry attempts
483
+ - After all retries fail, the final error is stored
484
+ - Errors do not prevent component rendering
485
+
486
+ ---
487
+
488
+ ## Testing
489
+
490
+ This package maintains comprehensive test coverage to ensure reliability and stability.
491
+
492
+ ### Test Coverage
493
+
494
+ 📊 <a href="https://geon0529.github.io/usefy/coverage/use-init/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages)
495
+
496
+ ### Test Categories
497
+
498
+ <details>
499
+ <summary><strong>Basic Initialization Tests</strong></summary>
500
+
501
+ - Run callback once on mount
502
+ - Not run callback again on re-render
503
+ - Support synchronous callbacks
504
+ - Support asynchronous callbacks
505
+ - Track initialization state correctly
506
+
507
+ </details>
508
+
509
+ <details>
510
+ <summary><strong>Cleanup Function Tests</strong></summary>
511
+
512
+ - Call cleanup function on unmount
513
+ - Call cleanup function before re-initialization
514
+ - Support synchronous cleanup functions
515
+ - Support asynchronous cleanup functions
516
+ - Handle cleanup function errors gracefully
517
+ - Not call cleanup if callback doesn't return one
518
+
519
+ </details>
520
+
521
+ <details>
522
+ <summary><strong>Conditional Execution Tests</strong></summary>
523
+
524
+ - Not run when `when` is false
525
+ - Run when `when` changes from false to true
526
+ - Not run again if already initialized
527
+ - Respect `when` condition in `reinitialize()`
528
+
529
+ </details>
530
+
531
+ <details>
532
+ <summary><strong>Retry Logic Tests</strong></summary>
533
+
534
+ - Retry on failure with correct number of attempts
535
+ - Wait correct delay between retries
536
+ - Stop retrying after successful attempt
537
+ - Store final error after all retries fail
538
+ - Not retry if component unmounts during retry
539
+
540
+ </details>
541
+
542
+ <details>
543
+ <summary><strong>Timeout Tests</strong></summary>
544
+
545
+ - Timeout async callbacks that exceed timeout
546
+ - Not timeout sync callbacks
547
+ - Clear timeout after successful execution
548
+ - Throw InitTimeoutError on timeout
549
+ - Handle timeout with retry logic
550
+
551
+ </details>
552
+
553
+ <details>
554
+ <summary><strong>Manual Re-initialization Tests</strong></summary>
555
+
556
+ - Reinitialize when `reinitialize()` is called
557
+ - Respect `when` condition in `reinitialize()`
558
+ - Clean up previous initialization before re-running
559
+ - Update state correctly after re-initialization
560
+
561
+ </details>
562
+
563
+ <details>
564
+ <summary><strong>State Management Tests</strong></summary>
565
+
566
+ - Track `isInitializing` state correctly
567
+ - Track `isInitialized` state correctly
568
+ - Track `error` state correctly
569
+ - Update state only when component is mounted
570
+ - Handle rapid state changes correctly
571
+
572
+ </details>
573
+
574
+ <details>
575
+ <summary><strong>Edge Cases Tests</strong></summary>
576
+
577
+ - Handle component unmount during initialization
578
+ - Handle component unmount during retry
579
+ - Prevent concurrent initializations
580
+ - Handle callback reference changes
581
+ - Handle undefined/null errors gracefully
582
+
583
+ </details>
584
+
585
+ ### Running Tests
586
+
587
+ ```bash
588
+ # Run all tests
589
+ pnpm test
590
+
591
+ # Run tests in watch mode
592
+ pnpm test:watch
593
+
594
+ # Run tests with coverage report
595
+ pnpm test --coverage
596
+ ```
597
+
598
+ ---
599
+
600
+ ## License
601
+
602
+ MIT © [mirunamu](https://github.com/geon0529)
603
+
604
+ This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
605
+
606
+ ---
607
+
608
+ <p align="center">
609
+ <sub>Built with care by the usefy team</sub>
610
+ </p>
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Options for useInit hook
3
+ */
4
+ interface UseInitOptions {
5
+ /**
6
+ * Only run initialization when this condition is true
7
+ * @default true
8
+ */
9
+ when?: boolean;
10
+ /**
11
+ * Number of retry attempts on failure
12
+ * @default 0
13
+ */
14
+ retry?: number;
15
+ /**
16
+ * Delay between retry attempts in milliseconds
17
+ * @default 1000
18
+ */
19
+ retryDelay?: number;
20
+ /**
21
+ * Timeout for initialization in milliseconds
22
+ * @default undefined (no timeout)
23
+ */
24
+ timeout?: number;
25
+ }
26
+ /**
27
+ * Result object returned by useInit hook
28
+ */
29
+ interface UseInitResult {
30
+ /**
31
+ * Whether initialization has completed successfully
32
+ */
33
+ isInitialized: boolean;
34
+ /**
35
+ * Whether initialization is currently in progress
36
+ */
37
+ isInitializing: boolean;
38
+ /**
39
+ * Error that occurred during initialization, if any
40
+ */
41
+ error: Error | null;
42
+ /**
43
+ * Manually trigger re-initialization (respects `when` condition)
44
+ */
45
+ reinitialize: () => void;
46
+ }
47
+ /**
48
+ * Type for cleanup function returned by init callback
49
+ */
50
+ type CleanupFn = () => void;
51
+ /**
52
+ * Type for init callback function
53
+ */
54
+ type InitCallback = () => void | CleanupFn | Promise<void | CleanupFn>;
55
+ /**
56
+ * A React hook for one-time initialization with async support, retry, timeout, and conditional execution.
57
+ *
58
+ * @param callback - The initialization function to run. Can be sync or async.
59
+ * Can optionally return a cleanup function.
60
+ * @param options - Configuration options for initialization
61
+ * @returns Object containing initialization state and control functions
62
+ *
63
+ * @example
64
+ * // Basic synchronous initialization
65
+ * useInit(() => {
66
+ * console.log('Component initialized');
67
+ * });
68
+ *
69
+ * @example
70
+ * // With cleanup function
71
+ * useInit(() => {
72
+ * const subscription = eventBus.subscribe();
73
+ * return () => subscription.unsubscribe();
74
+ * });
75
+ *
76
+ * @example
77
+ * // Async initialization with status tracking
78
+ * const { isInitialized, isInitializing, error } = useInit(async () => {
79
+ * await loadConfiguration();
80
+ * });
81
+ *
82
+ * @example
83
+ * // Conditional initialization
84
+ * useInit(() => {
85
+ * initializeAnalytics();
86
+ * }, { when: isProduction });
87
+ *
88
+ * @example
89
+ * // With retry and timeout
90
+ * const { error, reinitialize } = useInit(async () => {
91
+ * await connectToServer();
92
+ * }, {
93
+ * retry: 3,
94
+ * retryDelay: 1000,
95
+ * timeout: 5000
96
+ * });
97
+ */
98
+ declare function useInit(callback: InitCallback, options?: UseInitOptions): UseInitResult;
99
+
100
+ export { type UseInitOptions, type UseInitResult, useInit };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Options for useInit hook
3
+ */
4
+ interface UseInitOptions {
5
+ /**
6
+ * Only run initialization when this condition is true
7
+ * @default true
8
+ */
9
+ when?: boolean;
10
+ /**
11
+ * Number of retry attempts on failure
12
+ * @default 0
13
+ */
14
+ retry?: number;
15
+ /**
16
+ * Delay between retry attempts in milliseconds
17
+ * @default 1000
18
+ */
19
+ retryDelay?: number;
20
+ /**
21
+ * Timeout for initialization in milliseconds
22
+ * @default undefined (no timeout)
23
+ */
24
+ timeout?: number;
25
+ }
26
+ /**
27
+ * Result object returned by useInit hook
28
+ */
29
+ interface UseInitResult {
30
+ /**
31
+ * Whether initialization has completed successfully
32
+ */
33
+ isInitialized: boolean;
34
+ /**
35
+ * Whether initialization is currently in progress
36
+ */
37
+ isInitializing: boolean;
38
+ /**
39
+ * Error that occurred during initialization, if any
40
+ */
41
+ error: Error | null;
42
+ /**
43
+ * Manually trigger re-initialization (respects `when` condition)
44
+ */
45
+ reinitialize: () => void;
46
+ }
47
+ /**
48
+ * Type for cleanup function returned by init callback
49
+ */
50
+ type CleanupFn = () => void;
51
+ /**
52
+ * Type for init callback function
53
+ */
54
+ type InitCallback = () => void | CleanupFn | Promise<void | CleanupFn>;
55
+ /**
56
+ * A React hook for one-time initialization with async support, retry, timeout, and conditional execution.
57
+ *
58
+ * @param callback - The initialization function to run. Can be sync or async.
59
+ * Can optionally return a cleanup function.
60
+ * @param options - Configuration options for initialization
61
+ * @returns Object containing initialization state and control functions
62
+ *
63
+ * @example
64
+ * // Basic synchronous initialization
65
+ * useInit(() => {
66
+ * console.log('Component initialized');
67
+ * });
68
+ *
69
+ * @example
70
+ * // With cleanup function
71
+ * useInit(() => {
72
+ * const subscription = eventBus.subscribe();
73
+ * return () => subscription.unsubscribe();
74
+ * });
75
+ *
76
+ * @example
77
+ * // Async initialization with status tracking
78
+ * const { isInitialized, isInitializing, error } = useInit(async () => {
79
+ * await loadConfiguration();
80
+ * });
81
+ *
82
+ * @example
83
+ * // Conditional initialization
84
+ * useInit(() => {
85
+ * initializeAnalytics();
86
+ * }, { when: isProduction });
87
+ *
88
+ * @example
89
+ * // With retry and timeout
90
+ * const { error, reinitialize } = useInit(async () => {
91
+ * await connectToServer();
92
+ * }, {
93
+ * retry: 3,
94
+ * retryDelay: 1000,
95
+ * timeout: 5000
96
+ * });
97
+ */
98
+ declare function useInit(callback: InitCallback, options?: UseInitOptions): UseInitResult;
99
+
100
+ export { type UseInitOptions, type UseInitResult, useInit };
package/dist/index.js ADDED
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ useInit: () => useInit
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/useInit.ts
28
+ var import_react = require("react");
29
+ var InitTimeoutError = class extends Error {
30
+ constructor(timeout) {
31
+ super(`Initialization timed out after ${timeout}ms`);
32
+ this.name = "InitTimeoutError";
33
+ }
34
+ };
35
+ function useInit(callback, options = {}) {
36
+ const { when = true, retry = 0, retryDelay = 1e3, timeout } = options;
37
+ const [state, setState] = (0, import_react.useState)({
38
+ isInitialized: false,
39
+ isInitializing: false,
40
+ error: null
41
+ });
42
+ const callbackRef = (0, import_react.useRef)(callback);
43
+ const cleanupRef = (0, import_react.useRef)(null);
44
+ const hasInitializedRef = (0, import_react.useRef)(false);
45
+ const mountedRef = (0, import_react.useRef)(true);
46
+ const initializingRef = (0, import_react.useRef)(false);
47
+ callbackRef.current = callback;
48
+ const runInit = (0, import_react.useCallback)(async () => {
49
+ if (initializingRef.current) {
50
+ return;
51
+ }
52
+ initializingRef.current = true;
53
+ if (cleanupRef.current) {
54
+ cleanupRef.current();
55
+ cleanupRef.current = null;
56
+ }
57
+ setState({
58
+ isInitialized: false,
59
+ isInitializing: true,
60
+ error: null
61
+ });
62
+ let lastError = null;
63
+ const maxAttempts = retry + 1;
64
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
65
+ if (!mountedRef.current) {
66
+ initializingRef.current = false;
67
+ return;
68
+ }
69
+ try {
70
+ let result;
71
+ if (timeout !== void 0) {
72
+ let timeoutId;
73
+ const timeoutPromise = new Promise((_, reject) => {
74
+ timeoutId = setTimeout(() => {
75
+ reject(new InitTimeoutError(timeout));
76
+ }, timeout);
77
+ });
78
+ const callbackResult = callbackRef.current();
79
+ if (callbackResult instanceof Promise) {
80
+ try {
81
+ result = await Promise.race([callbackResult, timeoutPromise]);
82
+ } finally {
83
+ if (timeoutId !== void 0) {
84
+ clearTimeout(timeoutId);
85
+ }
86
+ }
87
+ } else {
88
+ if (timeoutId !== void 0) {
89
+ clearTimeout(timeoutId);
90
+ }
91
+ result = callbackResult;
92
+ }
93
+ } else {
94
+ const callbackResult = callbackRef.current();
95
+ if (callbackResult instanceof Promise) {
96
+ result = await callbackResult;
97
+ } else {
98
+ result = callbackResult;
99
+ }
100
+ }
101
+ if (typeof result === "function") {
102
+ cleanupRef.current = result;
103
+ }
104
+ if (mountedRef.current) {
105
+ hasInitializedRef.current = true;
106
+ setState({
107
+ isInitialized: true,
108
+ isInitializing: false,
109
+ error: null
110
+ });
111
+ }
112
+ initializingRef.current = false;
113
+ return;
114
+ } catch (err) {
115
+ lastError = err instanceof Error ? err : new Error(String(err));
116
+ if (attempt < maxAttempts - 1 && mountedRef.current) {
117
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
118
+ }
119
+ }
120
+ }
121
+ if (mountedRef.current) {
122
+ setState({
123
+ isInitialized: false,
124
+ isInitializing: false,
125
+ error: lastError
126
+ });
127
+ }
128
+ initializingRef.current = false;
129
+ }, [retry, retryDelay, timeout]);
130
+ const reinitialize = (0, import_react.useCallback)(() => {
131
+ if (!when) {
132
+ return;
133
+ }
134
+ runInit();
135
+ }, [when, runInit]);
136
+ const prevWhenRef = (0, import_react.useRef)(when);
137
+ const hasRunOnceRef = (0, import_react.useRef)(false);
138
+ (0, import_react.useEffect)(() => {
139
+ mountedRef.current = true;
140
+ const whenJustBecameTrue = !prevWhenRef.current && when;
141
+ const shouldInit = when && !hasInitializedRef.current && (!hasRunOnceRef.current || whenJustBecameTrue);
142
+ prevWhenRef.current = when;
143
+ if (shouldInit) {
144
+ hasRunOnceRef.current = true;
145
+ runInit();
146
+ }
147
+ return () => {
148
+ mountedRef.current = false;
149
+ if (cleanupRef.current) {
150
+ cleanupRef.current();
151
+ cleanupRef.current = null;
152
+ }
153
+ };
154
+ }, [when, runInit]);
155
+ return {
156
+ isInitialized: state.isInitialized,
157
+ isInitializing: state.isInitializing,
158
+ error: state.error,
159
+ reinitialize
160
+ };
161
+ }
162
+ // Annotate the CommonJS export names for ESM import in node:
163
+ 0 && (module.exports = {
164
+ useInit
165
+ });
166
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/useInit.ts"],"sourcesContent":["export { useInit, type UseInitOptions, type UseInitResult } from \"./useInit\";\n","import { useState, useEffect, useRef, useCallback } from \"react\";\n\n/**\n * Options for useInit hook\n */\nexport interface UseInitOptions {\n /**\n * Only run initialization when this condition is true\n * @default true\n */\n when?: boolean;\n /**\n * Number of retry attempts on failure\n * @default 0\n */\n retry?: number;\n /**\n * Delay between retry attempts in milliseconds\n * @default 1000\n */\n retryDelay?: number;\n /**\n * Timeout for initialization in milliseconds\n * @default undefined (no timeout)\n */\n timeout?: number;\n}\n\n/**\n * Result object returned by useInit hook\n */\nexport interface UseInitResult {\n /**\n * Whether initialization has completed successfully\n */\n isInitialized: boolean;\n /**\n * Whether initialization is currently in progress\n */\n isInitializing: boolean;\n /**\n * Error that occurred during initialization, if any\n */\n error: Error | null;\n /**\n * Manually trigger re-initialization (respects `when` condition)\n */\n reinitialize: () => void;\n}\n\n/**\n * Type for cleanup function returned by init callback\n */\ntype CleanupFn = () => void;\n\n/**\n * Type for init callback function\n */\ntype InitCallback = () => void | CleanupFn | Promise<void | CleanupFn>;\n\n/**\n * Custom error for timeout\n */\nclass InitTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Initialization timed out after ${timeout}ms`);\n this.name = \"InitTimeoutError\";\n }\n}\n\n/**\n * A React hook for one-time initialization with async support, retry, timeout, and conditional execution.\n *\n * @param callback - The initialization function to run. Can be sync or async.\n * Can optionally return a cleanup function.\n * @param options - Configuration options for initialization\n * @returns Object containing initialization state and control functions\n *\n * @example\n * // Basic synchronous initialization\n * useInit(() => {\n * console.log('Component initialized');\n * });\n *\n * @example\n * // With cleanup function\n * useInit(() => {\n * const subscription = eventBus.subscribe();\n * return () => subscription.unsubscribe();\n * });\n *\n * @example\n * // Async initialization with status tracking\n * const { isInitialized, isInitializing, error } = useInit(async () => {\n * await loadConfiguration();\n * });\n *\n * @example\n * // Conditional initialization\n * useInit(() => {\n * initializeAnalytics();\n * }, { when: isProduction });\n *\n * @example\n * // With retry and timeout\n * const { error, reinitialize } = useInit(async () => {\n * await connectToServer();\n * }, {\n * retry: 3,\n * retryDelay: 1000,\n * timeout: 5000\n * });\n */\nexport function useInit(\n callback: InitCallback,\n options: UseInitOptions = {}\n): UseInitResult {\n const { when = true, retry = 0, retryDelay = 1000, timeout } = options;\n\n const [state, setState] = useState<{\n isInitialized: boolean;\n isInitializing: boolean;\n error: Error | null;\n }>({\n isInitialized: false,\n isInitializing: false,\n error: null,\n });\n\n const callbackRef = useRef<InitCallback>(callback);\n const cleanupRef = useRef<CleanupFn | null>(null);\n const hasInitializedRef = useRef(false);\n const mountedRef = useRef(true);\n const initializingRef = useRef(false);\n\n // Always update callback ref to latest version\n callbackRef.current = callback;\n\n const runInit = useCallback(async () => {\n // Prevent concurrent initializations\n if (initializingRef.current) {\n return;\n }\n\n initializingRef.current = true;\n\n // Clean up previous initialization if any\n if (cleanupRef.current) {\n cleanupRef.current();\n cleanupRef.current = null;\n }\n\n setState({\n isInitialized: false,\n isInitializing: true,\n error: null,\n });\n\n let lastError: Error | null = null;\n const maxAttempts = retry + 1;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n if (!mountedRef.current) {\n initializingRef.current = false;\n return;\n }\n\n try {\n let result: void | CleanupFn;\n\n if (timeout !== undefined) {\n // Race between callback and timeout\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new InitTimeoutError(timeout));\n }, timeout);\n });\n\n const callbackResult = callbackRef.current();\n\n if (callbackResult instanceof Promise) {\n try {\n result = await Promise.race([callbackResult, timeoutPromise]);\n } finally {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n }\n } else {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n result = callbackResult;\n }\n } else {\n const callbackResult = callbackRef.current();\n if (callbackResult instanceof Promise) {\n result = await callbackResult;\n } else {\n result = callbackResult;\n }\n }\n\n // Store cleanup function if returned\n if (typeof result === \"function\") {\n cleanupRef.current = result;\n }\n\n if (mountedRef.current) {\n hasInitializedRef.current = true;\n setState({\n isInitialized: true,\n isInitializing: false,\n error: null,\n });\n }\n\n initializingRef.current = false;\n return;\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n\n // If not the last attempt and still mounted, wait before retrying\n if (attempt < maxAttempts - 1 && mountedRef.current) {\n await new Promise((resolve) => setTimeout(resolve, retryDelay));\n }\n }\n }\n\n // All attempts failed\n if (mountedRef.current) {\n setState({\n isInitialized: false,\n isInitializing: false,\n error: lastError,\n });\n }\n\n initializingRef.current = false;\n }, [retry, retryDelay, timeout]);\n\n const reinitialize = useCallback(() => {\n if (!when) {\n return;\n }\n runInit();\n }, [when, runInit]);\n\n // Track when condition changes from false to true\n const prevWhenRef = useRef(when);\n const hasRunOnceRef = useRef(false);\n\n useEffect(() => {\n mountedRef.current = true;\n\n // Run initialization if:\n // 1. `when` is true AND\n // 2. Never successfully initialized AND\n // 3. Either first run OR `when` just changed from false to true\n const whenJustBecameTrue = !prevWhenRef.current && when;\n const shouldInit =\n when &&\n !hasInitializedRef.current &&\n (!hasRunOnceRef.current || whenJustBecameTrue);\n\n prevWhenRef.current = when;\n\n if (shouldInit) {\n hasRunOnceRef.current = true;\n runInit();\n }\n\n return () => {\n mountedRef.current = false;\n if (cleanupRef.current) {\n cleanupRef.current();\n cleanupRef.current = null;\n }\n };\n }, [when, runInit]);\n\n return {\n isInitialized: state.isInitialized,\n isInitializing: state.isInitializing,\n error: state.error,\n reinitialize,\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,139 @@
1
+ // src/useInit.ts
2
+ import { useState, useEffect, useRef, useCallback } from "react";
3
+ var InitTimeoutError = class extends Error {
4
+ constructor(timeout) {
5
+ super(`Initialization timed out after ${timeout}ms`);
6
+ this.name = "InitTimeoutError";
7
+ }
8
+ };
9
+ function useInit(callback, options = {}) {
10
+ const { when = true, retry = 0, retryDelay = 1e3, timeout } = options;
11
+ const [state, setState] = useState({
12
+ isInitialized: false,
13
+ isInitializing: false,
14
+ error: null
15
+ });
16
+ const callbackRef = useRef(callback);
17
+ const cleanupRef = useRef(null);
18
+ const hasInitializedRef = useRef(false);
19
+ const mountedRef = useRef(true);
20
+ const initializingRef = useRef(false);
21
+ callbackRef.current = callback;
22
+ const runInit = useCallback(async () => {
23
+ if (initializingRef.current) {
24
+ return;
25
+ }
26
+ initializingRef.current = true;
27
+ if (cleanupRef.current) {
28
+ cleanupRef.current();
29
+ cleanupRef.current = null;
30
+ }
31
+ setState({
32
+ isInitialized: false,
33
+ isInitializing: true,
34
+ error: null
35
+ });
36
+ let lastError = null;
37
+ const maxAttempts = retry + 1;
38
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
39
+ if (!mountedRef.current) {
40
+ initializingRef.current = false;
41
+ return;
42
+ }
43
+ try {
44
+ let result;
45
+ if (timeout !== void 0) {
46
+ let timeoutId;
47
+ const timeoutPromise = new Promise((_, reject) => {
48
+ timeoutId = setTimeout(() => {
49
+ reject(new InitTimeoutError(timeout));
50
+ }, timeout);
51
+ });
52
+ const callbackResult = callbackRef.current();
53
+ if (callbackResult instanceof Promise) {
54
+ try {
55
+ result = await Promise.race([callbackResult, timeoutPromise]);
56
+ } finally {
57
+ if (timeoutId !== void 0) {
58
+ clearTimeout(timeoutId);
59
+ }
60
+ }
61
+ } else {
62
+ if (timeoutId !== void 0) {
63
+ clearTimeout(timeoutId);
64
+ }
65
+ result = callbackResult;
66
+ }
67
+ } else {
68
+ const callbackResult = callbackRef.current();
69
+ if (callbackResult instanceof Promise) {
70
+ result = await callbackResult;
71
+ } else {
72
+ result = callbackResult;
73
+ }
74
+ }
75
+ if (typeof result === "function") {
76
+ cleanupRef.current = result;
77
+ }
78
+ if (mountedRef.current) {
79
+ hasInitializedRef.current = true;
80
+ setState({
81
+ isInitialized: true,
82
+ isInitializing: false,
83
+ error: null
84
+ });
85
+ }
86
+ initializingRef.current = false;
87
+ return;
88
+ } catch (err) {
89
+ lastError = err instanceof Error ? err : new Error(String(err));
90
+ if (attempt < maxAttempts - 1 && mountedRef.current) {
91
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
92
+ }
93
+ }
94
+ }
95
+ if (mountedRef.current) {
96
+ setState({
97
+ isInitialized: false,
98
+ isInitializing: false,
99
+ error: lastError
100
+ });
101
+ }
102
+ initializingRef.current = false;
103
+ }, [retry, retryDelay, timeout]);
104
+ const reinitialize = useCallback(() => {
105
+ if (!when) {
106
+ return;
107
+ }
108
+ runInit();
109
+ }, [when, runInit]);
110
+ const prevWhenRef = useRef(when);
111
+ const hasRunOnceRef = useRef(false);
112
+ useEffect(() => {
113
+ mountedRef.current = true;
114
+ const whenJustBecameTrue = !prevWhenRef.current && when;
115
+ const shouldInit = when && !hasInitializedRef.current && (!hasRunOnceRef.current || whenJustBecameTrue);
116
+ prevWhenRef.current = when;
117
+ if (shouldInit) {
118
+ hasRunOnceRef.current = true;
119
+ runInit();
120
+ }
121
+ return () => {
122
+ mountedRef.current = false;
123
+ if (cleanupRef.current) {
124
+ cleanupRef.current();
125
+ cleanupRef.current = null;
126
+ }
127
+ };
128
+ }, [when, runInit]);
129
+ return {
130
+ isInitialized: state.isInitialized,
131
+ isInitializing: state.isInitializing,
132
+ error: state.error,
133
+ reinitialize
134
+ };
135
+ }
136
+ export {
137
+ useInit
138
+ };
139
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useInit.ts"],"sourcesContent":["import { useState, useEffect, useRef, useCallback } from \"react\";\n\n/**\n * Options for useInit hook\n */\nexport interface UseInitOptions {\n /**\n * Only run initialization when this condition is true\n * @default true\n */\n when?: boolean;\n /**\n * Number of retry attempts on failure\n * @default 0\n */\n retry?: number;\n /**\n * Delay between retry attempts in milliseconds\n * @default 1000\n */\n retryDelay?: number;\n /**\n * Timeout for initialization in milliseconds\n * @default undefined (no timeout)\n */\n timeout?: number;\n}\n\n/**\n * Result object returned by useInit hook\n */\nexport interface UseInitResult {\n /**\n * Whether initialization has completed successfully\n */\n isInitialized: boolean;\n /**\n * Whether initialization is currently in progress\n */\n isInitializing: boolean;\n /**\n * Error that occurred during initialization, if any\n */\n error: Error | null;\n /**\n * Manually trigger re-initialization (respects `when` condition)\n */\n reinitialize: () => void;\n}\n\n/**\n * Type for cleanup function returned by init callback\n */\ntype CleanupFn = () => void;\n\n/**\n * Type for init callback function\n */\ntype InitCallback = () => void | CleanupFn | Promise<void | CleanupFn>;\n\n/**\n * Custom error for timeout\n */\nclass InitTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Initialization timed out after ${timeout}ms`);\n this.name = \"InitTimeoutError\";\n }\n}\n\n/**\n * A React hook for one-time initialization with async support, retry, timeout, and conditional execution.\n *\n * @param callback - The initialization function to run. Can be sync or async.\n * Can optionally return a cleanup function.\n * @param options - Configuration options for initialization\n * @returns Object containing initialization state and control functions\n *\n * @example\n * // Basic synchronous initialization\n * useInit(() => {\n * console.log('Component initialized');\n * });\n *\n * @example\n * // With cleanup function\n * useInit(() => {\n * const subscription = eventBus.subscribe();\n * return () => subscription.unsubscribe();\n * });\n *\n * @example\n * // Async initialization with status tracking\n * const { isInitialized, isInitializing, error } = useInit(async () => {\n * await loadConfiguration();\n * });\n *\n * @example\n * // Conditional initialization\n * useInit(() => {\n * initializeAnalytics();\n * }, { when: isProduction });\n *\n * @example\n * // With retry and timeout\n * const { error, reinitialize } = useInit(async () => {\n * await connectToServer();\n * }, {\n * retry: 3,\n * retryDelay: 1000,\n * timeout: 5000\n * });\n */\nexport function useInit(\n callback: InitCallback,\n options: UseInitOptions = {}\n): UseInitResult {\n const { when = true, retry = 0, retryDelay = 1000, timeout } = options;\n\n const [state, setState] = useState<{\n isInitialized: boolean;\n isInitializing: boolean;\n error: Error | null;\n }>({\n isInitialized: false,\n isInitializing: false,\n error: null,\n });\n\n const callbackRef = useRef<InitCallback>(callback);\n const cleanupRef = useRef<CleanupFn | null>(null);\n const hasInitializedRef = useRef(false);\n const mountedRef = useRef(true);\n const initializingRef = useRef(false);\n\n // Always update callback ref to latest version\n callbackRef.current = callback;\n\n const runInit = useCallback(async () => {\n // Prevent concurrent initializations\n if (initializingRef.current) {\n return;\n }\n\n initializingRef.current = true;\n\n // Clean up previous initialization if any\n if (cleanupRef.current) {\n cleanupRef.current();\n cleanupRef.current = null;\n }\n\n setState({\n isInitialized: false,\n isInitializing: true,\n error: null,\n });\n\n let lastError: Error | null = null;\n const maxAttempts = retry + 1;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n if (!mountedRef.current) {\n initializingRef.current = false;\n return;\n }\n\n try {\n let result: void | CleanupFn;\n\n if (timeout !== undefined) {\n // Race between callback and timeout\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new InitTimeoutError(timeout));\n }, timeout);\n });\n\n const callbackResult = callbackRef.current();\n\n if (callbackResult instanceof Promise) {\n try {\n result = await Promise.race([callbackResult, timeoutPromise]);\n } finally {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n }\n } else {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n result = callbackResult;\n }\n } else {\n const callbackResult = callbackRef.current();\n if (callbackResult instanceof Promise) {\n result = await callbackResult;\n } else {\n result = callbackResult;\n }\n }\n\n // Store cleanup function if returned\n if (typeof result === \"function\") {\n cleanupRef.current = result;\n }\n\n if (mountedRef.current) {\n hasInitializedRef.current = true;\n setState({\n isInitialized: true,\n isInitializing: false,\n error: null,\n });\n }\n\n initializingRef.current = false;\n return;\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n\n // If not the last attempt and still mounted, wait before retrying\n if (attempt < maxAttempts - 1 && mountedRef.current) {\n await new Promise((resolve) => setTimeout(resolve, retryDelay));\n }\n }\n }\n\n // All attempts failed\n if (mountedRef.current) {\n setState({\n isInitialized: false,\n isInitializing: false,\n error: lastError,\n });\n }\n\n initializingRef.current = false;\n }, [retry, retryDelay, timeout]);\n\n const reinitialize = useCallback(() => {\n if (!when) {\n return;\n }\n runInit();\n }, [when, runInit]);\n\n // Track when condition changes from false to true\n const prevWhenRef = useRef(when);\n const hasRunOnceRef = useRef(false);\n\n useEffect(() => {\n mountedRef.current = true;\n\n // Run initialization if:\n // 1. `when` is true AND\n // 2. Never successfully initialized AND\n // 3. Either first run OR `when` just changed from false to true\n const whenJustBecameTrue = !prevWhenRef.current && when;\n const shouldInit =\n when &&\n !hasInitializedRef.current &&\n (!hasRunOnceRef.current || whenJustBecameTrue);\n\n prevWhenRef.current = when;\n\n if (shouldInit) {\n hasRunOnceRef.current = true;\n runInit();\n }\n\n return () => {\n mountedRef.current = false;\n if (cleanupRef.current) {\n cleanupRef.current();\n cleanupRef.current = null;\n }\n };\n }, [when, runInit]);\n\n return {\n isInitialized: state.isInitialized,\n isInitializing: state.isInitializing,\n error: state.error,\n reinitialize,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@usefy/use-init",
3
+ "version": "0.0.24",
4
+ "description": "A React hook for one-time initialization with async support, retry, timeout, and conditional execution",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "peerDependencies": {
20
+ "react": "^18.0.0 || ^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@testing-library/jest-dom": "^6.9.1",
24
+ "@testing-library/react": "^16.3.1",
25
+ "@testing-library/user-event": "^14.6.1",
26
+ "@types/react": "^19.0.0",
27
+ "jsdom": "^27.3.0",
28
+ "react": "^19.0.0",
29
+ "rimraf": "^6.0.1",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0",
32
+ "vitest": "^4.0.16"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/geon0529/usefy.git",
40
+ "directory": "packages/use-init"
41
+ },
42
+ "license": "MIT",
43
+ "keywords": [
44
+ "react",
45
+ "hooks",
46
+ "init",
47
+ "initialization",
48
+ "mount",
49
+ "async",
50
+ "retry",
51
+ "timeout"
52
+ ],
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "dev": "tsup --watch",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "typecheck": "tsc --noEmit",
59
+ "clean": "rimraf dist"
60
+ }
61
+ }