@kirill.konshin/react 0.0.1 → 0.0.3

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/src/form/form.tsx CHANGED
@@ -1,18 +1,18 @@
1
- import { typeToFlattenedError, z, SafeParseReturnType, TypeOf } from 'zod';
1
+ import { z } from 'zod';
2
2
  import { Context, createContext, useContext, useMemo, JSX, FC, memo } from 'react';
3
3
  import clsx from 'clsx';
4
4
 
5
5
  const nonEmpty = 'This field cannot be empty';
6
6
 
7
- export const stringRequired = (): z.ZodString => z.string({ required_error: nonEmpty }).min(1, nonEmpty);
7
+ export const stringRequired = (): z.ZodString => z.string({ error: nonEmpty }).min(1, { error: nonEmpty });
8
8
  export const maxLength = (schema: z.ZodString): number => schema.maxLength || 0;
9
9
  export const minLength = (schema: z.ZodString): number => schema.minLength || 0;
10
10
  export const isRequired = (schema: z.ZodString): boolean => minLength(schema) > 0;
11
11
 
12
- export type ZodObject = z.ZodObject<any> | z.ZodEffects<z.ZodObject<any>>; // z.ZodType<any, any, any>
13
- export type MaybeTypeOf<S extends ZodObject> = Partial<TypeOf<S>>;
14
- export type SafeTypeOf<S extends ZodObject> = SafeParseReturnType<TypeOf<S>, TypeOf<S>>['data'];
15
- export type Errors<S extends ZodObject> = typeToFlattenedError<TypeOf<S>>['fieldErrors'];
12
+ export type ZodObject = z.ZodObject<any> | z.ZodPipe<any, any>; // z.ZodType<any, any, any>
13
+ export type MaybeTypeOf<S extends ZodObject> = Partial<z.output<S>>;
14
+ export type SafeTypeOf<S extends ZodObject> = z.core.util.SafeParseResult<z.output<S>>['data'];
15
+ export type Errors<S extends ZodObject> = z.core.$ZodFlattenedError<z.output<S>>['fieldErrors'];
16
16
  export type Validation<S extends ZodObject> =
17
17
  | {
18
18
  success: true; // this is true only if form was validated successfully
@@ -40,13 +40,13 @@ export const Form: FC<FormProps<any>> = memo(function Form({ schema, children })
40
40
  });
41
41
 
42
42
  const getShape = <S extends ZodObject>(schema: S) =>
43
- (schema as z.ZodObject<any>).shape || (schema as z.ZodEffects<z.ZodObject<any>>).sourceType().shape;
43
+ (schema as z.ZodObject<any>).shape || (schema as z.ZodPipe<any, any>).in.shape;
44
44
 
