Building UIs at Pinecast

Matt Basta
Pinecast
Published in
8 min readDec 9, 2018

--

In the beginning, Pinecast was a very simple application. The first interactive features included the category picker and the upload widget: both started as vanilla JavaScript (i.e., no framework) components without even a minification step.

Pinecast has long outgrown the ability to ship features like this. While we do have the odd one-off <script> tag to support a minor feature here and there, almost all interactive UIs are driven by our main JavaScript bundle, db-omnibus (the site builder has its own bundle, sb-editor, for reasons). This bundle is created by compiling our TypeScript monorepo using Webpack. A lot happens behind the scenes, so let’s talk about some of the more interesting stuff.

Common

Pinecast Common is the name given to the component library used for modern Pinecast interfaces. It’s simple, flexible, and has a wide array of features. At the time of writing, the features that use Common include:

  • The Site Builder, which Common was originally built for
  • The upload widget
  • The Upgrade page
  • The Tip Jar signup form
  • The import tool
  • The category picker
  • The publish picker
  • The analytics dashboards
  • The Spotify submission dashboard
  • and some currently-unreleased features 😎

Almost all of the recently-built UIs use Common, and for good reason: it makes building complicated interfaces damn simple.

Common makes development easy by providing a set of components that are easily composed. Most components provide a number of variations, like you’d find with other component libraries. We were heavily inspired by Palantir’s Blueprint, for instance.

One notable feature is the ability for Common components to provide out-of-the-box accessibility. By implementing ARIA tags, components integrate well with screen readers. The colors that Common provides are designed to meet minimum contrast ratios set forth in the WAI guidelines.

Common components are also mobile-friendly. The patterns used in Common are chosen in such a way that UIs can be easily adapted for mobile. By taking advantage of inline media queries on style={} props, components can adapt to any screen size without having to add boilerplate.

Showcase

To use Common effectively, it’s important to be able to browse the available components and their variations. To support this use case, Showcase was built.

Common’s color palettes

Showcase makes it easy to find exactly the component needed to help build a UI. And, in fact, Showcase is itself built using Common components.

Besides easing development of features, Showcase also helps decrease the amount of time needed to build and test Common components. New components can be built and tested in Showcase with a variety of configurations. webpack-dev-server's auto-reloading feature makes it simple to see your changes as you make them. And when you’re done, all consumers of that component have updated across the entire app.

At Pinecast, I’ve considered switching to a tool like Framer X or Figma to help reduce the amount of time involved in designing, and shipping components. Without commenting on the efficacy of these tools, I’ve found that Showcase makes it easier for someone with an engineering focus to get a new component integrated quickly and without hassle.

Styletron

Pinecast makes heavy use of Styletron, a project by Ryan Tsao. Styletron works much like styled-components, but makes it easy to build one-off versions of styled DOM elements.

Styletron backs all Common components (with few exceptions) and all sites generated with the Site Builder. Because Styletron doesn’t generate external stylesheets, there is no extra CSS minification step, caching concerns, or asset pipeline issues to deal with. By using Styletron, we can deploy a single JavaScript artifact to our S3 bucket with no extra fuss. Unlike CSS loaded with style-loader, Styletron styles don’t require a processing step at build-time. We can inject into the DOM only the styles that are applicable for the user’s browser.

Besides being fast and developer-friendly, Styletron provides another great benefit: unlike many other primitives from other component libraries, Styletron components can be purpose-built and mixed together with O(1) initialization cost. For instance, consider the generated markup for Gestalt’s Radio component:

<div class="_12 _50 _h _z7 _4q _j">
<div class="_uy _45 _4q _2w _4s _50 _5a _v0 _v2 _37">
<input class="_v4 _4h _od _v5 _z9 _v7" id="usa" type="radio" value="usa" checked="">
<div class="_v8 _2w undefined _v9 _3d"></div>
</div>
<div class="_5j _h _z7 _4q">
<label class="_u7 _45 _z9" for="usa">
<div class="_6e _h _z7 _4q">
<div class="_w7 _0 _1 _2 _wa _3c _3 _d _b _6">U.S.A.</div>
</div>
</label>
</div>
</div>

Now consider the equivalent Common component:

<label class="f dj dk Radio--WrapperLabel Label--NativeLabel">
<input name="1" type="radio" class="t b1 dl dm bb dn co do a9 dp dq dr Radio--InvisibleRadio">
<div class="b f z b2 l ag bu ds a3 dt du dv dw dx dy dz e0 e1 e2 e3 e4 e5 e6 Radio-text">Unchecked</div>
</label>

Rather than building on top of primitives like Label, Box, or other intermediary components, each individual “styled element” in a component powered by Styletron is its own unique element specifically designed to serve precisely the function it needs to.

