Image from rocor on Flickr

This article is about using a dependency inversion to decouple view components. The advantages of doing this are as follows: more succinct legible render functions, reduced imports, and decoupled components.

Component-based UI

Component-based UI development is the standard for web application development. The component tree represents every possible state of the UI. The beauty of it is that you never have to think about the component tree. You can focus on one component at a time, along with the components that it renders. Sets of components can be grouped together and made into a package for easy import; however, in some frameworks, the UI components still need to be imported into each component in which they are used (except for Vue, yay Vue!).

Prior to components, you would need to construct a group of HTML elements according to the docs of the UI library, like Bootstrap. You would then write some JS to find that HTML and make it interactive. Now, with components, we have a much more cohesive model for building apps. Minimal coordination is needed, as the component contains most of what it needs, and you only need to interact with the props and events supported by the component.

One advantage of UI components is that it feels very similar to other, non-UI, parts of the code base. You import what you need, and then you invoke it with arguments to make it do what you want; however, there are times when a layer of indirection/abstraction makes for better code.

Dependency Inversion in UI components

I haven’t seen this type of pattern used to resolve the dependencies between view components in a component-based architecture, but it seems to me that the advantages outweigh the disadvantages.

Let’s take a look at what a component might look like in a React application. I took this example from the Material-UI docs. I swapped out JSX for React’s createElement method. To remove noise, I’m passing null as props to all components.

import { createElement as h } from 'react';

import Card from '@material-ui/core/Card';

import CardActionArea from '@material-ui/core/CardActionArea';

import CardActions from '@material-ui/core/CardActions';

import CardContent from '@material-ui/core/CardContent';

import CardMedia from '@material-ui/core/CardMedia';

import Button from '@material-ui/core/Button';

import Typography from '@material-ui/core/Typography'; function MediaCard (props) {

return h(Card, null,

h(CardActionArea, null,

h(CardMedia, null),

h(CardContent, null,

h(Typography, null, 'Lizard')

)

),

h(CardActions, null,

h(Button, null, 'Share')

)

)

}

As you can see, a large part of the code is import statements. Also, this component is inextricably coupled with the Material UI library. This is a small isolated example, but imagine how many different application components use cards for the UI. I know from experience in React that a very large part of the code base is import statements.

Let’s see know what the component might look like when using dependency inversion. Let’s call the abstraction “ui-resolver”.

import uiResolver from '../ui-resolver'

const h = uiResolver.createElement function MediaCard (props) {

return h('card', null,

h('card-main', null,

h('card-media', null),

h('card-content', null,

h('text', null, 'Lizard')

)

),

h('card-actions', null,

h('button', null, 'Share')

)

)

} // optional step

uiResolver.on('media-card', MediaCard)

In the second version, the MediaCard component is not dependent on the Material UI library at all. Material design cards are a generic design template, and are implemented by many UI packages. The less obvious part, and you may have noticed, is that now our component is no longer dependent on React! We can use this “MediaCard” component with any component library, so long as we can map the arguments to the component framework’s API.

Using dependency inversion in view components reminds me of using web components. You have to ensure that you’ve imported the web component into your project, but once you have, you can use it anywhere you’d like. And like custom elements, we can setup the ui-resolver to fail silently if the component hasn’t been registered. The ui-resolver could also print a warning to the console, whereas the custom element cannot.

Let’s take a look at how we would set this up. First, gather all of the Material UI components together as a plugin for the ui-resolver.

import { createElement } from 'react';

import Card from '@material-ui/core/Card';

import CardActionArea from '@material-ui/core/CardActionArea';

import CardActions from '@material-ui/core/CardActions';

import CardContent from '@material-ui/core/CardContent';

import CardMedia from '@material-ui/core/CardMedia';

import Button from '@material-ui/core/Button';

import Typography from '@material-ui/core/Typography'; const h = c => (...args) => createElement(c, ...args) export default function UiLibrary (uiResolver, options) {

const { on } = uiResolver



on('button', h(Button))

on('card', h(Card))

on('card-main', h(CardActionArea))

on('card-actions', h(CardActions))

on('card-content', h(CardContent))

on('card-media', h(CardMedia))

on('button', h(Button))

on('text', h(Typography))

}

Then, instantiate the ui-resolver and pass the plugin.

import UiResolverLib from 'ui-resolver-lib'

import UiLibrary from './ui-library' const uiResolver = UiResolverLib()

uiResolver.use(UiLibrary) export default uiResolver

How about the disadvantages of this method? There’s some small computational overhead. You also can’t use JSX (at least not yet).

Reference Implementation

I’ve put together a small reference implementation called Ioku (name might change). It’s 30 lines of code, not tested, not yet used, and seriously discouraged for anything other than toying around. I’ll start to iterate on it as I have time to determine whether or not this idea is even usable. Contributions are more than welcome, if anyone else is interested in exploring the idea.