T
TanStack•4w ago
graceful-beige

Accessing form API outside of the form component

I'm struggling to understand how to access formApi outside of the component where I've created the form. Is there some context provider or something to be able to use the form? What I'm trying to do is reset the form (form.reset()) outside of the form itself. So basically, I have made a simple draft system with zustand persistent store. I have a component which uses the store, and there is a clearDraft function, which is triggered by a button. I want it to also reset the form since clearing the draft from the store does not re-render/reset the form.
19 Replies
graceful-beige
graceful-beigeOP•4w ago
My form
export default function ClientApplicationForm() {
...
const form = useAppForm({
...createClientApplicationformOptions(
initialValues || (draft as ClientApplicationFormValues),
clientInformation?.company_name
),
onSubmit: ({ value }) => {
if (mode === "edit") {
updateClientApplication(value);
return;
}

createClientApplication(toClientApplicationRequestData(value));
},
});

...

return (
<form>
...
</form>
)
}
export default function ClientApplicationForm() {
...
const form = useAppForm({
...createClientApplicationformOptions(
initialValues || (draft as ClientApplicationFormValues),
clientInformation?.company_name
),
onSubmit: ({ value }) => {
if (mode === "edit") {
updateClientApplication(value);
return;
}

createClientApplication(toClientApplicationRequestData(value));
},
});

...

return (
<form>
...
</form>
)
}
sunny-green
sunny-green•4w ago
if it's a simple wrapper and can be used from any form, you can use AnyFormApi as type reset would be one of those cases does the reset button live inside of the ClientApplicationForm, or is it a sibling / a parent?
graceful-beige
graceful-beigeOP•4w ago
It's a sibling. It would be easy if it was inside, but I don't want to move it inside - it's inconvenient ui-wise and other cases.
sunny-green
sunny-green•4w ago
your hook seems to live too far down, then. ClientApplicationForm could use withForm so that it preserves typing within, but accept a form from the parent
graceful-beige
graceful-beigeOP•4w ago
I generally struggle with wrapping the form, I also wanted to have a custom hook or a component which would create the form using the useAppForm and like add additional feature inside it like drafts so I don't connect everything manually every time I create a form.
sunny-green
sunny-green•4w ago
the form variable itself is stable, so you shouldn't expect rerenders
graceful-beige
graceful-beigeOP•4w ago
yep, that should work. Can a form which uses withForm have another forms deeper which also use withForm? Because currently my ClientApplicationForm already has couple of withForm for multi-steps
sunny-green
sunny-green•4w ago
yes, you can nest them. Just keep the following in mind: withForm expects the type of defaultValues, validators and onSubmitMeta to align strictly. If there's a mismatch, it will error. to nest them, you just pass the form you receive onto the next withForm
graceful-beige
graceful-beigeOP•4w ago
ok, thanks is there a way to type props in withForm?
sunny-green
sunny-green•4w ago
the props object is not used at runtime, so assertions are safe
withForm({
props: {} as YourComplexProps
})
withForm({
props: {} as YourComplexProps
})
typescript doesn't allow partial inference, so the current generics don't allow you to set it there
graceful-beige
graceful-beigeOP•4w ago
Seems like those are not the props I expected. Can't I pass props like children etc.? I get type Props is not assignable to Record<string, unknown>.
sunny-green
sunny-green•4w ago
that seems to be an old bug where interfaces couldn't be used as props. Try converting to a type to confirm this has been fixed, but I don't remember the version v1.13.1
graceful-beige
graceful-beigeOP•4w ago
yep, it worked, thank you
sunny-green
sunny-green•4w ago
one of the few cases where the difference between interfaces and types actually mattered. Oh well, at least newer versions don't have to deal with that in case you run into this in the future again, you can convert interfaces to types too:
interface Foo {}

// Converts Foo from interface to type.
const bar = Omit<Foo, never> satisfies Record<string, unknown>
interface Foo {}

// Converts Foo from interface to type.
const bar = Omit<Foo, never> satisfies Record<string, unknown>
Way uglier than just changing the interface to a type, but it's not always an option.
graceful-beige
graceful-beigeOP•4w ago
oh, haha, thanks will keep in mind 🙂 Hi, @Luca | LeCarbonator, I've realized it's too painful for me to move the form up as it's used also in a different place and I don't like how messy it becomes. I really want my form to be inside a specific form component. I tried to save the forms (formApi) in a global store/state but not sure if it's a solid approach.
import { create } from "zustand";
import type { AnyFormApi } from "@tanstack/react-form";

type FormRegistryStore = {
forms: Record<string, AnyFormApi | undefined>;
registerForm: (key: string, form: AnyFormApi) => void;
unregisterForm: (key: string) => void;
getForm: (key: string) => AnyFormApi | undefined;
};

export const useFormRegistry = create<FormRegistryStore>((set, get) => ({
forms: {},
registerForm: (key, form) =>
set((state) => ({
forms: { ...state.forms, [key]: form },
})),
unregisterForm: (key) =>
set((state) => {
const { [key]: _, ...rest } = state.forms;
return { forms: rest };
}),
getForm: (key) => get().forms[key],
}));
import { create } from "zustand";
import type { AnyFormApi } from "@tanstack/react-form";

type FormRegistryStore = {
forms: Record<string, AnyFormApi | undefined>;
registerForm: (key: string, form: AnyFormApi) => void;
unregisterForm: (key: string) => void;
getForm: (key: string) => AnyFormApi | undefined;
};

export const useFormRegistry = create<FormRegistryStore>((set, get) => ({
forms: {},
registerForm: (key, form) =>
set((state) => ({
forms: { ...state.forms, [key]: form },
})),
unregisterForm: (key) =>
set((state) => {
const { [key]: _, ...rest } = state.forms;
return { forms: rest };
}),
getForm: (key) => get().forms[key],
}));
But just in case, maybe there's already something in ts form for that case?
sunny-green
sunny-green•4w ago
how many side effects does the draft button have? Is it just to reset the form? native HTML allows buttons to use a form="id" attribute, pointing to a form element's id to trigger its events even when outside of it
graceful-beige
graceful-beigeOP•4w ago
It resets the form and clears the draft from the store
sunny-green
sunny-green•4w ago
<form>
{/* Works because button is inside form */}
<button type="submit">Submit</button>
</form>

<form id="foo">
</form>
{/* Works because button points to form */}
<button type="submit" form="foo">Submit</button>
<form>
{/* Works because button is inside form */}
<button type="submit">Submit</button>
</form>

<form id="foo">
</form>
{/* Works because button points to form */}
<button type="submit" form="foo">Submit</button>
so you could catch the form reset event, prevent default etc., then call form.reset from within the form component
graceful-beige
graceful-beigeOP•4w ago
thank you

Did you find this page helpful?