remix-validated-form 4.3.1-beta.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/browser/ValidatedForm.js +16 -30
  3. package/browser/hooks.d.ts +1 -1
  4. package/browser/hooks.js +10 -9
  5. package/browser/internal/hooks.d.ts +20 -9
  6. package/browser/internal/hooks.js +32 -23
  7. package/browser/internal/logic/getRadioChecked.js +1 -1
  8. package/browser/internal/state/cleanup.d.ts +2 -0
  9. package/browser/internal/state/cleanup.js +6 -0
  10. package/browser/internal/state/controlledFieldStore.d.ts +24 -0
  11. package/browser/internal/state/controlledFieldStore.js +57 -0
  12. package/browser/internal/state/controlledFields.d.ts +3 -116
  13. package/browser/internal/state/controlledFields.js +25 -68
  14. package/browser/internal/state/createFormStore.d.ts +40 -0
  15. package/browser/internal/state/createFormStore.js +83 -0
  16. package/browser/internal/state/storeFamily.d.ts +9 -0
  17. package/browser/internal/state/storeFamily.js +18 -0
  18. package/browser/internal/state/storeHooks.d.ts +5 -0
  19. package/browser/internal/state/storeHooks.js +10 -0
  20. package/browser/unreleased/formStateHooks.d.ts +15 -0
  21. package/browser/unreleased/formStateHooks.js +23 -14
  22. package/browser/userFacingFormContext.d.ts +8 -0
  23. package/browser/userFacingFormContext.js +5 -4
  24. package/dist/remix-validated-form.cjs.js +17 -1
  25. package/dist/remix-validated-form.es.js +1033 -1724
  26. package/dist/remix-validated-form.umd.js +17 -1
  27. package/dist/types/hooks.d.ts +1 -1
  28. package/dist/types/internal/hooks.d.ts +20 -9
  29. package/dist/types/internal/state/cleanup.d.ts +2 -0
  30. package/dist/types/internal/state/controlledFieldStore.d.ts +24 -0
  31. package/dist/types/internal/state/controlledFields.d.ts +3 -116
  32. package/dist/types/internal/state/createFormStore.d.ts +40 -0
  33. package/dist/types/internal/state/storeFamily.d.ts +9 -0
  34. package/dist/types/internal/state/storeHooks.d.ts +5 -0
  35. package/dist/types/unreleased/formStateHooks.d.ts +15 -0
  36. package/dist/types/userFacingFormContext.d.ts +8 -0
  37. package/package.json +4 -3
  38. package/src/ValidatedForm.tsx +25 -47
  39. package/src/hooks.ts +15 -18
  40. package/src/internal/hooks.ts +69 -45
  41. package/src/internal/logic/getRadioChecked.ts +1 -1
  42. package/src/internal/state/cleanup.ts +8 -0
  43. package/src/internal/state/controlledFieldStore.ts +91 -0
  44. package/src/internal/state/controlledFields.ts +31 -123
  45. package/src/internal/state/createFormStore.ts +152 -0
  46. package/src/internal/state/storeFamily.ts +24 -0
  47. package/src/internal/state/storeHooks.ts +22 -0
  48. package/src/unreleased/formStateHooks.ts +50 -27
  49. package/src/userFacingFormContext.ts +17 -5
  50. package/dist/types/internal/reset.d.ts +0 -28
  51. package/dist/types/internal/state/atomUtils.d.ts +0 -38
  52. package/dist/types/internal/state.d.ts +0 -343
  53. package/src/internal/reset.ts +0 -26
  54. package/src/internal/state/atomUtils.ts +0 -13
  55. package/src/internal/state.ts +0 -124
@@ -1,5 +1,4 @@
1
1
  import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
2
- import { useAtomCallback } from "jotai/utils";
3
2
  import uniq from "lodash/uniq";
