T
TanStack3mo ago
other-emerald

Apparently stale `state` in `useAppForm()`, only after submit?

I've got a composition like:
<ItemSummary>
...
<Calibration item={calibrationDetail}>
<form.AppForm>
...
</form.AppForm>
</Calibration>
</ItemSummary>
<ItemSummary>
...
<Calibration item={calibrationDetail}>
<form.AppForm>
...
</form.AppForm>
</Calibration>
</ItemSummary>
The calibrationDetail is getting updated by TSQ's useQuery() hook. In the React Developer Tools "Components" pane, as I navigate between items in the controlling table view (not shown above), I can see that the Calibration's item attribute value is changing, and when that changes, I see the form renders with the values belonging to the new item. So-far, so-good. However, once I submit the form, it seems the form context becomes detached from the item attribute value. When that happens, looking inside the <Calibration> component, where I use the useAppForm() hook, I'm seeing that the state isn't updating; the "Components" devtools pane shows a createFormHook.useAppForm.useMemo[AppForm] component as the first child of my Calibration component, and that thing's first child is a Context.Provider whose value property is a FormApi. As I navigate between item rows in the controlling table view, I can see that that Context.Provider's value's store.state.values are not changing whenever the Calibration's item attribute value changes. To repeat, the store.state.values were updating whenever item attribute value changed, before I submitted the form the first time. Reloading the page restores the expected behavior, again only until I submit the form. Here's my usage of useAppForm hook in Calibration:
const formValues = { /* initial form values based on the data in `item` */ };

const form = useResourceForm({ // `useResourceForm` is an alias of `useAppForm` as returned by `createFormHook`
...formOptions({ defaultValues: formValues }),
validators: {
onSubmit: updateInputsFormDataSchema,
},
onSubmit: async ({value}) => {...},
});
const formValues = { /* initial form values based on the data in `item` */ };

