DEV Community

Cover image for React Design Patterns: Generating User-configured UI Using The Visitor Pattern
Andrew Hansen
Andrew Hansen

Posted on • Originally published at arahansen.com

React Design Patterns: Generating User-configured UI Using The Visitor Pattern

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 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 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:

// 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"
Enter fullscreen mode Exit fullscreen mode

You can play with this example yourself here.

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:

const schema = [
  {
    label: "Name",
    required: true,
    fieldType: "Text",
  },
  {
    label: "Birthdate",
    required: true,
    fieldType: "Date",
  },
  {
    label: "Number of Pets",
    required: false,
    fieldType: "Number",
  },
]
Enter fullscreen mode Exit fullscreen mode

The Simple But Limited Approach

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

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
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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:

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}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

const components = {
  Section: ({ label }) => (
    <details>
      <summary>{label}</summary>
      {/* grouped fields go here? */}
    </details>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

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",
      },
    ],
  },
]
Enter fullscreen mode Exit fullscreen mode

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:

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

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 });
  })
}
Enter fullscreen mode Exit fullscreen mode

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 turtlesViewGenerator all the way down.

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

const components = {
  Section: ({ label, children }) => (
    <details>
      <summary>{label}</summary>
      {children}
    </details>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

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! I'd love to see what you build.

Additional Resources

Top comments (0)