Subscribe to receive notifications of new posts:

Open Sourcing CloudFlare’s UI Framework

2016-06-08

4 min read

Late last year, the CloudFlare UI team made a huge decision: to change JavaScript frameworks from Backbone & Marionette to React & Redux.

We’ve been one of the single biggest Backbone+Marionette apps on the web for a while now, and the decision to move was not taken lightly. On our team we have a former core team member of the Marionette team (myself), and the author of several popular Backbone projects: Backgrid and Backbone Paginator.

In the spirit of the open web, we want to share more about what we’re doing. This starts by open sourcing the UI framework that we have spent the last few months building.

Introducing cf-ui

While moving to React, we’ve taken our existing Backbone UI framework and rebuilt it from scratch on top of React. This includes over 50 packages that include dozens of components, utilities, test helpers, and more.

cf-ui logo

Examples: https://cloudflare.github.io/cf-uiGitHub: https://github.com/cloudflare/cf-ui

We’re not open sourcing cf-ui because we think our buttons are any better than anyone else’s buttons, but because it’s an opportunity to share some of the technical decisions that we’ve made while building out a massive React application. The hope is that this will be an awesome technical resource for many other devs.

We’ve made some interesting design decisions in cf-ui that we would like to talk about more, starting with the multi-package repo structure.

Improving the development experience with

Lerna logo

We have several different applications under development at CloudFlare: there’s our primary dashboard that the majority of our customers are familiar with, but also a number of internal and external applications that we build.

For us, being able to share code between repositories is a must have. We could just dump all the shared code into a single repository, but our applications are developed at different speeds and we want to be able to version our shared code using semver. So we use npm to build tons of separate packages.

However, maintaining dozens of packages across all of their repositories is a development nightmare. Making changes across packages is a difficult workflow, and it’s tough to test changes across all the packages.

Using Lerna, we’re able to put all of the packages into a single repository and have our ideal workflow while also versioning packages independently.

The basic idea here is that our cf-ui repository looks like this:

packages/
  cf-builder-card/
  cf-builder-form/
  ...
  cf-util-route-handler/
  cf-util-text/
lerna.json
package.json

Then each of the directories inside packages/ looks like this:

packages/cf-builder-card/
  src/
  test/
  package.json
  README.md

Each of these get built into a separate npm package and gets published whenever they have changed.

The other interesting part of Lerna is that it “links” cross-dependencies within the same repo so that you can test changes made in one package across all of the packages

For example, cf-util-http-poll depends on cf-util-http. If I make a change to cf-util-http, I want to be able to test that it didn’t break cf-util-http-poll and if it did I want to make sure that both packages get updated and versioned properly. Lerna allows us to do exactly that.

There are many more benefits to using this multi-package style repository which is why many other projects choose to use a similar structure. Projects like Babel, React, Ember, Angular, Jest, Meteor, PouchDB, and many many more all do the same thing.

Read more on the Lerna website.

Builder Components

Form Builder Component example

When we built the UI framework initially, we built each component package out to expose multiple components that you compose together like so:

import React from 'react';
import {
  Table,
  TableHead,
  TableHeadCell,
  TableBody,
  TableRow,
  TableCell
} from 'cf-component-table';