4
3
  import React, {
5
4
  ComponentProps,
@@ -11,7 +10,6 @@ import React, {
11
10
  useRef,
12
11
  useState,
13
12
  } from "react";
14
- import invariant from "tiny-invariant";
15
13
  import { useIsSubmitting, useIsValid } from "./hooks";
16
14
  import { FORM_ID_FIELD } from "./internal/constants";
17
15
  import {
@@ -21,23 +19,16 @@ import {
21
19
  import {
22
20
  useDefaultValuesFromLoader,
23
21
  useErrorResponseForForm,
24
- useFormUpdateAtom,
25
22
  useHasActiveFormSubmit,
23
+ useSetFieldErrors,
26
24
  } from "./internal/hooks";
27
25
  import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
28
- import { resetAtom } from "./internal/reset";
26
+ import { cleanupFormState } from "./internal/state/cleanup";
27
+ import { SyncedFormProps } from "./internal/state/createFormStore";
29
28
  import {
30
- cleanupFormState,
31
- endSubmitAtom,
32
- fieldErrorsAtom,
33
- formElementAtom,
34
- formPropsAtom,
35
- isHydratedAtom,
36
- setFieldErrorAtom,
37
- startSubmitAtom,
38
- SyncedFormProps,
39
- } from "./internal/state";
40
- import { useAwaitValue } from "./internal/state/controlledFields";
29
+ useControlledFieldStore,
30
+ useFormStore,
31
+ } from "./internal/state/storeHooks";
41
32
  import { useSubmitComplete } from "./internal/submissionCallbacks";
42
33
  import {
43
34
  mergeRefs,
@@ -234,41 +225,27 @@ export function ValidatedForm<DataType>({
234
225
  const Form = fetcher?.Form ?? RemixForm;
235
226
 
236
227
  const submit = useSubmit();
237
- const setFieldErrors = useFormUpdateAtom(fieldErrorsAtom(formId));
238
- const setFieldError = useFormUpdateAtom(setFieldErrorAtom(formId));
239
- const reset = useFormUpdateAtom(resetAtom(formId));
240
- const startSubmit = useFormUpdateAtom(startSubmitAtom(formId));
241
- const endSubmit = useFormUpdateAtom(endSubmitAtom(formId));
242
- const syncFormProps = useFormUpdateAtom(formPropsAtom(formId));
243
- const setHydrated = useFormUpdateAtom(isHydratedAtom(formId));
244
- const setFormElementInState = useFormUpdateAtom(formElementAtom(formId));
228
+ const setFieldErrors = useSetFieldErrors(formId);
229
+ const setFieldError = useFormStore(formId, (state) => state.setFieldError);
230
+ const reset = useFormStore(formId, (state) => state.reset);
231
+ const resetControlledFields = useControlledFieldStore(
232
+ formId,
233
+ (state) => state.reset
234
+ );
235
+ const startSubmit = useFormStore(formId, (state) => state.startSubmit);
236
+ const endSubmit = useFormStore(formId, (state) => state.endSubmit);
237
+ const syncFormProps = useFormStore(formId, (state) => state.syncFormProps);
238
+ const setHydrated = useFormStore(formId, (state) => state.setHydrated);
239
+ const setFormElementInState = useFormStore(
240
+ formId,
241
+ (state) => state.setFormElement
242
+ );
245
243
 
246
244
  useEffect(() => {
247
- setHydrated(true);
245
+ setHydrated();
248
246
  return () => cleanupFormState(formId);
249
247
  }, [formId, setHydrated]);
250
248
 
251
- const awaitValue = useAwaitValue(formId);
252
- const validateField: SyncedFormProps["validateField"] = useCallback(
253
- async (field) => {
254
- invariant(formRef.current, "Cannot find reference to form");
255
- await awaitValue(field);
256
- const { error } = await validator.validateField(
257
- getDataFromForm(formRef.current),
258
- field
259
- );
260
-
261
- if (error) {
262
- setFieldError({ field, error });
263
- return error;
264
- } else {
265
- setFieldError({ field, error: undefined });
266
- return null;
267
- }
268
- },
269
- [awaitValue, setFieldError, validator]
270
- );
271
-
272
249
  const customFocusHandlers = useMultiValueMap<string, () => void>();
273
250
  const registerReceiveFocus: SyncedFormProps["registerReceiveFocus"] =
274
251
  useCallback(
@@ -286,8 +263,8 @@ export function ValidatedForm<DataType>({
286
263
  action,
287
264
  defaultValues: providedDefaultValues ?? backendDefaultValues ?? {},
288
265
  subaction,
289
- validateField,
290
266
  registerReceiveFocus,
267
+ validator,
291
268
  });
292
269
  }, [
293
270
  action,
@@ -295,8 +272,8 @@ export function ValidatedForm<DataType>({
295
272
  registerReceiveFocus,
296
273
  subaction,
297
274
  syncFormProps,
298
- validateField,
299
275
  backendDefaultValues,
276
+ validator,
300
277
  ]);
301
278
 
302
279
  useEffect(() => {
@@ -386,6 +363,7 @@ export function ValidatedForm<DataType>({
386
363
  onReset?.(event);
387
364
  if (event.defaultPrevented) return;
388
365
  reset();
366
+ resetControlledFields();
389
367
  }}
390
368
  >
391
369
  <InternalFormContext.Provider value={contextValue}>
package/src/hooks.ts CHANGED
@@ -8,15 +8,14 @@ import {
8
8
  useInternalFormContext,
9
9
  useFieldTouched,
10
10
  useFieldError,
11
- useFormAtomValue,
12
11
  useFieldDefaultValue,
12
+ useClearError,
13
+ useInternalIsSubmitting,
14
+ useInternalIsValid,
15
+ useInternalHasBeenSubmitted,
16
+ useValidateField,
17
+ useRegisterReceiveFocus,
13
18
  } from "./internal/hooks";
14
- import {
15
- formPropsAtom,
16
- hasBeenSubmittedAtom,
17
- isSubmittingAtom,
18
- isValidAtom,
19
- } from "./internal/state";
20
19
  import {
21
20
  useControllableValue,
22
21
  useUpdateControllableValue,
@@ -31,7 +30,7 @@ import {
31
30
  */
32
31
  export const useIsSubmitting = (formId?: string) => {
33
32
  const formContext = useInternalFormContext(formId, "useIsSubmitting");
34
- return useFormAtomValue(isSubmittingAtom(formContext.formId));
33
+ return useInternalIsSubmitting(formContext.formId);
35
34
  };
36
35
 
37
36
  /**
@@ -41,7 +40,7 @@ export const useIsSubmitting = (formId?: string) => {
41
40
  */
42
41
  export const useIsValid = (formId?: string) => {
43
42
  const formContext = useInternalFormContext(formId, "useIsValid");
44
- return useFormAtomValue(isValidAtom(formContext.formId));
43
+ return useInternalIsValid(formContext.formId);
45
44
  };
46
45
 
47
46
  export type FieldProps = {
@@ -103,14 +102,12 @@ export const useField = (
103
102
 
104
103
  const defaultValue = useFieldDefaultValue(name, formContext);
105
104
  const [touched, setTouched] = useFieldTouched(name, formContext);
106
- const [error, setError] = useFieldError(name, formContext);
105
+ const error = useFieldError(name, formContext);
106
+ const clearError = useClearError(formContext);
107
107
 
108
- const hasBeenSubmitted = useFormAtomValue(
109
- hasBeenSubmittedAtom(formContext.formId)
110
- );
111
- const { validateField, registerReceiveFocus } = useFormAtomValue(
112
- formPropsAtom(formContext.formId)
113
- );
108
+ const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId);
109
+ const validateField = useValidateField(formContext.formId);
110
+ const registerReceiveFocus = useRegisterReceiveFocus(formContext.formId);
114
111
 
115
112
  useEffect(() => {
116
113
  if (handleReceiveFocus)
@@ -120,7 +117,7 @@ export const useField = (
120
117
  const field = useMemo<FieldProps>(() => {
121
118
  const helpers = {
122
119
  error,
123
- clearError: () => setError(undefined),
120
+ clearError: () => clearError(name),
124
121
  validate: () => {
125
122
  validateField(name);
126
123
  },
@@ -140,13 +137,13 @@ export const useField = (
140
137
  };
141
138
  }, [
142
139
  error,
140
+ clearError,
143
141
  defaultValue,
144
142
  touched,
145
143
  setTouched,
146
144
  name,
147
145
  hasBeenSubmitted,
148
146
  options?.validationBehavior,
149
- setError,
150
147
  validateField,
151
148
  ]);
152
149
 
@@ -1,6 +1,4 @@
1
1
  import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
- import { Atom, useAtom, WritableAtom } from "jotai";
3
- import { useAtomValue, useUpdateAtom } from "jotai/utils";
4
2
  import lodashGet from "lodash/get";
5
3
  import { useCallback, useContext } from "react";
6
4
  import invariant from "tiny-invariant";
@@ -8,25 +6,8 @@ import { FieldErrors, ValidationErrorResponseData } from "..";
8
6
  import { formDefaultValuesKey } from "./constants";
9
7
  import { InternalFormContext, InternalFormContextValue } from "./formContext";
10
8
  import { Hydratable, hydratable } from "./hydratable";
11
- import {
12
- ATOM_SCOPE,
13
- fieldErrorAtom,
14
- fieldTouchedAtom,
15
- formPropsAtom,
16
- isHydratedAtom,
17
- setFieldErrorAtom,
18
- setTouchedAtom,
19
- } from "./state";
20
-
21
- export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
22
- useUpdateAtom(atom, ATOM_SCOPE);
23
-
24
- export const useFormAtom = <Value, Update, Result extends void | Promise<void>>(
25
- anAtom: WritableAtom<Value, Update, Result>
26
- ) => useAtom(anAtom, ATOM_SCOPE);
27
-
28
- export const useFormAtomValue = <Value>(anAtom: Atom<Value>) =>
29
- useAtomValue(anAtom, ATOM_SCOPE);
9
+ import { InternalFormId } from "./state/storeFamily";
10
+ import { useFormStore } from "./state/storeHooks";
30
11
 
31
12
  export const useInternalFormContext = (
32
13
  formId?: string | symbol,
@@ -72,7 +53,7 @@ export const useFieldErrorsForForm = (
72
53
  context: InternalFormContextValue
73
54
  ): Hydratable<FieldErrors | undefined> => {
74
55
  const response = useErrorResponseForForm(context);
75
- const hydrated = useFormAtomValue(isHydratedAtom(context.formId));
56
+ const hydrated = useFormStore(context.formId, (state) => state.isHydrated);
76
57
  return hydratable.from(response?.fieldErrors, hydrated);
77
58
  };
78
59
 
@@ -97,7 +78,7 @@ export const useDefaultValuesForForm = (
97
78
  context: InternalFormContextValue
98
79
  ): Hydratable<{ [fieldName: string]: any }> => {
99
80
  const { formId, defaultValuesProp } = context;
100
- const hydrated = useFormAtomValue(isHydratedAtom(formId));
81
+ const hydrated = useFormStore(formId, (state) => state.isHydrated);
101
82
  const errorResponse = useErrorResponseForForm(context);
102
83
  const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
103
84
 
@@ -133,20 +114,31 @@ export const useHasActiveFormSubmit = ({
133
114
  export const useFieldTouched = (
134
115
  field: string,
135
116
  { formId }: InternalFormContextValue
136
- ) => useFormAtom(fieldTouchedAtom({ formId, field }));
117
+ ) => {
118
+ const touched = useFormStore(formId, (state) => state.touchedFields[field]);
119
+ const setFieldTouched = useFormStore(formId, (state) => state.setTouched);
120
+ const setTouched = useCallback(
121
+ (touched: boolean) => setFieldTouched(field, touched),
122
+ [field, setFieldTouched]
123
+ );
124
+ return [touched, setTouched] as const;
125
+ };
137
126
 
138
127
  export const useFieldError = (
139
128
  name: string,
140
129
  context: InternalFormContextValue
141
130
  ) => {
142
131
  const fieldErrors = useFieldErrorsForForm(context);
143
- const [state, set] = useFormAtom(
144
- fieldErrorAtom({ formId: context.formId, field: name })
132
+ const state = useFormStore(
133
+ context.formId,
134
+ (state) => state.fieldErrors[name]
145
135
  );
146
- return [
147
- fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state),
148
- set,
149
- ] as const;
136
+ return fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state);
137
+ };
138
+
139
+ export const useClearError = (context: InternalFormContextValue) => {
140
+ const { formId } = context;
141
+ return useFormStore(formId, (state) => state.clearFieldError);
150
142
  };
151
143
 
152
144
  export const useFieldDefaultValue = (
@@ -154,26 +146,58 @@ export const useFieldDefaultValue = (
154
146
  context: InternalFormContextValue
155
147
  ) => {
156
148
  const defaultValues = useDefaultValuesForForm(context);
157
- const { defaultValues: state } = useFormAtomValue(
158
- formPropsAtom(context.formId)
159
- );
149
+ const state = useSyncedDefaultValues(context.formId);
160
150
  return defaultValues
161
151
  .map((val) => lodashGet(val, name))
162
152
  .hydrateTo(lodashGet(state, name));
163
153
  };
164
154
 
165
- export const useClearError = ({ formId }: InternalFormContextValue) => {
166
- const updateError = useFormUpdateAtom(setFieldErrorAtom(formId));
167
- return useCallback(
168
- (name: string) => updateError({ field: name, error: undefined }),
169
- [updateError]
155
+ export const useInternalIsSubmitting = (formId: InternalFormId) =>
156
+ useFormStore(formId, (state) => state.isSubmitting);
157
+
158
+ export const useInternalIsValid = (formId: InternalFormId) =>
159
+ useFormStore(formId, (state) => state.isValid());
160
+
161
+ export const useInternalHasBeenSubmitted = (formId: InternalFormId) =>
162
+ useFormStore(formId, (state) => state.hasBeenSubmitted);
163
+
164
+ export const useValidateField = (formId: InternalFormId) =>
165
+ useFormStore(formId, (state) => state.validateField);
166
+
167
+ export const useValidate = (formId: InternalFormId) =>
168
+ useFormStore(formId, (state) => state.validate);
169
+
170
+ const noOpReceiver = () => () => {};
171
+ export const useRegisterReceiveFocus = (formId: InternalFormId) =>
172
+ useFormStore(
173
+ formId,
174
+ (state) => state.formProps?.registerReceiveFocus ?? noOpReceiver
170
175
  );
171
- };
172
176
 
173
- export const useSetTouched = ({ formId }: InternalFormContextValue) => {
174
- const setTouched = useFormUpdateAtom(setTouchedAtom(formId));
175
- return useCallback(
176
- (name: string, touched: boolean) => setTouched({ field: name, touched }),
177
- [setTouched]
177
+ const defaultDefaultValues = {};
178
+ export const useSyncedDefaultValues = (formId: InternalFormId) =>
179
+ useFormStore(
180
+ formId,
181
+ (state) => state.formProps?.defaultValues ?? defaultDefaultValues
178
182
  );
179
- };
183
+
184
+ export const useSetTouched = ({ formId }: InternalFormContextValue) =>
185
+ useFormStore(formId, (state) => state.setTouched);
186
+
187
+ export const useTouchedFields = (formId: InternalFormId) =>
188
+ useFormStore(formId, (state) => state.touchedFields);
189
+
190
+ export const useFieldErrors = (formId: InternalFormId) =>
191
+ useFormStore(formId, (state) => state.fieldErrors);
192
+
193
+ export const useSetFieldErrors = (formId: InternalFormId) =>
194
+ useFormStore(formId, (state) => state.setFieldErrors);
195
+
196
+ export const useResetFormElement = (formId: InternalFormId) =>
197
+ useFormStore(formId, (state) => state.resetFormElement);
198
+
199
+ export const useFormActionProp = (formId: InternalFormId) =>
200
+ useFormStore(formId, (state) => state.formProps?.action);
201
+
202
+ export const useFormSubactionProp = (formId: InternalFormId) =>
203
+ useFormStore(formId, (state) => state.formProps?.subaction);
@@ -8,7 +8,7 @@ export const getRadioChecked = (
8
8
 
9
9
  if (import.meta.vitest) {
10
10
  const { it, expect } = import.meta.vitest;
11
- it("add", () => {
11
+ it("getRadioChecked", () => {
12
12
  expect(getRadioChecked("on", "on")).toBe(true);
13
13
  expect(getRadioChecked("on", undefined)).toBe(undefined);
14
14
  expect(getRadioChecked("trueValue", undefined)).toBe(undefined);
@@ -0,0 +1,8 @@
1
+ import { controlledFieldStore } from "./controlledFieldStore";
2
+ import { formStore } from "./createFormStore";
3
+ import { InternalFormId } from "./storeFamily";
4
+
5
+ export const cleanupFormState = (formId: InternalFormId) => {
6
+ formStore.remove(formId);
7
+ controlledFieldStore.remove(formId);
8
+ };
@@ -0,0 +1,91 @@
1
+ import invariant from "tiny-invariant";
2
+ import create from "zustand";
3
+ import { immer } from "zustand/middleware/immer";
4
+ import { storeFamily } from "./storeFamily";
5
+
6
+ export type ControlledFieldState = {
7
+ fields: {
8
+ [fieldName: string]:
9
+ | {
10
+ refCount: number;
11
+ value: unknown;
12
+ defaultValue?: unknown;
13
+ hydrated: boolean;
14
+ valueUpdatePromise: Promise<void> | undefined;
15
+ resolveValueUpdate: (() => void) | undefined;
16
+ }
17
+ | undefined;
18
+ };
19
+ register: (fieldName: string) => void;
20
+ unregister: (fieldName: string) => void;
21
+ setValue: (fieldName: string, value: unknown) => void;
22
+ hydrateWithDefault: (fieldName: string, defaultValue: unknown) => void;
23
+ awaitValueUpdate: (fieldName: string) => Promise<void>;
24
+ reset: () => void;
25
+ };
26
+
27
+ export const controlledFieldStore = storeFamily(() =>
28
+ create<ControlledFieldState>()(
29
+ immer((set, get, api) => ({
30
+ fields: {},
31
+
32
+ register: (field) =>
33
+ set((state) => {
34
+ if (state.fields[field]) {
35
+ state.fields[field]!.refCount++;
36
+ } else {
37
+ state.fields[field] = {
38
+ refCount: 1,
39
+ value: undefined,
40
+ hydrated: false,
41
+ valueUpdatePromise: undefined,
42
+ resolveValueUpdate: undefined,
43
+ };
44
+ }
45
+ }),
46
+
47
+ unregister: (field) =>
48
+ set((state) => {
49
+ const fieldState = state.fields[field];
50
+ if (!fieldState) return;
51
+
52
+ fieldState.refCount--;
53
+ if (fieldState.refCount === 0) delete state.fields[field];
54
+ }),
55
+
56
+ setValue: (field, value) =>
57
+ set((state) => {
58
+ const fieldState = state.fields[field];
59
+ if (!fieldState) return;
60
+
61
+ fieldState.value = value;
62
+ const promise = new Promise<void>((resolve) => {
63
+ fieldState.resolveValueUpdate = resolve;
64
+ });
65
+ fieldState.valueUpdatePromise = promise;
66
+ }),
67
+
68
+ hydrateWithDefault: (field, defaultValue) =>
69
+ set((state) => {
70
+ const fieldState = state.fields[field];
71
+ if (!fieldState) return;
72
+
73
+ fieldState.value = defaultValue;
74
+ fieldState.defaultValue = defaultValue;
75
+ fieldState.hydrated = true;
76
+ }),
77
+
78
+ awaitValueUpdate: async (field) => {
79
+ await get().fields[field]?.valueUpdatePromise;
80
+ },
81
+
82
+ reset: () =>
83
+ set((state) => {
84
+ Object.values(state.fields).forEach((field) => {
85
+ if (!field) return;
86
+ field.value = field.defaultValue;
87
+ });
88
+ }),
89
+ }))
90
+ )
91
+ );
@@ -1,115 +1,36 @@
1
- import { atom, PrimitiveAtom } from "jotai";
2
- import { useAtomCallback } from "jotai/utils";
3
- import omit from "lodash/omit";
4
1
  import { useCallback, useEffect } from "react";
5
2
  import { InternalFormContextValue } from "../formContext";
6
- import {
7
- useFieldDefaultValue,
8
- useFormAtomValue,
9
- useFormAtom,
10
- useFormUpdateAtom,
11
- } from "../hooks";
12
- import { isHydratedAtom } from "../state";
13
- import {
14
- fieldAtomFamily,
15
- FieldAtomKey,
16
- formAtomFamily,
17
- InternalFormId,
18
- } from "./atomUtils";
19
-
20
- export const controlledFieldsAtom = formAtomFamily<
21
- Record<string, PrimitiveAtom<unknown>>
22
- >({});
23
- const refCountAtom = fieldAtomFamily(() => atom(0));
24
- const fieldValueAtom = fieldAtomFamily(() => atom<unknown>(undefined));
25
- const fieldValueHydratedAtom = fieldAtomFamily(() => atom(false));
26
-
27
- export const valueUpdatePromiseAtom = fieldAtomFamily(() =>
28
- atom<Promise<void> | undefined>(undefined)
29
- );
30
- export const resolveValueUpdateAtom = fieldAtomFamily(() =>
31
- atom<(() => void) | undefined>(undefined)
32
- );
33
-
34
- const registerAtom = atom(null, (get, set, { formId, field }: FieldAtomKey) => {
35
- set(refCountAtom({ formId, field }), (prev) => prev + 1);
36
- const newRefCount = get(refCountAtom({ formId, field }));
37
- // We don't set hydrated here because it gets set when we know
38
- // we have the right default values
39
- if (newRefCount === 1) {
40
- set(controlledFieldsAtom(formId), (prev) => ({
41
- ...prev,
42
- [field]: fieldValueAtom({ formId, field }),
43
- }));
44
- }
45
- });
46
-
47
- const unregisterAtom = atom(
48
- null,
49
- (get, set, { formId, field }: FieldAtomKey) => {
50
- set(refCountAtom({ formId, field }), (prev) => prev - 1);
51
- const newRefCount = get(refCountAtom({ formId, field }));
52
- if (newRefCount === 0) {
53
- set(controlledFieldsAtom(formId), (prev) => omit(prev, field));
54
- fieldValueAtom.remove({ formId, field });
55
- resolveValueUpdateAtom.remove({ formId, field });
56
- fieldValueHydratedAtom.remove({ formId, field });
57
- }
58
- }
59
- );
60
-
61
- export const setControlledFieldValueAtom = atom(
62
- null,
63
- (
64
- _get,
65
- set,
66
- {
67
- formId,
68
- field,
69
- value,
70
- }: { formId: InternalFormId; field: string; value: unknown }
71
- ) => {
72
- set(fieldValueAtom({ formId, field }), value);
73
- const resolveAtom = resolveValueUpdateAtom({ formId, field });
74
- const promiseAtom = valueUpdatePromiseAtom({ formId, field });
75
-
76
- const promise = new Promise<void>((resolve) =>
77
- set(resolveAtom, () => {
78
- resolve();
79
- set(resolveAtom, undefined);
80
- set(promiseAtom, undefined);
81
- })
82
- );
83
- set(promiseAtom, promise);
84
- }
85
- );
3
+ import { useFieldDefaultValue } from "../hooks";
4
+ import { controlledFieldStore } from "./controlledFieldStore";
5
+ import { formStore } from "./createFormStore";
6
+ import { InternalFormId } from "./storeFamily";
86
7
 
87
8
  export const useControlledFieldValue = (
88
9
  context: InternalFormContextValue,
89
10
  field: string
90
11
  ) => {
91
- const fieldAtom = fieldValueAtom({ formId: context.formId, field });
92
- const [value, setValue] = useFormAtom(fieldAtom);
12
+ const useValueStore = controlledFieldStore(context.formId);
13
+ const value = useValueStore((state) => state.fields[field]?.value);
93
14
 
15
+ const useFormStore = formStore(context.formId);
16
+ const isFormHydrated = useFormStore((state) => state.isHydrated);
94
17
  const defaultValue = useFieldDefaultValue(field, context);
95
- const isHydrated = useFormAtomValue(isHydratedAtom(context.formId));
96
- const [isFieldHydrated, setIsFieldHydrated] = useFormAtom(
97
- fieldValueHydratedAtom({ formId: context.formId, field })
18
+
19
+ const isFieldHydrated = useValueStore(
20
+ (state) => state.fields[field]?.hydrated ?? false
98
21
  );
22
+ const hydrateWithDefault = useValueStore((state) => state.hydrateWithDefault);
99
23
 
100
24
  useEffect(() => {
101
- if (isHydrated && !isFieldHydrated) {
102
- setValue(defaultValue);
103
- setIsFieldHydrated(true);
25
+ if (isFormHydrated && !isFieldHydrated) {
26
+ hydrateWithDefault(field, defaultValue);
104
27
  }
105
28
  }, [
106
29
  defaultValue,
107
30
  field,
108
- context.formId,
31
+ hydrateWithDefault,
109
32
  isFieldHydrated,
110
- isHydrated,
111
- setIsFieldHydrated,
112
- setValue,
33
+ isFormHydrated,
113
34
  ]);
114
35
 
115
36
  return isFieldHydrated ? value : defaultValue;
@@ -119,27 +40,26 @@ export const useControllableValue = (
119
40
  context: InternalFormContextValue,
120
41
  field: string
121
42
  ) => {
122
- const resolveUpdate = useFormAtomValue(
123
- resolveValueUpdateAtom({ formId: context.formId, field })
43
+ const useValueStore = controlledFieldStore(context.formId);
44
+
45
+ const resolveUpdate = useValueStore(
46
+ (state) => state.fields[field]?.resolveValueUpdate
124
47
  );
125
48
  useEffect(() => {
126
49
  resolveUpdate?.();
127
50
  }, [resolveUpdate]);
128
51
 
129
- const register = useFormUpdateAtom(registerAtom);
130
- const unregister = useFormUpdateAtom(unregisterAtom);
52
+ const register = useValueStore((state) => state.register);
53
+ const unregister = useValueStore((state) => state.unregister);
131
54
  useEffect(() => {
132
- register({ formId: context.formId, field });
133
- return () => unregister({ formId: context.formId, field });
55
+ register(field);
56
+ return () => unregister(field);
134
57
  }, [context.formId, field, register, unregister]);
135
58
 
136
- const setControlledFieldValue = useFormUpdateAtom(
137
- setControlledFieldValueAtom
138
- );
59
+ const setControlledFieldValue = useValueStore((state) => state.setValue);
139
60
  const setValue = useCallback(
140
- (value: unknown) =>
141
- setControlledFieldValue({ formId: context.formId, field, value }),
142
- [field, context.formId, setControlledFieldValue]
61
+ (value: unknown) => setControlledFieldValue(field, value),
62
+ [field, setControlledFieldValue]
143
63
  );
144
64
 
145
65
  const value = useControlledFieldValue(context, field);
@@ -148,23 +68,11 @@ export const useControllableValue = (
148
68
  };
149
69
 
150
70
  export const useUpdateControllableValue = (formId: InternalFormId) => {
151
- const setControlledFieldValue = useFormUpdateAtom(
152
- setControlledFieldValueAtom
153
- );
154
- return useCallback(
155
- (field: string, value: unknown) =>
156
- setControlledFieldValue({ formId, field, value }),
157
- [formId, setControlledFieldValue]
158
- );
71
+ const useValueStore = controlledFieldStore(formId);
72
+ return useValueStore((state) => state.setValue);
159
73
  };
160
74
 
161
75
  export const useAwaitValue = (formId: InternalFormId) => {
162
- return useAtomCallback(
163
- useCallback(
164
- async (get, _set, field: string) => {
165
- await get(valueUpdatePromiseAtom({ formId, field }));
166
- },
167
- [formId]
168
- )
169
- );
76
+ const useValueStore = controlledFieldStore(formId);
77
+ return useValueStore((state) => state.awaitValueUpdate);
170
78
  };