Skip to content

Commit

Permalink
feat: Add init.onlyOnce. (#111)
Browse files Browse the repository at this point in the history
A previous refactoring removed the ability to do `init: pojo` in
the API, and instead consolidated usage on `init: { input: pojo }`.

This was good, but a surprising artifact of `init: pojo` was that it did
not re-init as the identity of pojo changes (vs. the `init.input = pojo`
whcih *does* re-init as the identity changes).

This dictomoy between "one re-inits and one does not" was fairly
aribtrary and was a big reason behind removing the "two ways of doing
things".

That said, in rolling out the change to internal-frontend, a number of
callers were enjoying the implicit useMemo-ification of their `init:
pojo`, and so this PR adds an `init.onlyOnce` flag that basically
restores the "don't watch the init.input identity" behavior.
  • Loading branch information
stephenh authored Oct 17, 2024
1 parent 0ade243 commit 67683fb
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 3 deletions.
29 changes: 27 additions & 2 deletions src/useFormState.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { click, clickAndWait, render, typeAndWait, wait } from "@homebound/rtl-utils";
import { act } from "@testing-library/react";
import { makeAutoObservable, reaction } from "mobx";
import { Observer } from "mobx-react";
import { useMemo, useState } from "react";
import { observer, Observer } from "mobx-react";
import { useMemo, useRef, useState } from "react";
import { TextField } from "src/FormStateApp";
import { ObjectConfig } from "src/config";
import { ObjectState } from "src/fields/objectField";
Expand Down Expand Up @@ -117,6 +117,31 @@ describe("useFormState", () => {
expect(r.baseElement.textContent).toEqual("");
});

it("uses init.onlyOnce to not react to identity changes", async () => {
// Given a component
type FormValue = Pick<AuthorInput, "firstName">;
const config: ObjectConfig<FormValue> = { firstName: { type: "value" } };
const TestComponent = observer(() => {
// And we have an `init` value that is dynamic, so we can observe whether init reruns
const renderCount = useRef(0).current++;
const form = useFormState({
config,
init: { input: { firstName: `${renderCount}` }, onlyOnce: true },
});
const [, setTick] = useState(0);
return (
<div>
<button data-testid="change" onClick={() => setTick(1)} />
<div data-testid="firstName">{form.firstName.value}</div>
</div>
);
});
const r = await render(<TestComponent />);
expect(r.firstName).toHaveTextContent("0");
click(r.change);
expect(r.firstName).toHaveTextContent("0");
});

it("keeps local changed values when a query refreshes", async () => {
// Given a component
function TestComponent() {
Expand Down
9 changes: 8 additions & 1 deletion src/useFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type InputAndMap<T, I> = {
input: I;
map?: (input: Exclude<I, null | undefined>) => T;
ifUndefined?: T;
onlyOnce?: boolean;
};

export type QueryAndMap<T, I> = {
Expand Down Expand Up @@ -98,7 +99,13 @@ export function useFormState<T, I>(opts: UseFormStateOpts<T, I>): ObjectState<T>
const [firstInitValue] = useState(() => initValue(config, init));
const isWrappingMobxProxy = !isPlainObject(firstInitValue);
// If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
const dep = isInput(init) ? makeArray(init.input) : isQuery(init) ? [init.query.data, init.query.loading] : [];
const dep = isInput(init)
? init.onlyOnce
? []
: makeArray(init.input)
: isQuery(init)
? [init.query.data, init.query.loading]
: [];

const form = useMemo(
() => {
Expand Down

0 comments on commit 67683fb

Please sign in to comment.