T
TanStack•5w ago
absent-sapphire

Generic array field with form composition

How can I create a generic array field that can be used across different forms that are created using createFormHook? Eg. I want to create this array component:
import { withForm } from '../hooks/form';

const ArrayComponent = withForm({
props: {
name: '',
children: null,
className: '',
id: '',
},
render: function Render({ form, name, children, className, id }) {
return (
<form.AppField name={name} mode='array'>
{(field) =>
field.state.value.map((_, index) => (
<div className={className} key={index} id={id}>
{children(field, index)}
</div>
))
}
</form.AppField>
);
},
});

export default ArrayComponent;
import { withForm } from '../hooks/form';

const ArrayComponent = withForm({
props: {
name: '',
children: null,
className: '',
id: '',
},
render: function Render({ form, name, children, className, id }) {
return (
<form.AppField name={name} mode='array'>
{(field) =>
field.state.value.map((_, index) => (
<div className={className} key={index} id={id}>
{children(field, index)}
</div>
))
}
</form.AppField>
);
},
});

export default ArrayComponent;
And then use it in my forms like
<ArrayComponent name='people'>
{(field, index) => (
<div key={index}>
<field.TextField
name={`people[${index}].name`}
label={i18n._(msg`Name`)}
/>
</div>
)}
</ArrayComponent>
<ArrayComponent name='people'>
{(field, index) => (
<div key={index}>
<field.TextField
name={`people[${index}].name`}
label={i18n._(msg`Name`)}
/>
</div>
)}
</ArrayComponent>
But TS is telling me that 'field.state.value' is of type 'unknown'. I originally tried using useFormContext in ArrayComponent but after doing some searching I saw that wasn't recommended, so am trying the withForm pattern. Is there something I'm missing?
10 Replies
foreign-sapphire
foreign-sapphire•4w ago
withForm would work, but not in the way you described. You could instead look at it this way:
<form.AppField name="people" mode="array">
{field => field.state.value.map((_, i) => <ArrayComponent index={i} form={form} />}
</form.AppField>
<form.AppField name="people" mode="array">
{field => field.state.value.map((_, i) => <ArrayComponent index={i} form={form} />}
</form.AppField>
The only parameter to reach the nested name field is the index, so you can add it to the withForm parameters:
const ArrayComponent = withForm({
...sharedFormOpts, // what defaultValues does the form have that's calling me?
props: { index: 0 }, // {} as YourProps is a safe assertion if you prefer that
render: function Render({ form, index }) {
const namespace = `people[${index}]` as const;
return <form.AppField name={`${namespace}.name`}>
</form.AppField>
}
})
const ArrayComponent = withForm({
...sharedFormOpts, // what defaultValues does the form have that's calling me?
props: { index: 0 }, // {} as YourProps is a safe assertion if you prefer that
render: function Render({ form, index }) {
const namespace = `people[${index}]` as const;
return <form.AppField name={`${namespace}.name`}>
</form.AppField>
}
})
absent-sapphire
absent-sapphireOP•4w ago
Interesting, will try it and get back to you in a sec. For learning's sake, why does it have to be done that way?
foreign-sapphire
foreign-sapphire•4w ago
The library philosophy is structured around passing no generics when possible. You can see https://tanstack.com/form/latest/docs/philosophy for reference
Philosophy | TanStack Form Docs
Every well-established project should have a philosophy that guides its development. Without a core philosophy, development can languish in endless decision-making and have weaker APIs as a result. Th...
foreign-sapphire
foreign-sapphire•4w ago
if I instead gave you the types it asked for, that would put your code at risk of migrating versions. Type changes are not considered breaking changes
absent-sapphire
absent-sapphireOP•4w ago
I see, thank you. Some follow-up questions then: 1. In other words passing name as a prop would go against this philosophy? Eg. if I wanted to do const namespace = ${name}[${index}] as const; 2. Can I still pass children? I want to use this as a component that can render arrays of whatever child I pass 3. If the answer is no to the above, would withFieldGroup maybe be a better fit for this?
foreign-sapphire
foreign-sapphire•4w ago
Yeah, because consider your example you gave:
<ArrayComponent name='people'>
{(field, index) => (
<div key={index}>
<field.TextField
name={`people[${index}].name`}
label={i18n._(msg`Name`)}
/>
</div>
)}
</ArrayComponent>
<ArrayComponent name='people'>
{(field, index) => (
<div key={index}>
<field.TextField
name={`people[${index}].name`}
label={i18n._(msg`Name`)}
/>
</div>
)}
</ArrayComponent>
This isn't a field behind the scenes. The internal wrapper would expect there to be a field.AppField name="..." This TextField (and all field/form components in general) are in the wrong layer. These components were added to reduce the UI boilerplate, but are unrelated to the logic side of things.
- <field.TextField name="myField" /> // but what's "field" here?
+ <form.AppField name="myField"> // responsible for logic
+ {field => <field.TextField /> } // responsible for UI
+ </form.AppField>
- <field.TextField name="myField" /> // but what's "field" here?
+ <form.AppField name="myField"> // responsible for logic
+ {field => <field.TextField /> } // responsible for UI
+ </form.AppField>
withForm and withFieldGroup are a different category because they also include the logic side of things. However, for that, you need additional data. For withForm: * What form do I expect? What component was I split from? For withFieldGroup: * What fields do I need to map to the internal ones?
absent-sapphire
absent-sapphireOP•4w ago
Okay, interesting. I'll take a bit to think through this but basically it sounds like it's not really possible to create a generic array component that I can use in different forms with very different children
foreign-sapphire
foreign-sapphire•4w ago
not in a type safe way, no I understand the sentiment completely, mind you. Arrays can get very bulky very quickly
absent-sapphire
absent-sapphireOP•4w ago
That's okay. I'll take type safety over prettiness 🙂 I think I'm fine for now. Might come back with a question or two here if I get stuck again. Thank you!
foreign-sapphire
foreign-sapphire•4w ago
let me know when there are problems :PepeThumbs:

Did you find this page helpful?