diff --git a/.changeset/mighty-wasps-speak.md b/.changeset/mighty-wasps-speak.md
new file mode 100644
index 00000000..73b05f7d
--- /dev/null
+++ b/.changeset/mighty-wasps-speak.md
@@ -0,0 +1,5 @@
+---
+"react-cool-form": patch
+---
+
+fix(useFieldArray): nested field array not working
diff --git a/README.md b/README.md
index 66c2d62d..46dc403d 100644
--- a/README.md
+++ b/README.md
@@ -111,7 +111,7 @@ const App = () => {
✨ Pretty easy right? React Cool Form is more powerful than you think. Let's [explore it](https://react-cool-form.netlify.app) now!
-## Articles / Blog Posts
+## Articles / Blog Posts
> 💡 If you have written any blog post or article about React Cool Form, please open a PR to add it here.
diff --git a/app/src/Playground/index.tsx b/app/src/Playground/index.tsx
index ce93eb03..7bc26a20 100644
--- a/app/src/Playground/index.tsx
+++ b/app/src/Playground/index.tsx
@@ -1,23 +1,92 @@
/* eslint-disable no-console */
-import { useForm } from "react-cool-form";
+import { useForm, useFieldArray } from "react-cool-form";
export default () => {
- const { form, runValidation } = useForm({
- // validate: () => ({ foo: "Required" }),
- focusOnError: ["foo"],
+ const { form } = useForm({
+ defaultValues: {
+ foo: [
+ {
+ name: "Iron Man",
+ arr: [{ name: "iron arr.0" }, { name: "iron arr.1" }],
+ },
+ ],
+ },
+ onSubmit: (values) => console.log("LOG ===> Form data: ", values),
});
+ const [fields, { push, insert, remove }] = useFieldArray("foo");
return (
- <>
-
-
- >
+
);
};
+
+function Arr({ field }: any) {
+ const [fields, { push }] = useFieldArray(`${field}.arr`);
+
+ return (
+
+ {fields.map((key, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 14390f9f..216dd068 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -92,7 +92,7 @@ export type Fields = Map<
export type Parsers = ObjMap>;
-export type FieldArray = ObjMap<{ fields: ObjMap; reset: () => void }>;
+export type FieldArray = Map void }>;
interface EventOptions {
removeField: RemoveField;
diff --git a/src/useFieldArray.ts b/src/useFieldArray.ts
index 6b2e3000..27be03a7 100644
--- a/src/useFieldArray.ts
+++ b/src/useFieldArray.ts
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import {
FieldArrayConfig,
@@ -18,6 +18,7 @@ import {
compact,
get,
getIsDirty,
+ getUid,
invariant,
isUndefined,
set,
@@ -48,15 +49,19 @@ export default (
runValidation,
removeField,
} = methods;
+ const keysRef = useRef([]);
const getFields = useCallback(
- (init = false): string[] => {
+ (init = false) => {
let fields = getState(name);
if (init && isUndefined(fields)) fields = defaultValue;
return Array.isArray(fields)
- ? fields.map((_, index) => `${name}[${index}]`)
+ ? fields.map((_, index) => {
+ keysRef.current[index] = keysRef.current[index] || getUid();
+ return keysRef.current[index];
+ })
: [];
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -65,13 +70,14 @@ export default (
const [fields, setFields] = useState(getFields(true));
- const updateFields = useCallback(() => {
- setFields(getFields());
- setNodesOrValues(getState("values"), {
- shouldSetValues: false,
- fields: Object.keys(fieldArrayRef.current[name].fields),
- });
- }, [fieldArrayRef, getFields, getState, name, setNodesOrValues]);
+ useEffect(
+ () =>
+ setNodesOrValues(getState("values"), {
+ shouldSetValues: false,
+ fields: Object.keys(fieldArrayRef.current.get(name)!.fields),
+ }),
+ [fieldArrayRef, fields, getState, name, setNodesOrValues]
+ );
useEffect(() => {
if (
@@ -79,20 +85,20 @@ export default (
!isUndefined(defaultValue)
) {
setDefaultValue(name, defaultValue, true);
- updateFields();
+ setFields(getFields());
}
return () => {
- if (shouldRemoveField(name)) removeField(name);
+ if (shouldRemoveField(name)) removeField(name, ["defaultValue"]);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- if (!fieldArrayRef.current[name])
- fieldArrayRef.current[name] = {
- reset: updateFields,
+ if (!fieldArrayRef.current.has(name))
+ fieldArrayRef.current.set(name, {
+ update: () => setFields(getFields()),
fields: {},
- };
+ });
if (validate) fieldValidatorsRef.current[name] = validate;
const setState = useCallback(
@@ -106,8 +112,8 @@ export default (
let state = getState();
(["values", "touched", "errors", "dirty"] as Keys[]).forEach((key) => {
- const value = state[key][name];
- const fieldsLength = state.values[name]?.length;
+ const value = get(state[key], name);
+ const fieldsLength = get(state.values, name)?.length;
if (
key === "values" ||
@@ -117,25 +123,22 @@ export default (
)
state = set(
state,
- key,
- {
- ...state[key],
- [name]: handler(
- Array.isArray(value) ? [...value] : [],
- key,
- fieldsLength ? fieldsLength - 1 : 0
- ),
- },
+ `${key}.${name}`,
+ handler(
+ Array.isArray(value) ? [...value] : [],
+ key,
+ fieldsLength ? fieldsLength - 1 : 0
+ ),
true
);
});
setStateRef("", { ...state, shouldDirty: getIsDirty(state.dirty) });
- updateFields();
+ setFields(getFields());
if (validateOnChange) runValidation(name);
},
- [getState, name, runValidation, setStateRef, updateFields, validateOnChange]
+ [getFields, getState, name, runValidation, setStateRef, validateOnChange]
);
const push = useCallback>(
@@ -143,6 +146,7 @@ export default (
const handler: StateHandler = (f, type, lastIndex = 0) => {
if (type === "values") {
f.push(value);
+ keysRef.current.push(getUid());
} else if (
(type === "touched" && shouldTouched) ||
(type === "dirty" && shouldDirty)
@@ -163,6 +167,7 @@ export default (
const handler: StateHandler = (f, type) => {
if (type === "values") {
f.splice(index, 0, value);
+ keysRef.current.splice(index, 0, getUid());
} else if (
(type === "touched" && shouldTouched) ||
(type === "dirty" && shouldDirty)
@@ -184,6 +189,7 @@ export default (
(index) => {
const handler: StateHandler = (f) => {
f.splice(index, 1);
+ keysRef.current.splice(index, 1);
return compact(f).length ? f : [];
};
const value = (getState(name) || [])[index];
@@ -199,6 +205,10 @@ export default (
(indexA, indexB) => {
const handler: StateHandler = (f) => {
[f[indexA], f[indexB]] = [f[indexB], f[indexA]];
+ [keysRef.current[indexA], keysRef.current[indexB]] = [
+ keysRef.current[indexB],
+ keysRef.current[indexA],
+ ];
return f;
};
@@ -211,6 +221,7 @@ export default (
(from, to) => {
const handler: StateHandler = (f) => {
f.splice(to, 0, f.splice(from, 1)[0]);
+ keysRef.current.splice(to, 0, f.splice(from, 1)[0]);
return f;
};
diff --git a/src/useForm.ts b/src/useForm.ts
index b003ee65..77b32847 100644
--- a/src/useForm.ts
+++ b/src/useForm.ts
@@ -90,7 +90,7 @@ export default ({
const formRef = useRef();
const fieldsRef = useRef(new Map());
const fieldParsersRef = useRef({});
- const fieldArrayRef = useRef({});
+ const fieldArrayRef = useRef(new Map());
const controlsRef = useRef({});
const formValidatorRef = useLatest(validate);
const fieldValidatorsRef = useRef>>({});
@@ -183,7 +183,7 @@ export default ({
const fieldArrayName = isFieldArray(fieldArrayRef.current, name);
if (fieldArrayName)
- fieldArrayRef.current[fieldArrayName].fields[name] = true;
+ fieldArrayRef.current.get(fieldArrayName)!.fields[name] = true;
acc.set(name, {
...acc.get(name),
@@ -389,7 +389,7 @@ export default ({
}, [builtInValidationMode, runBuiltInValidation]);
const runFieldValidation = useCallback(
- async (name: string): Promise => {
+ async (name: string) => {
const value = get(stateRef.current.values, name);
if (!fieldValidatorsRef.current[name] || isUndefined(value))
@@ -704,7 +704,7 @@ export default ({
setNodeValue(name, value);
isFieldArray(fieldArrayRef.current, name, (key) =>
- fieldArrayRef.current[key].reset()
+ fieldArrayRef.current.get(key)!.update()
);
if (shouldTouched) setTouched(name, true, { shouldValidate: false });
@@ -774,7 +774,7 @@ export default ({
setStateRef("", state);
onResetRef.current(state.values, getOptions(), e);
- Object.values(fieldArrayRef.current).forEach((field) => field.reset());
+ fieldArrayRef.current.forEach((field) => field.update());
},
[getOptions, onResetRef, setNodesOrValues, setStateRef, stateRef]
);
@@ -842,8 +842,8 @@ export default ({
? removeOnUnmounted
: [
...Array.from(fieldsRef.current.keys()),
+ ...Array.from(fieldArrayRef.current.keys()),
...Object.keys(controlsRef.current),
- ...Object.keys(fieldArrayRef.current),
];
names = isFunction(removeOnUnmounted) ? removeOnUnmounted(names) : names;
@@ -882,10 +882,9 @@ export default ({
delete fieldParsersRef.current[name];
delete fieldValidatorsRef.current[name];
- delete fieldArrayRef.current[name];
delete controlsRef.current[name];
-
- if (fieldsRef.current.has(name)) fieldsRef.current.delete(name);
+ fieldArrayRef.current.delete(name);
+ fieldsRef.current.delete(name);
},
[handleUnset, stateRef]
);
diff --git a/src/utils/getUid.test.ts b/src/utils/getUid.test.ts
new file mode 100644
index 00000000..2c77fa77
--- /dev/null
+++ b/src/utils/getUid.test.ts
@@ -0,0 +1,9 @@
+import getUid from "./getUid";
+
+describe("getUid", () => {
+ it("should work correctly", () => {
+ // @ts-expect-error
+ window.crypto = { getRandomValues: () => new Array(16).fill(0) };
+ expect(getUid()).toBe("00000000-0000-4000-8000-000000000000");
+ });
+});
diff --git a/src/utils/getUid.ts b/src/utils/getUid.ts
new file mode 100644
index 00000000..05a19290
--- /dev/null
+++ b/src/utils/getUid.ts
@@ -0,0 +1,18 @@
+/* eslint-disable no-bitwise */
+
+const hex: string[] = [];
+
+for (let i = 0; i < 256; i += 1) hex[i] = (i < 16 ? "0" : "") + i.toString(16);
+
+export default (): string => {
+ const r = crypto.getRandomValues(new Uint8Array(16));
+
+ r[6] = (r[6] & 0x0f) | 0x40;
+ r[8] = (r[8] & 0x3f) | 0x80;
+
+ return `${hex[r[0]] + hex[r[1]] + hex[r[2]] + hex[r[3]]}-${hex[r[4]]}${
+ hex[r[5]]
+ }-${hex[r[6]]}${hex[r[7]]}-${hex[r[8]]}${hex[r[9]]}-${hex[r[10]]}${
+ hex[r[11]]
+ }${hex[r[12]]}${hex[r[13]]}${hex[r[14]]}${hex[r[15]]}`;
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 7be11357..b6185eec 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -6,6 +6,7 @@ export { default as filterErrors } from "./filterErrors";
export { default as get } from "./get";
export { default as getIsDirty } from "./getIsDirty";
export { default as getPath } from "./getPath";
+export { default as getUid } from "./getUid";
export { default as invariant } from "./invariant";
export { default as isAsyncFunction } from "./isAsyncFunction";
export { default as isCheckboxInput } from "./isCheckboxInput";
diff --git a/src/utils/isFieldArray.test.ts b/src/utils/isFieldArray.test.ts
index d16a7001..159767a1 100644
--- a/src/utils/isFieldArray.test.ts
+++ b/src/utils/isFieldArray.test.ts
@@ -2,20 +2,26 @@ import isFieldArray from "./isFieldArray";
describe("isFieldArray", () => {
it("should work correctly", () => {
- // @ts-expect-error
- expect(isFieldArray({ foo: true }, "foo[0].a")).toBe("foo");
+ const value = { fields: {}, update: () => null };
+ const foo = new Map([
+ ["foo", value],
+ ["foo[0].a[0]", value],
+ ]);
- // @ts-expect-error
- expect(isFieldArray({ foo: true }, "bar[0].a")).toBeUndefined();
+ expect(isFieldArray(foo, "foo[0].a")).toBe("foo");
+
+ expect(isFieldArray(foo, "foo[0].a[0].b")).toBe("foo[0].a[0]");
+
+ expect(isFieldArray(foo, "bar[0].a")).toBeUndefined();
let callback = jest.fn();
- // @ts-expect-error
- isFieldArray({ foo: true }, "foo[0].a", callback);
+
+ isFieldArray(foo, "foo[0].a", callback);
expect(callback).toHaveBeenCalledWith("foo");
callback = jest.fn();
- // @ts-expect-error
- isFieldArray({ foo: true }, "bar[0].a", callback);
+
+ isFieldArray(foo, "bar[0].a", callback);
expect(callback).not.toHaveBeenCalled();
});
});
diff --git a/src/utils/isFieldArray.ts b/src/utils/isFieldArray.ts
index 324a14c2..23f9d6e2 100644
--- a/src/utils/isFieldArray.ts
+++ b/src/utils/isFieldArray.ts
@@ -7,14 +7,16 @@ export default (
): string | void => {
let fieldName;
- Object.keys(fields).some((key) => {
- if (name.startsWith(key)) {
- fieldName = key;
- if (callback) callback(key);
- return true;
- }
- return false;
- });
+ Array.from(fields)
+ .reverse()
+ .some(([key]) => {
+ if (name.startsWith(key)) {
+ fieldName = key;
+ if (callback) callback(key);
+ return true;
+ }
+ return false;
+ });
return fieldName;
};