@leancodepl/utils 9.6.5 → 9.6.6

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/index.esm.js CHANGED
@@ -1,121 +1,217 @@
1
+ import { useState, useCallback, useMemo, createContext, useEffect, useContext } from 'react';
1
2
  import invariant from 'tiny-invariant';
2
- import { useState, useCallback, useMemo } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
3
4
 
4
5
  /**
5
- * Adds a prefix to all keys in an object, creating a new object with prefixed keys.
6
- *
7
- * @template T - The type of the input object
8
- * @template TPrefix - The type of the prefix string
9
- * @param object - The object whose keys will be prefixed
10
- * @param prefix - The prefix string to add to each key
11
- * @returns A new object with all keys prefixed
6
+ * React hook for tracking async task execution with loading state.
7
+ * Automatically manages a loading counter and provides a wrapper function for tasks.
8
+ *
9
+ * @returns A tuple containing [isLoading: boolean, runInTask: function]
12
10
  * @example
13
11
  * ```typescript
14
- * const apiData = { userId: 1, userName: 'John' };
15
- * const prefixed = addPrefix(apiData, 'api_');
16
- * // Result: { api_userId: 1, api_userName: 'John' }
12
+ * function MyComponent() {
13
+ * const [isLoading, runInTask] = useRunInTask();
14
+ *
15
+ * const handleSave = async () => {
16
+ * await runInTask(async () => {
17
+ * await saveData();
18
+ * });
19
+ * };
20
+ *
21
+ * return (
22
+ * <button onClick={handleSave} disabled={isLoading}>
23
+ * {isLoading ? 'Saving...' : 'Save'}
24
+ * </button>
25
+ * );
26
+ * }
17
27
  * ```
18
- */ function addPrefix(object, prefix) {
19
- return Object.fromEntries(Object.entries(object).map(([key, value])=>[
20
- `${prefix}${key}`,
21
- value
22
- ]));
28
+ */ function useRunInTask() {
29
+ const [runningTasks, setRunningTasks] = useState(0);
30
+ const runInTask = useCallback(async (task)=>{
31
+ setRunningTasks((runningTasks)=>runningTasks + 1);
32
+ try {
33
+ return await task();
34
+ } finally{
35
+ setRunningTasks((runningTasks)=>runningTasks - 1);
36
+ }
37
+ }, []);
38
+ return [
39
+ runningTasks > 0,
40
+ runInTask
41
+ ];
23
42
  }
24
43
 
25
- function transformFirst(value, transformFn) {
26
- if (value.length === 0) {
27
- return "";
28
- }
29
- return transformFn(value[0]) + value.slice(1);
44
+ function useBoundRunInTask(block) {
45
+ const [isRunning, runInTask] = useRunInTask();
46
+ const runBlockInTask = useMemo(()=>block ? (...args)=>runInTask(()=>block(...args)) : undefined, [
47
+ block,
48
+ runInTask
49
+ ]);
50
+ return [
51
+ isRunning,
52
+ runBlockInTask
53
+ ];
30
54
  }
55
+
31
56
  /**
32
- * Converts the first character of a string to lowercase.
57
+ * React hook for boolean state management helpers.
33
58
  *
34
- * @param value - The string to transform
35
- * @returns The string with the first character in lowercase
59
+ * @param set - The state setter function from useState
60
+ * @returns A tuple containing [setTrue: function, setFalse: function]
36
61
  * @example
37
62
  * ```typescript
38
- * const result = toLowerFirst('UserName');
39
- * // Result: 'userName'
63
+ * function MyComponent() {
64
+ * const [isVisible, setIsVisible] = useState(false);
65
+ * const [show, hide] = useSetUnset(setIsVisible);
66
+ *
67
+ * return (
68
+ * <div>
69
+ * <button onClick={show}>Show</button>
70
+ * <button onClick={hide}>Hide</button>
71
+ * {isVisible && <div>Content is visible</div>}
72
+ * </div>
73
+ * );
74
+ * }
40
75
  * ```
41
- */ function toLowerFirst(value) {
42
- return transformFirst(value, (value)=>value.toLowerCase());
76
+ */ function useSetUnset(set) {
77
+ return [
78
+ useCallback(()=>set(true), [
79
+ set
80
+ ]),
81
+ useCallback(()=>set(false), [
82
+ set
83
+ ])
84
+ ];
43
85
  }
86
+
44
87
  /**
45
- * Converts the first character of a string to uppercase.
88
+ * React hook for managing dialog state with optional callback after closing.
89
+ * Provides convenient open/close functions and tracks the dialog's open state.
46
90
  *
47
- * @param value - The string to transform
48
- * @returns The string with the first character in uppercase
91
+ * @param onAfterClose - Optional callback function to execute after the dialog closes
92
+ * @returns Object containing dialog state and control functions
49
93
  * @example
50
94
  * ```typescript
51
- * const result = toUpperFirst('userName');
52
- * // Result: 'UserName'
95
+ * function MyComponent() {
96
+ * const { isDialogOpen, openDialog, closeDialog } = useDialog(() => {
97
+ * console.log('Dialog closed');
98
+ * });
99
+ *
100
+ * return (
101
+ * <div>
102
+ * <button onClick={openDialog}>Open Dialog</button>
103
+ * {isDialogOpen && <Dialog onClose={closeDialog} />}
104
+ * </div>
105
+ * );
106
+ * }
53
107
  * ```
54
- */ function toUpperFirst(value) {
55
- return transformFirst(value, (value)=>value.toUpperCase());
56
- }
57
-
58
- function _extends() {
59
- _extends = Object.assign || function assign(target) {
60
- for(var i = 1; i < arguments.length; i++){
61
- var source = arguments[i];
62
- for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
63
- }
64
- return target;
108
+ */ function useDialog(onAfterClose) {
109
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
110
+ const [openDialog, closeDialog] = useSetUnset(setIsDialogOpen);
111
+ const close = useCallback(()=>{
112
+ closeDialog();
113
+ if (onAfterClose) setTimeout(onAfterClose);
114
+ }, [
115
+ closeDialog,
116
+ onAfterClose
117
+ ]);
118
+ return {
119
+ isDialogOpen,
120
+ openDialog,
121
+ closeDialog: close
65
122
  };
66
- return _extends.apply(this, arguments);
67
123
  }
68
124
 
69
- function transformDeep(value, mode) {
70
- if (value === null || value === undefined) {
71
- return undefined;
72
- }
73
- if (Array.isArray(value)) {
74
- return value.map((val)=>transformDeep(val, mode));
75
- }
76
- if (typeof value === "object") {
77
- const transformKey = mode === "capitalize" ? toUpperFirst : toLowerFirst;
78
- return Object.entries(value).reduce((accumulator, [key, value])=>_extends({}, accumulator, {
79
- [transformKey(key)]: transformDeep(value, mode)
80
- }), {});
125
+ /**
126
+ * React hook for generating keys based on current route matches.
127
+ *
128
+ * @template TKey - The type of the route keys
129
+ * @param routeMatches - Record of route keys to match objects or arrays
130
+ * @returns Array of active route keys
131
+ * @example
132
+ * ```typescript
133
+ * function NavigationComponent() {
134
+ * const routeMatches = {
135
+ * home: useRouteMatch('/'),
136
+ * about: useRouteMatch('/about'),
137
+ * contact: useRouteMatch('/contact')
138
+ * };
139
+ *
140
+ * const activeRoutes = useKeyByRoute(routeMatches);
141
+ * // Returns ['home'] if on home page, ['about'] if on about page, etc.
142
+ *
143
+ * return (
144
+ * <nav>
145
+ * {activeRoutes.map(route => (
146
+ * <span key={route}>Active: {route}</span>
147
+ * ))}
148
+ * </nav>
149
+ * );
150
+ * }
151
+ * ```
152
+ */ function useKeyByRoute(routeMatches) {
153
+ const keys = [];
154
+ for(const key in routeMatches){
155
+ const matches = routeMatches[key];
156
+ if (Array.isArray(matches) ? matches.some((match)=>match !== null) : matches !== null) {
157
+ keys.push(key);
158
+ }
81
159
  }
82
- return value;
160
+ return keys;
83
161
  }
162
+
84
163
  /**
85
- * Recursively transforms all object keys to use uncapitalized (camelCase) format.
86
- *
87
- * @template T - The type of the input value
88
- * @param value - The value to transform (can be object, array, or primitive)
89
- * @returns A new object with all keys converted to camelCase
164
+ * Synchronizes external state changes with a callback function, calling the callback only when state actually changes.
165
+ *
166
+ * @param state - The current state value to monitor for changes
167
+ * @param onChange - Callback function executed when state changes
168
+ * @param compare - Optional comparison function to determine if state has changed (defaults to strict equality)
90
169
  * @example
91
170
  * ```typescript
92
- * const serverData = { UserId: 1, UserName: 'John', Profile: { FirstName: 'John' } };
93
- * const clientData = uncapitalizeDeep(serverData);
94
- * // Result: { userId: 1, userName: 'John', profile: { firstName: 'John' } }
171
+ * import { useSyncState } from "@leancodepl/utils";
172
+ *
173
+ * function MyComponent({ externalValue }: { externalValue: string }) {
174
+ * useSyncState(externalValue, (newValue) => {
175
+ * console.log('Value changed to:', newValue);
176
+ * });
177
+ *
178
+ * return <div>{externalValue}</div>;
179
+ * }
95
180
  * ```
96
- */ function uncapitalizeDeep(value) {
97
- return transformDeep(value, "uncapitalize");
181
+ */ function useSyncState(state, onChange, compare) {
182
+ const [prevState, setPrevState] = useState(state);
183
+ const hasChanged = compare ? !compare(state, prevState) : state !== prevState;
184
+ if (hasChanged) {
185
+ setPrevState(state);
186
+ onChange(state);
187
+ }
98
188
  }
189
+
99
190
  /**
100
- * Recursively transforms all object keys to use capitalized (PascalCase) format.
101
- *
102
- * @template T - The type of the input value
103
- * @param value - The value to transform (can be object, array, or primitive)
104
- * @returns A new object with all keys converted to PascalCase
191
+ * Adds a prefix to all keys in an object, creating a new object with prefixed keys.
192
+ *
193
+ * @template T - The type of the input object
194
+ * @template TPrefix - The type of the prefix string
195
+ * @param object - The object whose keys will be prefixed
196
+ * @param prefix - The prefix string to add to each key
197
+ * @returns A new object with all keys prefixed
105
198
  * @example
106
199
  * ```typescript
107
- * const clientData = { userId: 1, userName: 'John', profile: { firstName: 'John' } };
108
- * const serverData = capitalizeDeep(clientData);
109
- * // Result: { UserId: 1, UserName: 'John', Profile: { FirstName: 'John' } }
200
+ * const apiData = { userId: 1, userName: 'John' };
201
+ * const prefixed = addPrefix(apiData, 'api_');
202
+ * // Result: { api_userId: 1, api_userName: 'John' }
110
203
  * ```
111
- */ function capitalizeDeep(value) {
112
- return transformDeep(value, "capitalize");
204
+ */ function addPrefix(object, prefix) {
205
+ return Object.fromEntries(Object.entries(object).map(([key, value])=>[
206
+ `${prefix}${key}`,
207
+ value
208
+ ]));
113
209
  }
114
210
 
115
211
  /**
116
212
  * Asserts that a value is not undefined. Throws an error if the value is undefined.
117
213
  * This is a type assertion function that narrows the type to exclude undefined.
118
- *
214
+ *
119
215
  * @template T - The type of the value being checked
120
216
  * @param value - The value to check for undefined
121
217
  * @param message - Optional error message to use if assertion fails
@@ -131,10 +227,29 @@ function transformDeep(value, mode) {
131
227
  invariant(value !== undefined, message);
132
228
  }
133
229
 
230
+ /**
231
+ * Asserts that a value is not null or undefined. Throws an error if the value is null or undefined.
232
+ * This is a type assertion function that narrows the type to exclude null and undefined.
233
+ *
234
+ * @template T - The type of the value being checked
235
+ * @param value - The value to check for null or undefined
236
+ * @param message - Optional error message to use if assertion fails
237
+ * @throws {Error} When the value is null or undefined
238
+ * @example
239
+ * ```typescript
240
+ * function processOptionalData(data?: string | null) {
241
+ * assertNotEmpty(data);
242
+ * return data.toUpperCase(); // TypeScript knows data is not null/undefined
243
+ * }
244
+ * ```
245
+ */ function assertNotEmpty(value, message) {
246
+ invariant(value !== null && value !== undefined, message);
247
+ }
248
+
134
249
  /**
135
250
  * Asserts that a value is not null. Throws an error if the value is null.
136
251
  * This is a type assertion function that narrows the type to exclude null.
137
- *
252
+ *
138
253
  * @template T - The type of the value being checked
139
254
  * @param value - The value to check for null
140
255
  * @param message - Optional error message to use if assertion fails
@@ -150,28 +265,24 @@ function transformDeep(value, mode) {
150
265
  invariant(value !== null, message);
151
266
  }
152
267
 
153
- /**
154
- * Asserts that a value is not null or undefined. Throws an error if the value is null or undefined.
155
- * This is a type assertion function that narrows the type to exclude null and undefined.
156
- *
157
- * @template T - The type of the value being checked
158
- * @param value - The value to check for null or undefined
159
- * @param message - Optional error message to use if assertion fails
160
- * @throws {Error} When the value is null or undefined
161
- * @example
162
- * ```typescript
163
- * function processOptionalData(data?: string | null) {
164
- * assertNotEmpty(data);
165
- * return data.toUpperCase(); // TypeScript knows data is not null/undefined
166
- * }
167
- * ```
168
- */ function assertNotEmpty(value, message) {
169
- invariant(value !== null && value !== undefined, message);
268
+ function downloadFile(dataOrUrl, options = {}) {
269
+ if (typeof dataOrUrl === "string") {
270
+ const { name } = options;
271
+ const a = document.createElement("a");
272
+ a.href = dataOrUrl;
273
+ a.target = "_blank";
274
+ if (name) a.download = name;
275
+ a.click();
276
+ } else {
277
+ const url = URL.createObjectURL(dataOrUrl);
278
+ downloadFile(url, options);
279
+ URL.revokeObjectURL(url);
280
+ }
170
281
  }
171
282
 
172
283
  /**
173
284
  * Ensures that a value is defined, returning it if defined or throwing an error if undefined.
174
- *
285
+ *
175
286
  * @template T - The type of the value being checked
176
287
  * @param value - The value to ensure is defined
177
288
  * @param message - Optional error message to use if the value is undefined
@@ -184,15 +295,35 @@ function transformDeep(value, mode) {
184
295
  * return definedUser.name; // definedUser is guaranteed to be defined
185
296
  * }
186
297
  * ```
187
- */ function ensureDefined(value, message) {
188
- assertDefined(value, message);
298
+ */ function ensureDefined(value, message) {
299
+ assertDefined(value, message);
300
+ return value;
301
+ }
302
+
303
+ /**
304
+ * Ensures that a value is not null or undefined, returning it if valid or throwing an error if empty.
305
+ *
306
+ * @template T - The type of the value being checked
307
+ * @param value - The value to ensure is not null or undefined
308
+ * @param message - Optional error message to use if the value is null or undefined
309
+ * @returns The value if it is not null or undefined
310
+ * @throws {Error} When the value is null or undefined
311
+ * @example
312
+ * ```typescript
313
+ * function processOptionalData(data?: string | null) {
314
+ * const validData = ensureNotEmpty(data);
315
+ * return validData.toUpperCase(); // validData is guaranteed to be not null/undefined
316
+ * }
317
+ * ```
318
+ */ function ensureNotEmpty(value, message) {
319
+ assertNotEmpty(value, message);
189
320
  return value;
190
321
  }
191
322
 
192
323
  /**
193
324
  * Ensures that a value is not null, returning it if not null or throwing an error if null.
194
325
  * Unlike assertNotNull, this function returns the value for use in expressions.
195
- *
326
+ *
196
327
  * @template T - The type of the value being checked
197
328
  * @param value - The value to ensure is not null
198
329
  * @param message - Optional error message to use if the value is null
@@ -210,224 +341,161 @@ function transformDeep(value, mode) {
210
341
  return value;
211
342
  }
212
343
 
344
+ function _extends() {
345
+ _extends = Object.assign || function assign(target) {
346
+ for(var i = 1; i < arguments.length; i++){
347
+ var source = arguments[i];
348
+ for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
349
+ }
350
+ return target;
351
+ };
352
+ return _extends.apply(this, arguments);
353
+ }
354
+
355
+ function transformFirst(value, transformFn) {
356
+ if (value.length === 0) {
357
+ return "";
358
+ }
359
+ return transformFn(value[0]) + value.slice(1);
360
+ }
213
361
  /**
214
- * Ensures that a value is not null or undefined, returning it if valid or throwing an error if empty.
215
- *
216
- * @template T - The type of the value being checked
217
- * @param value - The value to ensure is not null or undefined
218
- * @param message - Optional error message to use if the value is null or undefined
219
- * @returns The value if it is not null or undefined
220
- * @throws {Error} When the value is null or undefined
362
+ * Converts the first character of a string to lowercase.
363
+ *
364
+ * @param value - The string to transform
365
+ * @returns The string with the first character in lowercase
221
366
  * @example
222
367
  * ```typescript
223
- * function processOptionalData(data?: string | null) {
224
- * const validData = ensureNotEmpty(data);
225
- * return validData.toUpperCase(); // validData is guaranteed to be not null/undefined
226
- * }
368
+ * const result = toLowerFirst('UserName');
369
+ * // Result: 'userName'
227
370
  * ```
228
- */ function ensureNotEmpty(value, message) {
229
- assertNotEmpty(value, message);
230
- return value;
231
- }
232
-
233
- function downloadFile(dataOrUrl, options = {}) {
234
- if (typeof dataOrUrl === "string") {
235
- const { name } = options;
236
- const a = document.createElement("a");
237
- a.href = dataOrUrl;
238
- a.target = "_blank";
239
- if (name) a.download = name;
240
- a.click();
241
- } else {
242
- const url = URL.createObjectURL(dataOrUrl);
243
- downloadFile(url, options);
244
- URL.revokeObjectURL(url);
245
- }
371
+ */ function toLowerFirst(value) {
372
+ return transformFirst(value, (value)=>value.toLowerCase());
246
373
  }
247
-
248
374
  /**
249
- * React hook for tracking async task execution with loading state.
250
- * Automatically manages a loading counter and provides a wrapper function for tasks.
251
- *
252
- * @returns A tuple containing [isLoading: boolean, runInTask: function]
375
+ * Converts the first character of a string to uppercase.
376
+ *
377
+ * @param value - The string to transform
378
+ * @returns The string with the first character in uppercase
253
379
  * @example
254
380
  * ```typescript
255
- * function MyComponent() {
256
- * const [isLoading, runInTask] = useRunInTask();
257
- *
258
- * const handleSave = async () => {
259
- * await runInTask(async () => {
260
- * await saveData();
261
- * });
262
- * };
263
- *
264
- * return (
265
- * <button onClick={handleSave} disabled={isLoading}>
266
- * {isLoading ? 'Saving...' : 'Save'}
267
- * </button>
268
- * );
269
- * }
381
+ * const result = toUpperFirst('userName');
382
+ * // Result: 'UserName'
270
383
  * ```
271
- */ function useRunInTask() {
272
- const [runningTasks, setRunningTasks] = useState(0);
273
- const runInTask = useCallback(async (task)=>{
274
- setRunningTasks((runningTasks)=>runningTasks + 1);
275
- try {
276
- return await task();
277
- } finally{
278
- setRunningTasks((runningTasks)=>runningTasks - 1);
279
- }
280
- }, []);
281
- return [
282
- runningTasks > 0,
283
- runInTask
284
- ];
384
+ */ function toUpperFirst(value) {
385
+ return transformFirst(value, (value)=>value.toUpperCase());
285
386
  }
286
387
 
287
- function useBoundRunInTask(block) {
288
- const [isRunning, runInTask] = useRunInTask();
289
- const runBlockInTask = useMemo(()=>block ? (...args)=>runInTask(()=>block(...args)) : undefined, [
290
- block,
291
- runInTask
292
- ]);
293
- return [
294
- isRunning,
295
- runBlockInTask
296
- ];
388
+ function transformDeep(value, mode) {
389
+ if (value === null || value === undefined) {
390
+ return undefined;
391
+ }
392
+ if (Array.isArray(value)) {
393
+ return value.map((val)=>transformDeep(val, mode));
394
+ }
395
+ if (typeof value === "object") {
396
+ const transformKey = mode === "capitalize" ? toUpperFirst : toLowerFirst;
397
+ return Object.entries(value).reduce((accumulator, [key, value])=>_extends({}, accumulator, {
398
+ [transformKey(key)]: transformDeep(value, mode)
399
+ }), {});
400
+ }
401
+ return value;
297
402
  }
298
-
299
403
  /**
300
- * React hook for generating keys based on current route matches.
301
- *
302
- * @template TKey - The type of the route keys
303
- * @param routeMatches - Record of route keys to match objects or arrays
304
- * @returns Array of active route keys
404
+ * Recursively transforms all object keys to use uncapitalized (camelCase) format.
405
+ *
406
+ * @template T - The type of the input value
407
+ * @param value - The value to transform (can be object, array, or primitive)
408
+ * @returns A new object with all keys converted to camelCase
305
409
  * @example
306
410
  * ```typescript
307
- * function NavigationComponent() {
308
- * const routeMatches = {
309
- * home: useRouteMatch('/'),
310
- * about: useRouteMatch('/about'),
311
- * contact: useRouteMatch('/contact')
312
- * };
313
- *
314
- * const activeRoutes = useKeyByRoute(routeMatches);
315
- * // Returns ['home'] if on home page, ['about'] if on about page, etc.
316
- *
317
- * return (
318
- * <nav>
319
- * {activeRoutes.map(route => (
320
- * <span key={route}>Active: {route}</span>
321
- * ))}
322
- * </nav>
323
- * );
324
- * }
411
+ * const serverData = { UserId: 1, UserName: 'John', Profile: { FirstName: 'John' } };
412
+ * const clientData = uncapitalizeDeep(serverData);
413
+ * // Result: { userId: 1, userName: 'John', profile: { firstName: 'John' } }
325
414
  * ```
326
- */ function useKeyByRoute(routeMatches) {
327
- const keys = [];
328
- for(const key in routeMatches){
329
- const matches = routeMatches[key];
330
- if (Array.isArray(matches) ? matches.some((match)=>match !== null) : matches !== null) {
331
- keys.push(key);
332
- }
333
- }
334
- return keys;
415
+ */ function uncapitalizeDeep(value) {
416
+ return transformDeep(value, "uncapitalize");
335
417
  }
336
-
337
418
  /**
338
- * React hook for boolean state management helpers.
339
- *
340
- * @param set - The state setter function from useState
341
- * @returns A tuple containing [setTrue: function, setFalse: function]
419
+ * Recursively transforms all object keys to use capitalized (PascalCase) format.
420
+ *
421
+ * @template T - The type of the input value
422
+ * @param value - The value to transform (can be object, array, or primitive)
423
+ * @returns A new object with all keys converted to PascalCase
342
424
  * @example
343
425
  * ```typescript
344
- * function MyComponent() {
345
- * const [isVisible, setIsVisible] = useState(false);
346
- * const [show, hide] = useSetUnset(setIsVisible);
347
- *
348
- * return (
349
- * <div>
350
- * <button onClick={show}>Show</button>
351
- * <button onClick={hide}>Hide</button>
352
- * {isVisible && <div>Content is visible</div>}
353
- * </div>
354
- * );
355
- * }
426
+ * const clientData = { userId: 1, userName: 'John', profile: { firstName: 'John' } };
427
+ * const serverData = capitalizeDeep(clientData);
428
+ * // Result: { UserId: 1, UserName: 'John', Profile: { FirstName: 'John' } }
356
429
  * ```
357
- */ function useSetUnset(set) {
358
- return [
359
- useCallback(()=>set(true), [
360
- set
361
- ]),
362
- useCallback(()=>set(false), [
363
- set
364
- ])
365
- ];
430
+ */ function capitalizeDeep(value) {
431
+ return transformDeep(value, "capitalize");
366
432
  }
367
433
 
368
434
  /**
369
- * React hook for managing dialog state with optional callback after closing.
370
- * Provides convenient open/close functions and tracks the dialog's open state.
371
- *
372
- * @param onAfterClose - Optional callback function to execute after the dialog closes
373
- * @returns Object containing dialog state and control functions
435
+ * Creates a React context hook for managing optional state values with Provider and setter utilities.
436
+ * Returns a hook with attached Provider component and set function for declarative value management.
437
+ *
438
+ * @returns Hook function with attached Provider component and set function
374
439
  * @example
375
440
  * ```typescript
376
- * function MyComponent() {
377
- * const { isDialogOpen, openDialog, closeDialog } = useDialog(() => {
378
- * console.log('Dialog closed');
379
- * });
380
- *
441
+ * import { mkValueContext } from "@leancodepl/utils";
442
+ *
443
+ * const useTheme = mkValueContext<string>();
444
+ *
445
+ * function App() {
381
446
  * return (
382
- * <div>
383
- * <button onClick={openDialog}>Open Dialog</button>
384
- * {isDialogOpen && <Dialog onClose={closeDialog} />}
385
- * </div>
447
+ * <useTheme.Provider initialValue="dark">
448
+ * <ThemeConsumer />
449
+ * </useTheme.Provider>
386
450
  * );
387
451
  * }
388
- * ```
389
- */ function useDialog(onAfterClose) {
390
- const [isDialogOpen, setIsDialogOpen] = useState(false);
391
- const [openDialog, closeDialog] = useSetUnset(setIsDialogOpen);
392
- const close = useCallback(()=>{
393
- closeDialog();
394
- if (onAfterClose) setTimeout(onAfterClose);
395
- }, [
396
- closeDialog,
397
- onAfterClose
398
- ]);
399
- return {
400
- isDialogOpen,
401
- openDialog,
402
- closeDialog: close
403
- };
404
- }
405
-
406
- /**
407
- * Synchronizes external state changes with a callback function, calling the callback only when state actually changes.
408
452
  *
409
- * @param state - The current state value to monitor for changes
410
- * @param onChange - Callback function executed when state changes
411
- * @param compare - Optional comparison function to determine if state has changed (defaults to strict equality)
453
+ * function ThemeConsumer() {
454
+ * const [theme] = useTheme();
455
+ * return <div>Current theme: {theme}</div>;
456
+ * }
457
+ * ```
412
458
  * @example
413
459
  * ```typescript
414
- * import { useSyncState } from "@leancodepl/utils";
460
+ * // Using set to declaratively set context value
461
+ * const useActiveUser = mkValueContext<string>();
415
462
  *
416
- * function MyComponent({ externalValue }: { externalValue: string }) {
417
- * useSyncState(externalValue, (newValue) => {
418
- * console.log('Value changed to:', newValue);
419
- * });
463
+ * function UserProfile({ userId }: { userId: string }) {
464
+ * useActiveUser.set(userId); // Sets value on mount, clears on unmount
465
+ * return <div>Profile content</div>;
466
+ * }
420
467
  *
421
- * return <div>{externalValue}</div>;
468
+ * function UserBadge() {
469
+ * const [activeUserId] = useActiveUser();
470
+ * return <div>Active user: {activeUserId}</div>;
422
471
  * }
423
472
  * ```
424
- */ function useSyncState(state, onChange, compare) {
425
- const [prevState, setPrevState] = useState(state);
426
- const hasChanged = compare ? !compare(state, prevState) : state !== prevState;
427
- if (hasChanged) {
428
- setPrevState(state);
429
- onChange(state);
473
+ */ function mkValueContext() {
474
+ const valueContext = /*#__PURE__*/ createContext([
475
+ undefined,
476
+ ()=>void 0
477
+ ]);
478
+ function useValueContext() {
479
+ return useContext(valueContext);
430
480
  }
481
+ useValueContext.Provider = function ValueContextProvider({ children, initialValue }) {
482
+ const contextValue = useState(initialValue);
483
+ return /*#__PURE__*/ jsx(valueContext.Provider, {
484
+ value: contextValue,
485
+ children: children
486
+ });
487
+ };
488
+ useValueContext.set = function useSetter(value) {
489
+ const [, setValue] = useValueContext();
490
+ useEffect(()=>void setValue(value), [
491
+ setValue,
492
+ value
493
+ ]);
494
+ useEffect(()=>()=>setValue(undefined), [
495
+ setValue
496
+ ]);
497
+ };
498
+ return useValueContext;
431
499
  }
432
500
 
433
- export { addPrefix, assertDefined, assertNotEmpty, assertNotNull, capitalizeDeep, downloadFile, ensureDefined, ensureNotEmpty, ensureNotNull, toLowerFirst, toUpperFirst, uncapitalizeDeep, useBoundRunInTask, useDialog, useKeyByRoute, useRunInTask, useSetUnset, useSyncState };
501
+ export { addPrefix, assertDefined, assertNotEmpty, assertNotNull, capitalizeDeep, downloadFile, ensureDefined, ensureNotEmpty, ensureNotNull, mkValueContext, toLowerFirst, toUpperFirst, uncapitalizeDeep, useBoundRunInTask, useDialog, useKeyByRoute, useRunInTask, useSetUnset, useSyncState };