Skip to content

Commit

Permalink
fix(list-atom): make pristine (!dirty) after new initialValue is set (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
MiroslavPetrik authored Feb 14, 2024
1 parent 46d4753 commit 170312d
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 42 deletions.
7 changes: 2 additions & 5 deletions src/atoms/_useFieldInitialValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ export function _useFieldInitialValue<Value>(
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]);
}
65 changes: 30 additions & 35 deletions src/atoms/list-atom/listAtom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand All @@ -288,7 +287,6 @@ describe("listAtom()", () => {
expect(state.current.dirty).toBe(false);

await act(async () => listActions.current.add());

expect(state.current.dirty).toBe(true);
});

Expand All @@ -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],
Expand All @@ -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);
});
});
});
1 change: 1 addition & 0 deletions src/hooks/use-list-field-initial-value/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useListFieldInitialValue";
Original file line number Diff line number Diff line change
@@ -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);
});
});
1 change: 0 additions & 1 deletion src/hooks/use-list-field/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./useListField";
export * from "./useListFieldInitialValue";
2 changes: 1 addition & 1 deletion src/hooks/use-list-field/useListField.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down

0 comments on commit 170312d

Please sign in to comment.