DEV Community

Cover image for Generics for user interfaces
Peter Szerzo
Peter Szerzo

Posted on

Generics for user interfaces

How often do we write code that manages a list of a certain resource? Despite how common this comes up, I often struggled to find good patterns for setting up dozens of them in the same codebase - manage the recipes, the team members, the invoices - without too much repetition. Recently, I took a hint from TypeScript to set up something in React that I was finally happy with.

Basically, if TypeScript is good at handling this:

interface Recipe {
  title: string
  description: string
}

type Recipes = Array<Recipe>
Enter fullscreen mode Exit fullscreen mode

Then a TypeScript-friendly frontend framework like React could surely do this:

// We get to RecipeEditorProps later in the post
const RecipeEditor: React.FC<RecipeEditorProps> = () => (
  <div>
    {/* TBA */}
  </div>
)

const RecipeListEditor: React.FC<RecipeListEditorProps> = () => (
  <div>
    {/* Some abstraction involving <RecipeEditor/> */}
  </div>
)
Enter fullscreen mode Exit fullscreen mode

tldr for those who would just like to see the thing, here is a CodeSandbox.

What is a sensible abstraction that takes a component responsible for a resource and turning it into a component that handles a list of them? For lack of a better word, I am going to call it a generic UI - one that works on structures comprised of a certain unspecified type of, well, thing.

FP enthusiasts might get excited about fancier names like functor UIs or UI lifting, but I will hold off on those for the time being.

A recipe editor

A component that is responsible for editing a recipe might look like this:

interface RecipeEditorProps {
  value: Recipe
  onChange: (newRecipe: Recipe) => void
}

const RecipeEditor: React.FC<RecipeEditorProps> = (props) => (
  <div>
    <input
      value={props.value.title}
      onChange={(ev: ChangeEvent<HTMLInputElement>) => {
        props.onChange({
          ...props.value,
          title: ev.target.value
        });
      }}
    />
    <input
      value={props.value.description}
      onChange={(ev: ChangeEvent<HTMLInputElement>) => {
        props.onChange({
          ...props.value,
          description: ev.target.value
        });
      }}
    />
  </div>
);
Enter fullscreen mode Exit fullscreen mode

This controlled component allows its parent to manage the resource in question so the state can be flexibly managed high enough in the component hierarchy.

Combining into a list

We can build on this simple editor to create a list of them: simply map over the list of resources and wire up the change events to (immutably) update the list at the right index, with some delete buttons to top it off. I could add some code for it here, but at that point I added another React todo list tutorial onto the pile.

Instead, let's look at a list manager component that doesn't care what is inside the node.

Abstracting a generic list editor

This abstract ListEditor component would take the resource editor component as a prop and do the rest of the work for us. Here's some type definitions for the props of such a component:

export interface Props<T> {
  values: Array<T>;
  onChange: (newValues: Array<T>) => void;
  newValue: () => T;
  newValueLabel?: string;
  Editor: React.FC<EditorProps<T>>;
}

// The props for the item editor, parameterized by T
export interface EditorProps<T> {
  value: T;
  onChange: (newValue: T) => void;
}
Enter fullscreen mode Exit fullscreen mode

At this point, everything is parameterized by T, which we can later fill in as Recipe, User etc. In addition to values and onChange, the component will also need a few peripheral props like how to create a new value when the add button is clicked, and what label said button should have.

The implementation looks roughly like this:

function ListEditor<T>(props: Props<T>) {
  return (
    <div>
      <div>
        {props.values.map((item, index) => (
          <div>
            <props.Editor
              value={item}
              onChange={(newItem) => {
                // Use a helper to immutably change item at an index
                props.onChange(setAt(index, newItem, props.values));
              }}
            />
            <button
              onClick={() => {
                // Use a helper to immutably remove an item at an index
                props.onChange(removeAt(index, props.values));
              }}
            >
              Delete
            </button>
          </div>
        )}
      </div>
      <button
        onClick={() => {
          props.onChange([...props.values, props.newValue()]);
        }}
      >
        {props.newValueLabel || "Add new"}
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We finally get the instantiate a <props.Editor /> instance with the appropriate props, and add all the peripheral UI like add and delete buttons that would then look consistent wherever this component is used.

Adding UX goodies in peace

Now that we have a generic list editor component, we can add fancy UX features knowing that they will propagate for every single list editor in our codebase.

In the CodeSandbox, I added react-beautiful-dnd to allow simple rearranging for both the recipes list and the users list. The individual editors never found out they were being pushed around 🤓.

Drag and drop demo

Conclusion

What can we do with this? I don't think it pattern makes sense as some kind of importable package - it is still quite coupled to styled UI code, and decoupling it would lead us down the road of fancy custom hooks, more custom component props or functions-as-child. I think we will be better off just setting up these few dozen lines of code in our projects and customize it to our own needs.

Perhaps more important is the general idea: components that manage a constellation of things without needing to know what the things themselves are. This kind of decoupling has saved me countless hours of maintenance work on complex projects well beyond lists. I hope it's useful for you as well!

Latest comments (0)