1

I am building a form using an array of objects that describe the elements within the form.

const fields = [
  { name: "Name", type: "text", placeholder: "What's your fullname?" },
  { name: "Email", type: "email", placeholder: "We promise not to spam you." },
  { name: "Password", type: "password", placeholder: "Make it a good one!" },
  { name: "About", type: "textarea", placeholder: "Tell us a little about yourself ..." },
];

I map over each object to produce my form which all works as desired.

{fields.map((field, index) => (
    <div key={form-field-${index}`}>
        <label htmlFor={field.name}>{field.name}</label>
        <div>
            {field.type !== "textarea" &&
                <input
                    type={field.type}
                    id={field.name}
                    name={field.name}
                    placeholder={field.placeholder}
                />
            }
            {field.type === "textarea" &&
                <textarea
                    id={field.name}
                    name={field.name}
                    placeholder={field.placeholder}
                />
            }
        </div>
    </div>
))}

As you can see I have some conditional rendering based on the type value of each field. For two different form elements this is not horrendous, but if I go adding other form elements (<select> for example), I would prefer to not have x conditionals if there is a better alternative.

Is there a better alternative? Move this to its a function, or its own component perhaps?

I am kind of hoping there is a means of doing something like:

{fields.map((field, index) => {
  <field.formType
    id="{field.name}"
    ...
    />
});

Where field.formType maps to a formType: "input" in the fields array.

2
  • 1
    You can you a function with a switch statement Commented Oct 8, 2022 at 20:59
  • A switch or an object with a key of input type and a value of component function would my go-to Commented Oct 8, 2022 at 21:00

3 Answers 3

4

You can actually do this if you assign it to a capitalised variable and then use that (it is a weird JSX gotcha). See https://reactjs.org/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized.

{fields.map((field, index) => {
  const Tag = field.formType
  return <Tag 
    id={field.name}
    ...
    />
});

Should you do this?

After some time I feel the need to qualify this answer. Whilst this works perfectly fine, there's a reason it is not a regular pattern. When you drive the component type by some var, it implies you have built a sort of DSL on top of the one already available to you: JSX. It is a possible sign that you may not have modelled your problem in the most idiosyncratic way in React. In this particular example, why not just use different components in the JSX directly without this object model above it?

JSX is already a declarative model, just like the plain object. What's the gain other than added complexity? Two distinct components which are similar can easily share logic via custom hooks.

Different form fields have different "properties" (props) and things get complicated if you start to try to abstract those differences above the component layer that already is a manifestation in and of itself of those differences.

This is not to say there are no use cases. For example, if the form structure was driven by the server or some external source in a SPA scenario, there's a likely need for the serialisation benefits the JSON layer above the JSX gives you. But please think about why you are doing this, and if you should just use separate components instead.

Sign up to request clarification or add additional context in comments.

2 Comments

Wow that's amazing! I haven't seen this in any project out there. This is so helpful.
This is ideal and just what I was looking for. Need to adjust it for use in my TS React project (chucking type safety errors), but that is a me problem.
0

I would say create separate components for each type of input (input, textarea, select, etc.) and keep using conditionals:

{fields.map((field, index) => (
    <div key={form-field-${index}`}>
        <label htmlFor={field.name}>{field.name}</label>
        <div>
            {field.type === "text" && <Input field={field} />}
            {field.type === "email" && <Input field={field} />}
            {field.type === "password" && <Input field={field} />}
            {field.type === "textarea" && <Textarea field={field} />}
            {field.type === "select" && <Select field={field} />}
        </div>
    </div>
))}

or if you can change the structure of the data in the array, add a property like formType and use that to define which component to use:

{fields.map((field, index) => (
    <div key={form-field-${index}`}>
        <label htmlFor={field.name}>{field.name}</label>
        <div>
            {field.formType === "input" && <Input field={field} />}
            {field.formType === "textarea" && <Textarea field={field} />}
            {field.formType === "select" && <Select field={field} />}
        </div>
    </div>
))}

It's clean and you can use individual logic/state/etc. inside each component if needed

Comments

0

You can provide a new field (property) like Component to your array of fields and there you can have anything you like, like Component:input, Component:select, Component: CustomComponent and then when mapping you render that component and provide other fields as props like: (inside map here)

const {Component, ...props} = field;

return <Component {...props}/>

note that for select you might need to create a custom component that when receives options as props knows how to handle them since select takes options as children and not as attributes (props)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.