export default class MyTableComponent extends React.Component {
  render() {
    return (
      <Table>
        <TableHead>
          <TableRow>
            <TableHeadCell>Name</TableHeadCell>
            <TableHeadCell>Value</TableHeadCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {this.props.data.map(row =>
            <TableRow key={row.id}>
              <TableCell>{row.name}</TableCell>
              <TableCell>{row.value}</TableCell>
            </TableRow>
          }
        </TableBody>
      </Table>
    );
  }
}

This was obviously a lot of repetition, so we wanted some kind of “factory” components that would set it up for us. Something like:

const data = [
  { id: 1, name: 'Foo', value: 'foo' },
  { id: 2, name: 'Bar', value: 'bar' }
];

<TableBuilder
  rows={data.map(item => {
    return { id: item.id, data: item }
  }}
  columns={[{
    label: 'Name',
    cell: (data) => {
      return <TableCell>{data.name}</TableCell>
    }
  }, {
    label: 'Value',
    cell: (data) => {
      return <TableCell>{data.value}</TableCell>
    }
  }]}/>

In addition to that, one of the frustrating things with base components in Redux is hooking them into the state tree all the time. Setting up handlers for every event that dispatches actions and get reduced into state is a tiresome process.

Instead we have defined that these “builder” components can be tied directly to Redux and have their own reducers and actions.

All you have to do is set up the reducer before using the builder component.

import {combineReducers, applyMiddleware, createStore} from 'redux';
import thunk from 'redux-thunk';
import {tableReducer} from 'cf-builder-table';

const reducer = combineReducers({
  tables: tableReducer
});

const store = createStore(reducer, {}, applyMiddleware(thunk));

Then you can use builder components like they were any other component. They each just need a unique ID, which you can store in a ComponentNames.js constants file.

<TableBuilder tableName={ComponentNames.EXAMPLE_TABLE} .../>

You can also dispatch actions that do various things to the builders.

import {tableActions} from 'cf-builder-table';

// ...

dispatch(tableActions.flashRow(
  ComponentNames.EXAMPLE_TABLE,
  'rowId',
  'success'
));

Integrated Test Utilities

cf-ui also includes a number of test utilites that integrate with Mocha, Sinon, and Redux in order to prevent various mistakes while writing tests.

For example using sinon you could stub out a method inside a test:

it('should stub a method', () => {
  sinon.stub(obj, 'methodName');
  // ...
});

But if you forget to restore that stub or maybe an error causes the stub to stick around through the rest of your tests you might run into some problems.

Instead there is a cf-test-stub package that includes a stubMethod() util that is automatically sandboxed and reset in between tests, preventing you from ever making a mistake and making it even easier to use.

There’s a handful of other useful test utils that do similar things.

Wrapping APIs for the sake of it

If you’re browsing the cf-ui packages you might notice there are some packages like cf-util-logger that wrap external APIs like this:

const debug = require(‘debug’);

function createLogger(name) {
  const logger = debug(name);

  return function(message) {
    logger(message);
  };
}

module.exports = createLogger;

This might seem unnecessary, however it forces us to expose a minimal API surface area that allows us to make changes to them confidently, even swapping out the underlying modules without having to update our application code.

For example, if we decided to stop using marked in favor of remarkable or some other markdown engine we could do that in a single pull request in one codebase.

Improving the accessibility of our components

Rewriting all of our components from the ground up was an excellent opportunity to evaluate how well we’ve been following accessibility best practices.

As we rewrote the components, we found a number of improvements that we could make and we took the time to make sure that our components were as accessible as possible.

For example, we greatly improved keyboard navigation across all of our components, even open sourcing some of the libraries we built to do it: react-modal2, a11y-focus-store, a11y-focus-scope.


In closing, we’re really really excited to be open sourcing our UI framework today. A lot of work went into this and it’s great to finally be putting it out there, so please give us a star on GitHub (we love stars).

Also, if this work interests you then you should come join our team!

Cloudflare's connectivity cloud protects entire corporate networks, helps customers build Internet-scale applications efficiently, accelerates any website or Internet application, wards off DDoS attacks, keeps hackers at bay, and can help you on your journey to Zero Trust.

Visit 1.1.1.1 from any device to get started with our free app that makes your Internet faster and safer.

To learn more about our mission to help build a better Internet, start here. If you're looking for a new career direction, check out our open positions.
JavaScriptProgrammingReliability

Follow on X

Cloudflare|@cloudflare

Related posts

October 09, 2024 1:00 PM

Improving platform resilience at Cloudflare through automation

We realized that we need a way to automatically heal our platform from an operations perspective, and designed and built a workflow orchestration platform to provide these self-healing capabilities across our global network. We explore how this has helped us to reduce the impact on our customers due to operational issues, and the rich variety of similar problems it has empowered us to solve....