From 4b35fd3d7fe1a4540a91e5098545e8265c8e3995 Mon Sep 17 00:00:00 2001 From: MiroslavPetrik Date: Wed, 14 Feb 2024 15:12:26 +0100 Subject: [PATCH] fix(list-atom) (#108) * fix(list-atom): make pristine (!dirty) after new initialValue is set (#107) Fixes #105 This requires the user to control the referential equality of the initialValue for the same values in order to prevent unnecessary updates. * chore(release): 4.0.13-next.1 ## [4.0.13-next.1](https://github.com/form-atoms/field/compare/v4.0.12...v4.0.13-next.1) (2024-02-14) ### Bug Fixes * **list-atom:** make pristine (!dirty) after new initialValue is set ([#107](https://github.com/form-atoms/field/issues/107)) ([170312d](https://github.com/form-atoms/field/commit/170312d952cb4a7e72eebb8ba2a2606feb0cbdbf)), closes [#105](https://github.com/form-atoms/field/issues/105) --------- Co-authored-by: semantic-release-bot --- CHANGELOG.md | 7 ++ src/atoms/_useFieldInitialValue.ts | 7 +- src/atoms/list-atom/listAtom.test.ts | 65 +++++++++---------- .../use-list-field-initial-value/index.ts | 1 + .../useListFieldInitialValue.test.ts | 42 ++++++++++++ .../useListFieldInitialValue.ts | 0 src/hooks/use-list-field/index.ts | 1 - src/hooks/use-list-field/useListField.ts | 2 +- 8 files changed, 83 insertions(+), 42 deletions(-) create mode 100644 src/hooks/use-list-field-initial-value/index.ts create mode 100644 src/hooks/use-list-field-initial-value/useListFieldInitialValue.test.ts rename src/hooks/{use-list-field => use-list-field-initial-value}/useListFieldInitialValue.ts (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ad9f7..091a820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [4.0.13-next.1](https://github.com/form-atoms/field/compare/v4.0.12...v4.0.13-next.1) (2024-02-14) + + +### Bug Fixes + +* **list-atom:** make pristine (!dirty) after new initialValue is set ([#107](https://github.com/form-atoms/field/issues/107)) ([170312d](https://github.com/form-atoms/field/commit/170312d952cb4a7e72eebb8ba2a2606feb0cbdbf)), closes [#105](https://github.com/form-atoms/field/issues/105) + ## [4.0.12](https://github.com/form-atoms/field/compare/v4.0.11...v4.0.12) (2024-02-14) diff --git a/src/atoms/_useFieldInitialValue.ts b/src/atoms/_useFieldInitialValue.ts index b120f61..8230b2f 100644 --- a/src/atoms/_useFieldInitialValue.ts +++ b/src/atoms/_useFieldInitialValue.ts @@ -24,9 +24,6 @@ export function _useFieldInitialValue( if (initialValue === undefined) { return; } - if (!store.get(field.dirty)) { - store.set(field.value, initialValue); - } - store.set(field._initialValue, initialValue); - }, [store, field._initialValue, field.value, field.dirty, initialValue]); + store.set(field.value, initialValue); + }, [store, field.value, initialValue]); } diff --git a/src/atoms/list-atom/listAtom.test.ts b/src/atoms/list-atom/listAtom.test.ts index c6ca351..0ca0cf7 100644 --- a/src/atoms/list-atom/listAtom.test.ts +++ b/src/atoms/list-atom/listAtom.test.ts @@ -272,11 +272,10 @@ describe("listAtom()", () => { expect(state.current.dirty).toBe(false); await act(async () => listActions.current.remove(list.current[0]!)); - expect(state.current.dirty).toBe(true); }); - it("becomes dirty when an item is added ", async () => { + it("becomes dirty when an item is added", async () => { const field = listAtom({ value: [], builder: (value) => numberField({ value }), @@ -288,7 +287,6 @@ describe("listAtom()", () => { expect(state.current.dirty).toBe(false); await act(async () => listActions.current.add()); - expect(state.current.dirty).toBe(true); }); @@ -306,10 +304,32 @@ describe("listAtom()", () => { expect(state.current.dirty).toBe(false); await act(async () => listActions.current.move(list.current[0]!)); - expect(state.current.dirty).toBe(true); }); + it("becomes dirty when some item field is edited", async () => { + const field = listAtom({ + value: [undefined], + builder: (value) => numberField({ value }), + }); + + const { result: fieldState } = renderHook(() => useFieldState(field)); + const { result: formFields } = renderHook(() => + useAtomValue(useAtomValue(field)._formFields), + ); + const { result: inputActions } = renderHook(() => + useFieldActions(formFields.current[0]!), + ); + + expect(fieldState.current.dirty).toBe(false); + + await act(async () => inputActions.current.setValue(42)); + expect(fieldState.current.dirty).toBe(true); + + await act(async () => inputActions.current.reset()); + expect(fieldState.current.dirty).toBe(false); + }); + it("becomes pristine when items are reordered & back", async () => { const field = listAtom({ value: [42, 84], @@ -325,54 +345,29 @@ describe("listAtom()", () => { // moves first item down await act(async () => listActions.current.move(list.current[0]!)); - expect(state.current.dirty).toBe(true); // moves first item down await act(async () => listActions.current.move(list.current[0]!)); - expect(state.current.dirty).toBe(false); }); it("becomes pristine after value is set (the set is usually called by useFieldInitialValue to hydrate the field)", async () => { const field = listAtom({ - value: [] as number[], + value: [1, 2], builder: (value) => numberField({ value }), }); const { result: state } = renderHook(() => useFieldState(field)); const { result: fieldActions } = renderHook(() => useFieldActions(field)); - expect(state.current.dirty).toBe(false); - - await act(async () => fieldActions.current.setValue([42])); + // make list dirty + const { result: listActions } = renderHook(() => useListActions(field)); + await act(async () => listActions.current.add()); + expect(state.current.dirty).toBe(true); + await act(async () => fieldActions.current.setValue([42, 84])); expect(state.current.dirty).toBe(false); }); - - it("becomes dirty when some item field is edited", async () => { - const field = listAtom({ - value: [undefined], - builder: (value) => numberField({ value }), - }); - - const { result: fieldState } = renderHook(() => useFieldState(field)); - const { result: formFields } = renderHook(() => - useAtomValue(useAtomValue(field)._formFields), - ); - const { result: inputActions } = renderHook(() => - useFieldActions(formFields.current[0]!), - ); - - expect(fieldState.current.dirty).toBe(false); - - await act(async () => inputActions.current.setValue(42)); - - expect(fieldState.current.dirty).toBe(true); - - await act(async () => inputActions.current.reset()); - - expect(fieldState.current.dirty).toBe(false); - }); }); }); diff --git a/src/hooks/use-list-field-initial-value/index.ts b/src/hooks/use-list-field-initial-value/index.ts new file mode 100644 index 0000000..b9e95b3 --- /dev/null +++ b/src/hooks/use-list-field-initial-value/index.ts @@ -0,0 +1 @@ +export * from "./useListFieldInitialValue"; diff --git a/src/hooks/use-list-field-initial-value/useListFieldInitialValue.test.ts b/src/hooks/use-list-field-initial-value/useListFieldInitialValue.test.ts new file mode 100644 index 0000000..e7e2e61 --- /dev/null +++ b/src/hooks/use-list-field-initial-value/useListFieldInitialValue.test.ts @@ -0,0 +1,42 @@ +import { act, renderHook } from "@testing-library/react"; +import { useFieldState } from "form-atoms"; +import { describe, expect, it } from "vitest"; + +import { useListFieldInitialValue } from "./useListFieldInitialValue"; +import { listAtom } from "../../atoms"; +import { numberField } from "../../fields"; +import { useListActions } from "../use-list-actions"; + +describe("useListFieldInitialValue()", () => { + it("reinitializes the field value", async () => { + const field = listAtom({ + value: [] as number[], + builder: (value) => numberField({ value }), + }); + + const { result: state } = renderHook(() => useFieldState(field)); + const { rerender } = renderHook( + (props) => useListFieldInitialValue(field, props.initialValue), + { initialProps: { initialValue: [1, 2] } }, + ); + + // make list dirty + const { result: listActions } = renderHook(() => useListActions(field)); + await act(async () => listActions.current.add()); + expect(state.current.dirty).toBe(true); + + const initialValue = [42, 84]; + + // initialization makes field pristine + rerender({ initialValue }); + expect(state.current.dirty).toBe(false); + + // make list dirty again + await act(async () => listActions.current.add()); + expect(state.current.dirty).toBe(true); + + // re-inititialation skipped with the same initialValue (useEffect dependency) + rerender({ initialValue }); + expect(state.current.dirty).toBe(true); + }); +}); diff --git a/src/hooks/use-list-field/useListFieldInitialValue.ts b/src/hooks/use-list-field-initial-value/useListFieldInitialValue.ts similarity index 100% rename from src/hooks/use-list-field/useListFieldInitialValue.ts rename to src/hooks/use-list-field-initial-value/useListFieldInitialValue.ts diff --git a/src/hooks/use-list-field/index.ts b/src/hooks/use-list-field/index.ts index 09c4d10..a8bb3cb 100644 --- a/src/hooks/use-list-field/index.ts +++ b/src/hooks/use-list-field/index.ts @@ -1,2 +1 @@ export * from "./useListField"; -export * from "./useListFieldInitialValue"; diff --git a/src/hooks/use-list-field/useListField.ts b/src/hooks/use-list-field/useListField.ts index b4334b5..ddea11a 100644 --- a/src/hooks/use-list-field/useListField.ts +++ b/src/hooks/use-list-field/useListField.ts @@ -1,10 +1,10 @@ import { UseFieldOptions } from "form-atoms"; import { useAtomValue } from "jotai"; -import { useListFieldInitialValue } from "./useListFieldInitialValue"; import { ListAtomItems, ListAtomValue } from "../../atoms/list-atom"; import { ListField } from "../../fields"; import { useListActions } from "../use-list-actions"; +import { useListFieldInitialValue } from "../use-list-field-initial-value"; export const useListField = < Fields extends ListAtomItems,