React Design Patterns: Generating User-configured UI Using The Visitor Pattern
Tags: react, javascript, algorithms
Published: 2021-08-16 17:55:10

---
tags: react, javascript, algorithms
---

I had a problem in my React app: I needed to render a form with multiple inputs  of multiple types: date fields, number fields, dropdowns: the usual suspects.

But here's the kicker: similar to form builders like SureveyMonkey or Typeform, users need to be able to design these forms themselves and configure them to include whatever fields they need.

How do I go about this? Users won't be writing React themselves so I need a data model that describes their form's configuration. While data structures and algorithms are not typically my strong-suit, what I landed on is what I came to realize is the Visitor Pattern but implemented with React components.

## What is the visitor pattern?

The [Wikipedia page for the visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern) describes the visitor pattern as __"a way of separating an algorithm from an object structure on which it operates".__ Another way to put this is it changes how an object or code works without needing to modify the object itself.

These sorts of computer science topics go over my head without seeing actual use cases for the concept. So let's briefly explore the visitor pattern using a real-world use case.

[Babel](https://babeljs.io/) is a great practical example of the visitor pattern in action. Babel operates on Abstract Syntax Trees (ASTs) and transforms your code by **visiting** various nodes (eg, blocks of text) in your source code.

Here is a minimal hello world example of how Babel uses the visitor pattern to transform your code:

```js
// source.js
const hello = "world"
const goodbye = "mars"

// babel-transform.js
export default function () {
  return {
    visitor: {
      Identifier(path) {
        path.node.name = path.node.name.split('').reverse().join('')
      }
    }
  }
}

// output.js
const olleh = "world"
const eybdoog = "mars"
```
    

You can play with this example yourself [here](https://astexplorer.net/#/gist/4eb83c0722f63c31492c7fb69d4f7d20/a4b3b0c9428af4046b5feeb05792b02eb03fcc15).

By implementing the Visitor Pattern, Babel **visits** each `Identifier` token within `source.js`. In the above example, the `Identifier` tokens are the variable names `hello` and `goodbye`.

When Babel finds an `Identifier`, it hands things over to our transformation code and lets us decide how we want to transform the token. Here, we reverse the variable string and assign the result as the new name for the variable. But we could modify the code however we want.

This is powerful because Babel does all the heavy lifting to parse the source code, figure out what type of token is where, etc. Babel just checks in with us whenever it finds a token type we care about (eg, `Identifier`) and asks what we want to do about it. We don't have to know how Babel works and Babel doesn't care what we do in our visitor function.

## The Visitor Pattern In React

Now we know what the visitor pattern looks like as a general-purpose algorithm, how do we leverage it in React to implement configurable UIs?

Well, in this React app I'm building, I mentioned I would need a data model that describes a user's configured custom form. Let's call this the form's `schema`. 

Each field in this schema has several attributes like:

- **Field type.** eg, dropdown, date, number, etc
- **Label.** What data the field represents. eg, First name, Birthdate, etc.
- **Required.** Whether or not the field is mandatory for the form.

The schema could also include other customization options but let's start with these.

We also need to be able to enforce the order in which each field shows up. To do that, we can put each field into an array.

Putting that all together, here's an example schema we could use for a form with three fields:

```js
const schema = [
  {
    label: "Name",
    required: true,
    fieldType: "Text",
  },
  {
    label: "Birthdate",
    required: true,
    fieldType: "Date",
  },
  {
    label: "Number of Pets",
    required: false,
    fieldType: "Number",
  },
]
```
    

## The Simple But Limited Approach

How might we go about rendering this in React? A straight-forward solution might look something like this:

```jsx
function Form({ schema }) {
  return schema.map((field) => {
    switch (field.fieldType) {
      case "Text":
        return <input type="text" /> 
      case "Date":
        return <input type="date" />
      case "Number":
        return <input type="number" />
      default:
        return null
    }
  })
}
```

This is already looking a bit like the visitor pattern like we saw with Babel. And,  this could probably scale decently for a lot of basic forms!

However, this approach is missing the key aspect of the visitor pattern: it doesn't allow customization without modifying the implementation.

For example, maybe we want to be able to re-use this schema for other use cases like a profile view, we would have to extend our `Form` component to capture both use-cases.

## The Customizable Visitor Pattern Approach

Let's formalize our usage of the visitor pattern to enable full customization of our `schema` rendering without needing to modify the `Form` implementation:

```js
const defaultComponents = {
  Text: () => <input type="text" />,
  Date: () => <input type="date" />,
  Number: () => <input type="number" />
}

function ViewGenerator({ schema, components }) {
  const mergedComponents = {
    ...defaultComponents,
    ...components,
  }
  
  return schema.map((field) => {
    return mergedComponents[field.fieldType](field);
  });
}
```

This new `ViewGenerator` component achieves the same thing `Form` was doing before: it takes in a `schema` and renders `input` elements based on `fieldType`. However, we've extracted each component type out of the switch statement and into a `components` map.

This change means we can still leverage the default behavior of `ViewGenerator` to render a form (which would use `defaultComponents`). But, if we wanted to change how `schema` is rendered we don't have to modify `ViewGenerator` at all! 

Instead, we can create a new `components` map that defines our new behavior. Here's how that might look:

```jsx
const data = {
  name: "John",
  birthdate: "1992-02-01",
  numPets: 2
}

const profileViewComponents = {
  Text: ({ label, name }) => (
    <div>
      <p>{label}</p>
      <p>{data[name]}</p>
    </div>
  ),
  Date: ({ label, name }) => (
    <div>
      <p>{label}</p>
      <p>{data[name]}</p>
    </div>
  ),
  Number: ({ label, name }) => (
    <div>
      <p>{label}</p>
      <p>{data[name]}</p>
    </div>
  )
}

function ProfileView({ schema }) {
  return (
    <ViewGenerator
      schema={schema}
      components={profileViewComponents}
    />
  )
}
```

`ViewGenerator` maps over the schema and blindly calls each of the functions in `profileViewComponents` as it comes across them in the `schema`. 

`ViewGenerator` doesn't care what we do in that function, and our functions don't have to care about how `ViewGenerator` is parsing the schema. The `components` prop is a powerful concept that leverages the visitor pattern to lets us customize how the schema  is interpreted without needing to think about how the schema is parsed. 

## Extending The Framework

Our app has a new requirement for these user-configured forms: users want to be able to group input fields into sections and collapse content to hide them.

Now that we have a framework for implementing basic user-configured forms, how would we extend this framework to enable these new capabilities while still keeping our schema and view decoupled?

To start, we could add a `Section` component to our `components` map:

```jsx
const components = {
  Section: ({ label }) => (
    <details>
      <summary>{label}</summary>
      {/* grouped fields go here? */}
    </details>
  )
}
```
    

But we don't have a good way of identifying which fields are related to our `Section`. One solution might be to add a `sectionId` to each field, then map over them to collect into our `Section`. But that requires parsing our schema which is supposed to be the `ViewGenerator`'s job!

Another option would be to extend the `ViewGenerator` framework to include a concept of child elements; similar to the `children` prop in React. Here's what that schema might look like:

```jsx
const schema = [
  {
    label: "Personal Details",
    fieldType: "Section",
    children: [
      {
        label: "Name",
        fieldType: "Text",
      },
      {
        label: "Birthdate",
        fieldType: "Date",
      },
    ],
  },
  {
    label: "Favorites",  
    fieldType: "Section",
    children: [
      {
        label: "Favorite Movie",
        fieldType: "Text",
      },
    ],
  },
]
```
    
Our schema is starting to look like a React tree! If we were to write out the jsx for a form version of this schema it would look like this:

```jsx
function Form() {
  return (
    <>
      <details>
        <summary>Personal Details</summary>
        <label>
          Name
          <input type="text" />
        </label>
        <label>
          Birthdate
          <input type="date" />
        </label>
      </details>
      <details>
        <summary>Favorites</summary>
        <label>
          Favorite Movies
          <input type="text" />
        </label>
      </details>
    </>
  )
}
``` 

Now let's update the `ViewGenerator` framework to support this new `children` concept and enable us to generate the jsx above:

```jsx
function ViewGenerator({ schema, components }) {
  const mergedComponents = {
    ...defaultComponents,
    ...components,
  }

  return schema.map((field) => {
    const children = field.children ? (
      <ViewGenerator
        schema={field.children}
        components={mergedComponents}
      />
    ) : null

    return mergedComponents[field.fieldType]({ ...field, children });
  })
}
```
    

Notice how `children` is just another instance of `ViewGenerator` with the schema prop set as the parent schema's `children` property. If we wanted we could nest `children` props as deep as we want just like normal jsx. Recursion! It's turtles`ViewGenerator` all the way down. 

`children` is now a React node that is passed to our `components` function map and use like so:

```jsx
const components = {
  Section: ({ label, children }) => (
    <details>
      <summary>{label}</summary>
      {children}
    </details>
  )
}
```


`Section` is returning the pre-rendered `children` and it doesn't have to care how `children` are rendered because the `ViewGenerator` component is handling that.

You can play with the final solution on codesandbox:
[![Edit view-generator-demo](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/view-generator-demo-svn3z?fontsize=14&hidenavigation=1&theme=dark)

## Conclusion

Nothing is new in software. New ideas are just old ideas with a hat on. As we see in the example above, it doesn't take much code to implement the visitor pattern in React. But as a concept, it unlocks powerful patterns for rendering configuration-driven UIs.

While this article covered building a configurable "form generator" component, this pattern could be applicable for many situations where you need configuration (aka, schema) driven UI.

I would love to see what use-cases you come up with for your own `ViewGenerator` framework. [Hit me up on twitter](https://twitter.com/arahansen)! I'd love to see what you build.

## Additional Resources

- [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) is a React library that generates forms based on a [json-schema](https://json-schema.org/) and uses concepts very similar to the ones introduced here
- If you want to learn more about Babel plugins, the [Babel plugin handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#visitors) by Jamie Kyle is a great resource for walking through a practical application of the visitor pattern.
- This [Tutorial on the visitor pattern in JavaScript](https://www.youtube.com/watch?v=x-Gx0Ym1Di0) shows a brief example of the visitor pattern with just vanilla JavaScript.