Generic components like Box can be useful ways to ensure consistency (grid alignment, colors, spacing) and safety (preventing float, avoiding typos). However, they also get in the way: adding a <Box padding={{left: 8}}>...</Box> component just to add a bit of spacing introduces a new <div> with its own classes, and likely resetting display, which requires additional workarounds. With Styletron, you can simply add style={{marginLeft: 8}} to any Styletron component to add a cheeky left margin. This keeps the DOM flat (improving performance and easing development), makes responsive layouts easier to build, and makes it straightforward to keep your application accessible. Adding safety is accomplished by wrapping Styletron’s styled() function and extending the provided style type.

As a matter of principle, Common doesn’t have any utility components that solely provide styles. Instead of treating a component as a parcel of styles, Common treats a component as a composition of styles. Styles can be stored as flat, vanilla (typed!) objects and spread into Styletron style objects as needed. Besides keeping the DOM flat and performance at its peak, this makes components easy to reason about and maintain. Changes to shared styles can be made with confidence without worrying about how combinations of inner or wrapper utility components might have negative effects.

Modifying Styletron

Some changes to Styletron were made for quality-of-life purposes. First, we support unitless values for all CSS properties that accept lengths, just like React’s style prop does. Second, we’ve added our own TypeScript typings to allow for more flexible management of styles.

At a higher level, we’ve added safety checks, like making sure content isn’t passed an empty string (instead of '””’, as it should be). The method that names CSS classes has also been adjusted to avoid generating classes that trigger ad blockers. We even have a template literal tag to safely allow unitless lengths in calc() functions and to sanity check syntax.

const elem = styled('div', {
height,
maxHeight,
position: 'absolute',
top: top: calc`calc(${maxHeight} - ${height})`,
});

An “example mode” was also built. This inlines styles per-component into the DOM rather than the <head>. By doing this, any UI’s DOM nodes can be extracted using dev tools (e.g., “Edit as HTML”) and copied into another non-Pinecast page. This negates many of the benefits of Styletron, but it allows for UIs to be easily shared and preserved without requiring JS dependencies or server rendering. A rendered component could, for instance, be easily shared as a Codepen simply by copying and pasting.

Performance

I mentioned above that we’ve gone to great lengths to keep the DOM flat. This, in my opinion, is a crucial aspect to any component library’s performance characteristics. It’s important to minimize load on React as much as possible, since large and unwieldy components will quickly bloat render times and make UIs feel sluggish and unresponsive.

Large amounts of effort have been expended to keep Common fast. Besides keeping the DOM flat, we’ve avoided React.Component in favor of stateless functional components. SFCs use less memory than full class-based components, which helps make sure mobile users don’t get a sub-par experience on heavy pages. To avoid excessive allocations, we switch from SFCs to class-based components with event handlers assigned to class members.

We’ve entirely avoided the use of React contexts. This has a number of benefits. Vanilla React context consumers require you to use a render prop to extract values. This necessitates the use of a closure, which gets reallocated on each render. The only other options are to use React 16.6’s contextTypes API or a closure assigned to a class member (as we do to SFCs): both require the use of a class-based component to be truly efficient. If you’re consuming multiple contexts, there is no good way to write truly efficient code.

Last, Common makes heavy use of tricks to precompute information when possible. For example, tooltips are rendered in React portals, and positioned using metrics from getClientRects. This positioning happens immediately upon mount so that tooltips (and menus, and special dropdowns) animate in and out crisply (and avoiding the need to keep rendered elements in state to transition unmounted elements in and out). Listeners to scroll, resize, and a ResizeObserver allow Common’s understanding of the tooltip’s position to be updated as it changes on-screen. Aggressive caching of ancestor nodes in WeakSets (to short-circuit expensive traversals), debouncing, and carefully hand-optimized repositioning logic ensures that views with hundreds (or even thousands) of tooltips scrolls smoothly.

Next steps

Pinecast is continuing to grow, and we’re continuing to add features! With each new feature, we expand and improve Common.

Audio components

Some upcoming features allow you to interact with audio in a much more hands-on way. Common is quickly filling out with components that help let you view and manipulate audio.

In-app audio componentry

Special uploads

One of the most important flows on Pinecast is the upload flow. Users need to quickly and easily upload audio, artwork, and more. As Pinecast grows, there are more flows which require upload components.

An upload component that you may already be familiar with

To ease the development of these features, work has been done to generalize Pinecast’s upload componentry. This work has (and will continue to) make uploads across all of Pinecast consistent and accessible.

Try it yourself

Pinecast’s front-end code is open source, so you can give Common a try for yourself:

git clone https://github.com/Pinecast/pinecast-ts.git
cd pinecast-ts
npm i
# Make sure `./node_modules/.bin` is on your path
lerna bootstrap
cd packages/common-showcase/
npm run dev

And open a browser to localhost:8004 to check out the showcase! If you have questions or comments, please reach out. We’d love to hear what you think.

--

--