45
45
  export function create<S extends ZodObject>(
46
46
  schema: S,
47
47
  ): {
48
48
  register: (
49
- name: keyof TypeOf<S>,
49
+ name: keyof z.output<S>,
50
50
  data?: MaybeTypeOf<S>,
51
51
  errors?: Errors<S>,
52
52
  mui?: boolean,
@@ -54,12 +54,12 @@ export function create<S extends ZodObject>(
54
54
  label?: any;
55
55
  helperText?: string;
56
56
  error?: boolean;
57
- name: keyof z.TypeOf<S>;
58
- id: keyof z.TypeOf<S>;
57
+ name: keyof z.output<S>;
58
+ id: keyof z.output<S>;
59
59
  required: boolean;
60
60
  maxLength: number;
61
61
  type: string;
62
- defaultValue?: Partial<z.TypeOf<S>>[keyof z.TypeOf<S>];
62
+ defaultValue?: Partial<z.output<S>>[keyof z.output<S>];
63
63
  };
64
64
  validate: (formData: FormData) => Validation<S>;
65
65
  validationError: (data: MaybeTypeOf<S>, errors: Errors<S>) => Validation<S>;
@@ -69,7 +69,7 @@ export function create<S extends ZodObject>(
69
69
  }
70
70
 
71
71
  function register(
72
- name: keyof TypeOf<S>,
72
+ name: keyof z.output<S>,
73
73
  data?: MaybeTypeOf<S>,
74
74
  errors?: Errors<S>,
75
75
  mui: boolean = false,
@@ -77,12 +77,12 @@ export function create<S extends ZodObject>(
77
77
  label?: any;
78
78
  helperText?: string;
79
79
  error?: boolean;
80
- name: keyof z.TypeOf<S>;
81
- id: keyof z.TypeOf<S>;
80
+ name: keyof z.output<S>;
81
+ id: keyof z.output<S>;
82
82
  required: boolean;
83
83
  maxLength: number;
84
84
  type: string;
85
- defaultValue?: Partial<z.TypeOf<S>>[keyof z.TypeOf<S>];
85
+ defaultValue?: Partial<z.output<S>>[keyof z.output<S>];
86
86
  } {
87
87
  const field = getShape(schema)[name];
88
88
  return {
@@ -111,18 +111,18 @@ export function create<S extends ZodObject>(
111
111
  }
112
112
 
113
113
  function validate(formData: FormData): Validation<S> {
114
- const rawData = Object.fromEntries(formData) as TypeOf<S>;
115
- const { error, data } = schema.safeParse(rawData);
114
+ const rawData = Object.fromEntries(formData) as z.output<S>;
115
+ const result = schema.safeParse(rawData);
116
116
 
117
117
  // console.log('Validate result', { error, data, rawData });
118
118
 
119
- if (error) {
119
+ if (!result.success) {
120
120
  // data is undefined if there are errors
121
121
  // Next.js will butcher error object, so we provide something more primitive
122
- return validationError(rawData, error.flatten().fieldErrors as any);
122
+ return validationError(rawData, z.flattenError(result.error).fieldErrors as any);
123
123
  }
124
124
 
125
- return { success: true, data };
125
+ return { success: true, data: result.data };
126
126
  }
127
127
 
128
128
  return { register, validate, validationError };
@@ -130,7 +130,7 @@ export function create<S extends ZodObject>(
130
130
 
131
131
  interface FieldProps<S extends ZodObject> {
132
132
  children?: any;
133
- name: keyof TypeOf<S>;
133
+ name: keyof z.output<S>;
134
134
  errors?: Validation<S>['errors'];
135
135
  hint?: string;
136
136
  className?: string;
@@ -159,7 +159,7 @@ export const Field: FC<FieldProps<any>> = memo(function Field({
159
159
  )}
160
160
  {children}
161
161
  {hint && <Hint>{hint}</Hint>}
162
- {errors?.[name]?.map((e: string) => (
162
+ {errors?.[name as any]?.map((e: string) => (
163
163
  <Hint error key={e}>
164
164
  {e}
165
165
  </Hint>
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- export * from './form';
2
1
  export * from './apiCall';
3
2
  export * from './keyboard';
4
3
  export * from './useFetch';
5
4
  export * from './useFetcher';
5
+ export * from './form';
package/src/keyboard.tsx CHANGED
@@ -1,6 +1,17 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, createContext, useMemo, useState, Dispatch, SetStateAction, useContext, FC, Context } from 'react';
3
+ import {
4
+ useEffect,
5
+ createContext,
6
+ useMemo,
7
+ useState,
8
+ Dispatch,
9
+ SetStateAction,
10
+ useContext,
11
+ FC,
12
+ memo,
13
+ Context,
14
+ } from 'react';
4
15
 
5
16
  const isCtrlOrMeta = (e: KeyboardEvent) => e.metaKey || e.ctrlKey;
6
17
 
@@ -18,13 +29,17 @@ export const HotkeysContext: Context<HotkeyContextType> = createContext<{
18
29
  setEnabled: Dispatch<SetStateAction<boolean>>;
19
30
  }>(null as never);
20
31
 
21
- export const HotkeysProvider: FC<any> = ({ children }) => {
32
+ export type HotkeysProviderProps = {
33
+ children: any;
34
+ };
35
+
36
+ export const HotkeysProvider: FC<HotkeysProviderProps> = memo(function HotkeysProvider({ children }) {
22
37
  const [enabled, setEnabled] = useState(true);
23
38
 
24
39
  const control = useMemo(() => ({ enabled, setEnabled }), [enabled, setEnabled]);
25
40
 
26
41
  return <HotkeysContext.Provider value={control}>{children}</HotkeysContext.Provider>;
27
- };
42
+ });
28
43
 
29
44
  export const useHotkeys = (hotkeys: Hotkeys): void => {
30
45
  const { enabled } = useContext(HotkeysContext);
@@ -0,0 +1,424 @@
1
+ import { expect, describe, test, vi } from 'vitest';
2
+ import { renderHook, waitFor } from '@testing-library/react';
3
+ import { useFetch } from './useFetch';
4
+ import { act } from 'react';
5
+
6
+ describe('useFetch', () => {
7
+ test('initializes with default value', () => {
8
+ const mockFn = vi.fn(async () => 'result');
9
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
10
+
11
+ const [data, actionFn, isPending, error] = result.current;
12
+
13
+ expect(data).toBe('default');
14
+ expect(typeof actionFn).toBe('function');
15
+ expect(isPending).toBe(false);
16
+ expect(error).toBeUndefined();
17
+ });
18
+
19
+ test('executes function and updates data on success', async () => {
20
+ const mockFn = vi.fn(async () => 'success');
21
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
22
+
23
+ await act(async () => {
24
+ result.current[1]();
25
+ });
26
+
27
+ await waitFor(() => {
28
+ expect(result.current[2]).toBe(false); // isPending becomes false
29
+ });
30
+
31
+ expect(result.current[0]).toBe('success');
32
+ expect(result.current[3]).toBeUndefined();
33
+ expect(mockFn).toHaveBeenCalledTimes(1);
34
+ });
35
+
36
+ test('handles function arguments', async () => {
37
+ const mockFn = vi.fn(async (a: number, b: string) => `${a}-${b}`);
38
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
39
+
40
+ await act(async () => {
41
+ result.current[1](42, 'test');
42
+ });
43
+
44
+ await waitFor(() => {
45
+ expect(result.current[0]).toBe('42-test');
46
+ });
47
+
48
+ expect(mockFn).toHaveBeenCalledWith(42, 'test');
49
+ });
50
+
51
+ test('sets isPending to true during execution', async () => {
52
+ let resolvePromise: (value: string) => void;
53
+ const promise = new Promise<string>((resolve) => {
54
+ resolvePromise = resolve;
55
+ });
56
+ const mockFn = vi.fn(async () => promise);
57
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
58
+
59
+ act(() => {
60
+ result.current[1]();
61
+ });
62
+
63
+ // Should be pending immediately
64
+ expect(result.current[2]).toBe(true);
65
+
66
+ await act(async () => {
67
+ resolvePromise!('resolved');
68
+ await promise;
69
+ });
70
+
71
+ await waitFor(() => {
72
+ expect(result.current[2]).toBe(false);
73
+ });
74
+
75
+ expect(result.current[0]).toBe('resolved');
76
+ });
77
+
78
+ test('captures and stores errors', async () => {
79
+ const error = new Error('Test error');
80
+ const mockFn = vi.fn(async () => {
81
+ throw error;
82
+ });
83
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
84
+
85
+ await act(async () => {
86
+ result.current[1]();
87
+ });
88
+
89
+ await waitFor(() => {
90
+ expect(result.current[3]).toBe(error);
91
+ });
92
+
93
+ expect(result.current[0]).toBe('default'); // data remains unchanged
94
+ expect(result.current[2]).toBe(false); // isPending becomes false
95
+ });
96
+
97
+ test('does not update data if component unmounts before resolution', async () => {
98
+ let resolvePromise: (value: string) => void;
99
+ const promise = new Promise<string>((resolve) => {
100
+ resolvePromise = resolve;
101
+ });
102
+ const mockFn = vi.fn(async () => promise);
103
+ const { result, unmount } = renderHook(() => useFetch(mockFn, 'default'));
104
+
105
+ act(() => {
106
+ result.current[1]();
107
+ });
108
+
109
+ unmount();
110
+
111
+ await act(async () => {
112
+ resolvePromise!('resolved');
113
+ await promise;
114
+ });
115
+
116
+ // No assertions needed - just verifying no errors on unmount
117
+ expect(mockFn).toHaveBeenCalledTimes(1);
118
+ });
119
+
120
+ test('returns the promise from actionFn', async () => {
121
+ const mockFn = vi.fn(async () => 'result');
122
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
123
+
124
+ let returnedPromise: Promise<string>;
125
+ await act(async () => {
126
+ returnedPromise = result.current[1]();
127
+ });
128
+
129
+ await expect(returnedPromise!).resolves.toBe('result');
130
+ });
131
+
132
+ test('handles multiple sequential calls', async () => {
133
+ const mockFn = vi.fn(async (n: number) => `result-${n}`);
134
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
135
+
136
+ await act(async () => {
137
+ result.current[1](1);
138
+ });
139
+
140
+ await waitFor(() => {
141
+ expect(result.current[0]).toBe('result-1');
142
+ });
143
+
144
+ await act(async () => {
145
+ result.current[1](2);
146
+ });
147
+
148
+ await waitFor(() => {
149
+ expect(result.current[0]).toBe('result-2');
150
+ });
151
+
152
+ expect(mockFn).toHaveBeenCalledTimes(2);
153
+ });
154
+
155
+ test('preserves actionFn reference when fn dependency does not change', () => {
156
+ const mockFn = vi.fn(async () => 'result');
157
+ const { result, rerender } = renderHook(() => useFetch(mockFn, 'default'));
158
+
159
+ const firstActionFn = result.current[1];
160
+
161
+ rerender();
162
+
163
+ const secondActionFn = result.current[1];
164
+
165
+ expect(firstActionFn).toBe(secondActionFn);
166
+ });
167
+
168
+ test('updates actionFn when fn dependency changes', async () => {
169
+ const mockFn1 = vi.fn(async () => 'result1');
170
+ const mockFn2 = vi.fn(async () => 'result2');
171
+
172
+ const { result, rerender } = renderHook(({ fn }) => useFetch(fn, 'default'), {
173
+ initialProps: { fn: mockFn1 },
174
+ });
175
+
176
+ const firstActionFn = result.current[1];
177
+
178
+ await act(async () => {
179
+ result.current[1]();
180
+ });
181
+
182
+ await waitFor(() => {
183
+ expect(result.current[0]).toBe('result1');
184
+ });
185
+
186
+ // Change the function
187
+ rerender({ fn: mockFn2 });
188
+
189
+ const secondActionFn = result.current[1];
190
+
191
+ expect(firstActionFn).not.toBe(secondActionFn);
192
+
193
+ await act(async () => {
194
+ result.current[1]();
195
+ });
196
+
197
+ await waitFor(() => {
198
+ expect(result.current[0]).toBe('result2');
199
+ });
200
+
201
+ expect(mockFn1).toHaveBeenCalledTimes(1);
202
+ expect(mockFn2).toHaveBeenCalledTimes(1);
203
+ });
204
+
205
+ test('does not trigger execution on rerender', () => {
206
+ const mockFn = vi.fn(async () => 'result');
207
+ const { rerender } = renderHook(() => useFetch(mockFn, 'default'));
208
+
209
+ rerender();
210
+ rerender();
211
+ rerender();
212
+
213
+ expect(mockFn).not.toHaveBeenCalled();
214
+ });
215
+
216
+ test('handles concurrent calls correctly', async () => {
217
+ let callCount = 0;
218
+ const mockFn = vi.fn(async () => {
219
+ callCount++;
220
+ const currentCall = callCount;
221
+ await new Promise((resolve) => setTimeout(resolve, 10));
222
+ return `result-${currentCall}`;
223
+ });
224
+
225
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
226
+
227
+ // Fire multiple calls at once
228
+ await act(async () => {
229
+ result.current[1]();
230
+ result.current[1]();
231
+ result.current[1]();
232
+ });
233
+
234
+ await waitFor(() => {
235
+ expect(result.current[2]).toBe(false);
236
+ });
237
+
238
+ // Last call should win
239
+ expect(mockFn).toHaveBeenCalledTimes(3);
240
+ expect(['result-1', 'result-2', 'result-3']).toContain(result.current[0]);
241
+ });
242
+
243
+ test('maintains separate state for different hook instances', async () => {
244
+ const mockFn1 = vi.fn(async () => 'result1');
245
+ const mockFn2 = vi.fn(async () => 'result2');
246
+
247
+ const { result: result1 } = renderHook(() => useFetch(mockFn1, 'default1'));
248
+ const { result: result2 } = renderHook(() => useFetch(mockFn2, 'default2'));
249
+
250
+ expect(result1.current[0]).toBe('default1');
251
+ expect(result2.current[0]).toBe('default2');
252
+
253
+ await act(async () => {
254
+ result1.current[1]();
255
+ });
256
+
257
+ await waitFor(() => {
258
+ expect(result1.current[0]).toBe('result1');
259
+ });
260
+
261
+ expect(result2.current[0]).toBe('default2'); // unchanged
262
+
263
+ await act(async () => {
264
+ result2.current[1]();
265
+ });
266
+
267
+ await waitFor(() => {
268
+ expect(result2.current[0]).toBe('result2');
269
+ });
270
+
271
+ expect(result1.current[0]).toBe('result1'); // still unchanged
272
+ });
273
+
274
+ test('handles closure over external state', async () => {
275
+ let externalValue = 10;
276
+
277
+ const { result, rerender } = renderHook(() => {
278
+ const mockFn = async () => `value-${externalValue}`;
279
+ return useFetch(mockFn, 'default');
280
+ });
281
+
282
+ await act(async () => {
283
+ result.current[1]();
284
+ });
285
+
286
+ await waitFor(() => {
287
+ expect(result.current[0]).toBe('value-10');
288
+ });
289
+
290
+ // Change external value and rerender
291
+ externalValue = 20;
292
+ rerender();
293
+
294
+ await act(async () => {
295
+ result.current[1]();
296
+ });
297
+
298
+ await waitFor(() => {
299
+ expect(result.current[0]).toBe('value-20');
300
+ });
301
+ });
302
+
303
+ test('clears error on successful subsequent call', async () => {
304
+ const error = new Error('Test error');
305
+ let shouldError = true;
306
+
307
+ const mockFn = vi.fn(async () => {
308
+ if (shouldError) {
309
+ throw error;
310
+ }
311
+ return 'success';
312
+ });
313
+
314
+ const { result } = renderHook(() => useFetch(mockFn, 'default'));
315
+
316
+ // First call errors
317
+ await act(async () => {
318
+ result.current[1]();
319
+ });
320
+
321
+ await waitFor(() => {
322
+ expect(result.current[3]).toBe(error);
323
+ });
324
+
325
+ // Second call succeeds
326
+ shouldError = false;
327
+ await act(async () => {
328
+ result.current[1]();
329
+ });
330
+
331
+ await waitFor(() => {
332
+ expect(result.current[0]).toBe('success');
333
+ });
334
+
335
+ // Error is cleared automatically
336
+ expect(result.current[3]).toBe(undefined);
337
+ });
338
+
339
+ test('handles callback that returns a function with old value', async () => {
340
+ const mockFn = vi.fn((increment: number) => (oldValue: number) => oldValue + increment);
341
+ const { result } = renderHook(() => useFetch(mockFn, 0));
342
+
343
+ // First call - should call the function with oldValue (0) and get 0 + 5 = 5
344
+ await act(async () => {
345
+ result.current[1](5);
346
+ });
347
+
348
+ await waitFor(() => {
349
+ expect(result.current[0]).toBe(5);
350
+ });
351
+
352
+ // Second call - should call the function with oldValue (5) and get 5 + 10 = 15
353
+ await act(async () => {
354
+ result.current[1](10);
355
+ });
356
+
357
+ await waitFor(() => {
358
+ expect(result.current[0]).toBe(15);
359
+ });
360
+
361
+ expect(mockFn).toHaveBeenCalledTimes(2);
362
+ });
363
+
364
+ test('handles multiple calls with old value pattern', async () => {
365
+ const mockFn = vi.fn(
366
+ (operation: string, value: number) => (oldValue: string) => `${oldValue}-${operation}${value}`,
367
+ );
368
+ const { result } = renderHook(() => useFetch(mockFn, 'initial'));
369
+
370
+ // First call
371
+ await act(async () => {
372
+ result.current[1]('add', 1);
373
+ });
374
+
375
+ await waitFor(() => {
376
+ expect(result.current[0]).toBe('initial-add1');
377
+ });
378
+
379
+ // Second call - should use the updated value
380
+ await act(async () => {
381
+ result.current[1]('multiply', 2);
382
+ });
383
+
384
+ await waitFor(() => {
385
+ expect(result.current[0]).toBe('initial-add1-multiply2');
386
+ });
387
+
388
+ expect(mockFn).toHaveBeenCalledTimes(2);
389
+ });
390
+
391
+ test('handles counter pattern with old value', async () => {
392
+ const increment = vi.fn(() => (prev: number) => prev + 1);
393
+ const { result } = renderHook(() => useFetch(increment, 0));
394
+
395
+ // First increment: 0 + 1 = 1
396
+ await act(async () => {
397
+ result.current[1]();
398
+ });
399
+
400
+ await waitFor(() => {
401
+ expect(result.current[0]).toBe(1);
402
+ });
403
+
404
+ // Second increment: 1 + 1 = 2
405
+ await act(async () => {
406
+ result.current[1]();
407
+ });
408
+
409
+ await waitFor(() => {
410
+ expect(result.current[0]).toBe(2);
411
+ });
412
+
413
+ // Third increment: 2 + 1 = 3
414
+ await act(async () => {
415
+ result.current[1]();
416
+ });
417
+
418
+ await waitFor(() => {
419
+ expect(result.current[0]).toBe(3);
420
+ });
421
+
422
+ expect(increment).toHaveBeenCalledTimes(3);
423
+ });
424
+ });