@kirill.konshin/react 0.0.5 → 0.0.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/src/form/form.tsx DELETED
@@ -1,178 +0,0 @@
1
- import { z } from 'zod';
2
- import { Context, createContext, useContext, useMemo, JSX, FC, memo } from 'react';
3
- import clsx from 'clsx';
4
-
5
- const nonEmpty = 'This field cannot be empty';
6
-
7
- export const stringRequired = (): z.ZodString => z.string({ error: nonEmpty }).min(1, { error: nonEmpty });
8
- export const maxLength = (schema: z.ZodString): number => schema.maxLength || 0;
9
- export const minLength = (schema: z.ZodString): number => schema.minLength || 0;
10
- export const isRequired = (schema: z.ZodString): boolean => minLength(schema) > 0;
11
-
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
- export type Validation<S extends ZodObject> =
17
- | {
18
- success: true; // this is true only if form was validated successfully
19
- data: SafeTypeOf<S>;
20
- errors?: never;
21
- }
22
- | {
23
- success: false;
24
- data?: MaybeTypeOf<S>;
25
- errors?: Errors<S>;
26
- };
27
-
28
- export const FormContext: Context<{
29
- schema: ZodObject;
30
- }> = createContext(null as never);
31
-
32
- export interface FormProps<S extends ZodObject> {
33
- schema: S;
34
- children: any;
35
- }
36
-
37
- export const Form: FC<FormProps<any>> = memo(function Form({ schema, children }) {
38
- const value = useMemo(() => ({ schema }), [schema]);
39
- return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
40
- });
41
-
42
- const getShape = <S extends ZodObject>(schema: S) =>
43
- (schema as z.ZodObject<any>).shape || (schema as z.ZodPipe<any, any>).in.shape;
44
-
45
- export function create<S extends ZodObject>(
46
- schema: S,
47
- ): {
48
- register: (
49
- name: keyof z.output<S>,
50
- data?: MaybeTypeOf<S>,
51
- errors?: Errors<S>,
52
- mui?: boolean,
53
- ) => {
54
- label?: any;
55
- helperText?: string;
56
- error?: boolean;
57
- name: keyof z.output<S>;
58
- id: keyof z.output<S>;
59
- required: boolean;
60
- maxLength: number;
61
- type: string;
62
- defaultValue?: Partial<z.output<S>>[keyof z.output<S>];
63
- };
64
- validate: (formData: FormData) => Validation<S>;
65
- validationError: (data: MaybeTypeOf<S>, errors: Errors<S>) => Validation<S>;
66
- } {
67
- if (!getShape(schema)) {
68
- throw new Error('Invalid schema: only z.object() or z.object().refine() are supported');
69
- }
70
-
71
- function register(
72
- name: keyof z.output<S>,
73
- data?: MaybeTypeOf<S>,
74
- errors?: Errors<S>,
75
- mui: boolean = false,
76
- ): {
77
- label?: any;
78
- helperText?: string;
79
- error?: boolean;
80
- name: keyof z.output<S>;
81
- id: keyof z.output<S>;
82
- required: boolean;
83
- maxLength: number;
84
- type: string;
85
- defaultValue?: Partial<z.output<S>>[keyof z.output<S>];
86
- } {
87
- const field = getShape(schema)[name];
88
- return {
89
- name,
90
- id: name,
91
- required: isRequired(field),
92
- maxLength: maxLength(field),
93
- type: field.isEmail ? 'email' : (name as string).includes('password') ? 'password' : 'text',
94
- defaultValue: data?.[name],
95
- ...(mui
96
- ? {
97
- label: field.description,
98
- helperText: errors?.[name]?.join(', '),
99
- error: !!errors?.[name]?.length,
100
- }
101
- : {}),
102
- };
103
- }
104
-
105
- function validationError(data: MaybeTypeOf<S>, errors: Errors<S>): Validation<S> {
106
- return {
107
- success: false,
108
- data, // data is undefined if there are errors
109
- errors, // Next.js will butcher error object, so we provide something more primitive
110
- };
111
- }
112
-
113
- function validate(formData: FormData): Validation<S> {
114
- const rawData = Object.fromEntries(formData) as z.output<S>;
115
- const result = schema.safeParse(rawData);
116
-
117
- // console.log('Validate result', { error, data, rawData });
118
-
119
- if (!result.success) {
120
- // data is undefined if there are errors
121
- // Next.js will butcher error object, so we provide something more primitive
122
- return validationError(rawData, z.flattenError(result.error).fieldErrors as any);
123
- }
124
-
125
- return { success: true, data: result.data };
126
- }
127
-
128
- return { register, validate, validationError };
129
- }
130
-
131
- interface FieldProps<S extends ZodObject> {
132
- children?: any;
133
- name: keyof z.output<S>;
134
- errors?: Validation<S>['errors'];
135
- hint?: string;
136
- className?: string;
137
- labelProps?: any;
138
- [key: string]: any;
139
- }
140
-
141
- export const Field: FC<FieldProps<any>> = memo(function Field({
142
- children,
143
- name,
144
- errors,
145
- hint,
146
- className,
147
- labelProps,
148
- ...props
149
- }) {
150
- const { schema } = useContext(FormContext);
151
- const { description } = getShape(schema)[name];
152
-
153
- return (
154
- <div {...props} className={clsx('form-row', className)}>
155
- {description && (
156
- <label {...labelProps} htmlFor={name}>
157
- {description}
158
- </label>
159
- )}
160
- {children}
161
- {hint && <Hint>{hint}</Hint>}
162
- {errors?.[name as any]?.map((e: string) => (
163
- <Hint error key={e}>
164
- {e}
165
- </Hint>
166
- ))}
167
- </div>
168
- );
169
- });
170
-
171
- export interface HintProps {
172
- children: any;
173
- error?: boolean;
174
- }
175
-
176
- export const Hint: FC<HintProps> = memo(function Hint({ children, error }) {
177
- return <div className={`hint ${error ? 'hint-error' : ''}`}>{children}</div>;
178
- });
package/src/form/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './client';
2
- export * from './form';
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from './apiCall';
2
- export * from './keyboard';
3
- export * from './useFetch';
4
- export * from './useFetchAction';
5
- export * from './form';
package/src/keyboard.tsx DELETED
@@ -1,73 +0,0 @@
1
- 'use client';
2
-
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';
15
-
16
- const isCtrlOrMeta = (e: KeyboardEvent) => e.metaKey || e.ctrlKey;
17
-
18
- const EVENT = 'keydown';
19
-
20
- export type Hotkeys = Record<KeyboardEvent['code'], (e: KeyboardEvent) => void>;
21
-
22
- export type HotkeyContextType = {
23
- enabled: boolean;
24
- setEnabled: Dispatch<SetStateAction<boolean>>;
25
- };
26
-
27
- export const HotkeysContext: Context<HotkeyContextType> = createContext<{
28
- enabled: boolean;
29
- setEnabled: Dispatch<SetStateAction<boolean>>;
30
- }>(null as never);
31
-
32
- export type HotkeysProviderProps = {
33
- children: any;
34
- };
35
-
36
- export const HotkeysProvider: FC<HotkeysProviderProps> = memo(function HotkeysProvider({ children }) {
37
- const [enabled, setEnabled] = useState(true);
38
-
39
- const control = useMemo(() => ({ enabled, setEnabled }), [enabled, setEnabled]);
40
-
41
- return <HotkeysContext.Provider value={control}>{children}</HotkeysContext.Provider>;
42
- });
43
-
44
- export const useHotkeys = (hotkeys: Hotkeys): void => {
45
- const { enabled } = useContext(HotkeysContext);
46
-
47
- useEffect(() => {
48
- if (typeof document === 'undefined' || !enabled) {
49
- return;
50
- }
51
-
52
- const ctrl = new AbortController();
53
-
54
- window.addEventListener(
55
- EVENT,
56
- (e: KeyboardEvent) => {
57
- if (!isCtrlOrMeta(e)) return;
58
-
59
- for (const [code, callback] of Object.entries(hotkeys)) {
60
- if (e.code === code) {
61
- callback(e);
62
- return;
63
- }
64
- }
65
- },
66
- { signal: ctrl.signal, capture: true },
67
- );
68
-
69
- return () => {
70
- ctrl.abort();
71
- };
72
- }, [hotkeys, enabled]);
73
- };
@@ -1,424 +0,0 @@
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
- });