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>
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>
)
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>
);
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;
}
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>
);
}
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 🤓.
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!
Top comments (0)