Building dynamic React forms

When I decided to develop an app for connecting people similar to you, I encountered some significant challenges. The app needed to feature different categories of groups, such as sports, music, and travel. Within each category, users had to fill out a distinct form, and I aimed to design them to resemble those of Typeform.

By "resemble those of Typeform", I mean that the questions had to depend on the previous answers. For example, if the user selected that he liked football, the next question would be about the team he supports, and if he selected he liked rock music, the next question would be about his favourite band. Here is an example of such form.

Coming up with a solution for this was not easy and there were two problems that I had to solve. The first one was that I had to come up with a way to define these forms as easy as possible, and the second one was that these forms had to be defined in a way that could be stored in a database such as MongoDB.

That's how I came up with Formity, an npm package that allows you to build dynamic React forms with ease. Using this package the forms are defined using JSON syntax in a way in which you can define logic in them, like conditions, loops and operations. Since these are defined using JSON, they can be easily stored in files and databases.

Dependencies

Formity depends on two packages, Mongu and React Hook Form. We suggest you to be at least a little familiar with these packages before learning about Formity.

Example

To learn about Formity we highly suggest you to clone the following github repository.

git clone https://github.com/martiserra99/formity-example

This repository is set up with the Formity package. As we explain its usage, we'll be using the code within this repository as our point of reference.

cd formity-example
npm install
npm run dev

Installation

To install this package you have to run the following command.

npm install formity react-hook-form mongu

Components

To use this package the first thing you have to do is define the components that your form will use. You can find these at the components folder of the repository. They are created using Radix Themes, but you can create them in any way you want.

React Hook Form

There is one requirement when creating these components, though. If you take a look at the form fields like the TextField component, you will see that they need to be registered to the form, as this package depends on React Hook Form.

export default function TextField({ label, name, placeholder }) {
  const { register, formState } = useFormContext()
  const error = formState.errors[name]
  return (
    <Text as="label" className={styles.label}>
      <Label as="div" mb="1" error={error}>
        {label}
      </Label>
      <RadixTextField.Input
        placeholder={placeholder}
        {...register(name)}
        {...(error && { color: 'red' })}
      />
      {error && <ErrorMessage mt="1">{error.message}</ErrorMessage>}
    </Text>
  )
}

FormityProvider

After defininig these components you have to provide them using the FormityProvider component. You can find how this component is used in the main.jsx file.

import { FormityProvider } from 'formity'

// ...

const components = {
  LayoutForm,
  TextField,
  TextArea,
  Select,
  RadioGroup,
  CheckboxGroup,
  Slider,
  Range,
  Button,
  Back,
}

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Theme appearance="dark" panelBackground="translucent">
      <FormityProvider components={components}>
        <App />
      </FormityProvider>
    </Theme>
  </React.StrictMode>,
)

Formity

Once you have the components defined you can start to build the form. If you take a look at App.jsx, you will see that the form is created using the Formity component.

import { Formity } from 'formity'

// ...

import form from './form'

export default function App() {
  const [result, setResult] = useState(null)

  function handleSubmit(result) {
    setResult(result)
  }

  // ...

  return (
    <Center>
      <Card>
        <Formity form={form} onSubmit={handleSubmit} />
      </Card>
    </Center>
  )
}

This component receives two props, the first one is the JSON form and the second one is the callback that will be called when the form is submitted.

JSON

The JSON that defines the form is in form.json. As you inspect it, you'll notice that this JSON is defined as an array of elements of different types. All these elements used in combination let you create any form you want in a very simple way.

The type of elements that exist are Form, Return, Variables, Condition and Loop. These JSON elements let you create all kinds of logic and you can find more about them in the documentation.

Back

Finally, to navigate back to previous steps while using the form, you can use the useFormityForm() hook. You can check out how it is used in the Back component.

import { useFormityForm } from 'formity'

import { Button } from '@radix-ui/themes'

export default function Back({ children }) {
  const { onBack } = useFormityForm()
  return (
    <Button type="button" variant="outline" onClick={onBack}>
      {children}
    </Button>
  )
}