const form = useResourceForm({ // `useResourceForm` is an alias of `useAppForm` as returned by `createFormHook`
...formOptions({ defaultValues: formValues }),
validators: {
onSubmit: updateInputsFormDataSchema,
},
onSubmit: async ({value}) => {...},
});
I wonder whether anyone has seen any similar behavior, and what did I do that broke it?
24 Replies
exotic-emerald
exotic-emerald3mo ago
does the error get fixed when wrapping the reactive state in a useStore? components may become stale (unlike callbacks or Subscribes), so if you need access to deep values reactively, you should use a useStore hook inside the component
other-emerald
other-emeraldOP3mo ago
inside the <Calibration/> component, I use several sub-form components, of which follows an example:
const StopwatchCalibrationSubForm = withResourceForm({
defaultValues: {
// only used for type-checking, see https://tanstack.com/form/latest/docs/framework/react/guides/form-composition#breaking-big-forms-into-smaller-pieces
stopwatchTypeData: {
time: "",
},
},
render: ({ form }) => (
<FormField> // from my chosen UI framework
<Container header={<Header variant="h3">Stopwatch calibration</Header>}>
<form.AppField
name={`stopwatchTypeData.time`}
validators={{
onBlur: stopwatchSubFormSchema.shape.time,
}}
children={(field) => (
<field.TextInputField
label="Stopwatch error"
inputMode="decimal"
placeholder="Input a number (sec/day)"
/>
)}
/>
</Container>
</FormField>
),
});
const StopwatchCalibrationSubForm = withResourceForm({
defaultValues: {
// only used for type-checking, see https://tanstack.com/form/latest/docs/framework/react/guides/form-composition#breaking-big-forms-into-smaller-pieces
stopwatchTypeData: {
time: "",
},
},
render: ({ form }) => (
<FormField> // from my chosen UI framework
<Container header={<Header variant="h3">Stopwatch calibration</Header>}>
<form.AppField
name={`stopwatchTypeData.time`}
validators={{
onBlur: stopwatchSubFormSchema.shape.time,
}}
children={(field) => (
<field.TextInputField
label="Stopwatch error"
inputMode="decimal"
placeholder="Input a number (sec/day)"
/>
)}
/>
</Container>
</FormField>
),
});
then, the <TextInputField /> uses useStore() hook:
const TextInputField: FC<Props> = ({
label,
inputMode,
disabled = false,
placeholder,
}) => {
const field = useFieldContext<string>();
const errors: ReadonlyArray<ZodError> = useStore(
field.store,
(state) => state.meta.errors,
);
const value = useStore(field.store, (state) => state.value);

return (
<FormField
label={label}
errorText={errors.map((e) => e.message).join(", ")}
>
<Input
disabled={disabled}
inputMode={inputMode}
value={value}
placeholder={placeholder}
onBlur={field.handleBlur}
onChange={({ detail: { value } }) => field.handleChange(value)}
/>
</FormField>
);
};
const TextInputField: FC<Props> = ({
label,
inputMode,
disabled = false,
placeholder,
}) => {
const field = useFieldContext<string>();
const errors: ReadonlyArray<ZodError> = useStore(
field.store,
(state) => state.meta.errors,
);
const value = useStore(field.store, (state) => state.value);

return (
<FormField
label={label}
errorText={errors.map((e) => e.message).join(", ")}
>
<Input
disabled={disabled}
inputMode={inputMode}
value={value}
placeholder={placeholder}
onBlur={field.handleBlur}
onChange={({ detail: { value } }) => field.handleChange(value)}
/>
</FormField>
);
};
So perhaps you're suggesting I need somehow to hoist my usage of useStore? I have assumed there would be some magic in TSF's <form.AppField/> component that subscribes to the (stored state) thing specified in the name attribute
exotic-emerald
exotic-emerald3mo ago
no, your usage of it looks correct … you mentioned the detached state from the form and the calibration values. It may be because of the form still containing the user overwrites so what you might need is a form reset after submission
other-emerald
other-emeraldOP3mo ago
I do already have a form.reset() in the TSF's onSubmit handler. (actually, in the onSuccess for the mutation invoked in the onSubmit; in any case, it is getting called) I elided that code from the copy/paste above for brevity. I'm in-progress trying a useStore subscription on state fields, higher in the component tree (closer to the useAppForm and with higher-level subscription spec). It must be exhausting for you to look at random folks code and try to divine what's going wrong. I appreciate the eyes, and the suggestions.
exotic-emerald
exotic-emerald3mo ago
not at all! It‘s amazing how much stuff can be found just by so many users giving it a try! Sure, sometimes the solution simple or „obvious“, but you never know when you find an issue that needs fixing if you can reproduce the issue on something like stackblitz, I can experiment with that and give more thorough feedback
other-emerald
other-emeraldOP3mo ago
I'll see if I can whittle it down to a minimal example that still exhibits the problem...
exotic-emerald
exotic-emerald3mo ago
sounds good! Just ping me with the results if you manage to reproduce it. It‘s just about midnight here so I‘ll look at it tomorrow
other-emerald
other-emeraldOP3mo ago
ttfn, have a good night ok, I tried without success to reduce the example's code footprint while still exhibiting the issue... (making a stackblitz example that reproduces the issue feels like a large side-project) but I have discovered that it's not the form submission that ends up breaking reactive re-rendering, but simply input focus (click on the input field) of any input in the form. When a user focuses a form field and then, even without updating the field value selects a different item from the controlling list, the form then fails to update, although the item prop passed from the parent component changed. Other components are updating when that selected item changes, only not the form itself. The form.state then ceases to update
exotic-emerald
exotic-emerald3mo ago
I‘m not sure I follow 😅 so you switch from one field to another, which causes desync?
other-emerald
other-emeraldOP3mo ago
so, there is a controlling "table view" and a split pane that includes the form. When an item is selected in the "table view" the split pane updates with the new item attribute rendered down via the parent. So, while I haven't yet ever focused any of the form fields in the split pane, moving around within the table view updates the whole split pane view as expected; however, if at some point I click on a form field in the split pane, then whether I update that & submit the form OR simply click a different item in the table view, tie split pane's form becomes stuck on stale state. (The rest of the split pane is updating as I click around on different items, only the form is continuing to use the stale state).
exotic-emerald
exotic-emerald3mo ago
hmm… I think I get it. If you want, you can provide the „complex“ reproduction and I can experiment and try to narrow it down I can see why reducing it is a hassle
other-emerald
other-emeraldOP3mo ago
This is the component that gets "stuck" and contains the form...
exotic-emerald
exotic-emerald3mo ago
that file size had me worried for a moment, but it's only 200 lines. I'll see if I can reproduce it, but it may take a while since it's exam weeks for me
other-emerald
other-emeraldOP3mo ago
I appreciate the extra eyes, and if I can solve on my own I'll circle back here and try to explain what I learned. It's still not clear to me whether this is "unexpected behavior from TSF" or "user error". But either way I'll post back here if I can solve. Best luck on examinations! You got this, you've studied well, no need for anxiety!
exotic-emerald
exotic-emerald3mo ago
cheers! At a glance, I see that zod'll be needed (and some UI library I can remove). Is there any other library that I should know about for the file?
other-emerald
other-emeraldOP3mo ago
TSQ for the mutation (actual persistence handled by AWS Amplify)
exotic-emerald
exotic-emerald3mo ago
on second thought, what's the UI library? best to not reduce before recreating it
other-emerald
other-emeraldOP3mo ago
aha yeah ok, the UI is AWS https://cloudscape.design
Cloudscape - Cloudscape Design System
Cloudscape offers user interface guidelines, front-end components, design resources, and development tools for building intuitive, engaging, and inclusive user experiences at scale.
exotic-emerald
exotic-emerald3mo ago
thanks 👍
other-emerald
other-emeraldOP3mo ago
This is what it looks like. At 0:00:20, when I focus the input field of the form, that's when the form state becomes disconnected from the activity in the main area; before that time, selecting different items resulted in updating the split-pane contents (including the form). After focusing the input field, selecting different items no longer updates the form (although other parts of the split pane are updated).
other-emerald
other-emeraldOP2mo ago
@Luca | LeCarbonator I appreciate the time you spent with me on this, circling back here to report: in the controlling component (the table list view where item selection is made) I added
useEffect(() => {
tsf.reset();
}, [item, tsf]);
useEffect(() => {
tsf.reset();
}, [item, tsf]);
... which completely resolves the stale-state issue.
exotic-emerald
exotic-emerald2mo ago
well, this also resets the overwritten values, but if you‘re fine with that it‘ll be an okay workaround
other-emerald
other-emeraldOP2mo ago
hmm I thought there was some /resolved or similar command to tell the Discord server to put the Big Green Checkmark on this question
exotic-emerald
exotic-emerald2mo ago
haven‘t gotten around to debugging the file you sent, but exams are finally over tomorrow, so I can do it after that this is just a forum, no bot involved you can simply close the post if you want, but inactivity will close it either way

Did you find this page helpful?