Tech Topics

Componentizing the Kibana UI, Part 2: Writing JavaScript with React

Components in JavaScript

This series is about how the Kibana team is componentizing the Kibana UI. In the last post in this series, I outlined how we've been applying this process to Kibana's CSS. But components are more than just CSS -- they're JavaScript, too. We went looking for a JS library to help us, and when we came upon React we found it to be a natural fit!

React is a library for building user interfaces with JavaScript, open-sourced by Facebook in 2015. In React everything is a component. This is the same fundamental principle which drives our componentization process.

According to the React documentation site, React allows you to “build encapsulated components that manage their own state, then compose them to make complex UIs.” Let’s explore these ideas in the context of our UI Framework.

Encapsulation lets us hide complexity

Engineers should be able to wield a UI Framework component like an artist wields a brush. You should be able to apply it to a medium and, with a little skill, expect a positive result.

As a corollary, an engineer who’s using a component shouldn’t have to worry about how the component works underneath the hood. Implementation details should be encapsulated, hidden from view. This includes JavaScripty things like state and performance optimizations, but it also means that entire layers of our stack become invisible. The very markup and CSS that make up our components become implementation details, which are encapsulated by our React components.

For example, take a look at how we converted our Button component to React (check out the pull request if you're interested in the details). Here’s what the component looked like originally:

<button class="kuiButton kuiButton--basic">
  Basic button
</button>

We just wrapped this markup inside of a React component, like so:

const KuiButton = ({
  className,
  type,
  children,
}) => {
  return (
    <button
      className={getClassName({
        className,
        type,
      })}
    >
      {children}
    </button>
  );
};

And now engineers can use this component like this:

<KuiButton
  type="basic"
  onClick={() => window.alert('Button clicked')}
 >
   Basic button
 </KuiButton>

By relegating the actual CSS and markup to mere implementation details, we gain three big advantages:

  1. The code that represents a user interface becomes more readable and succinct.
  2. Engineers no longer need to remember class names and specific markup. Instead, they can focus on wiring the UI with event handlers and business logic to bring the UI to life. This means greater code consistency, fewer hacks and custom solutions, and a faster development cycle.
  3. Engineers who maintain the UI Framework gain a greater degree of freedom when refactoring components -- we can rename classes and restructure a component’s markup without worrying about disrupting someone else’s workflow.

Componentization means composition

Good components can be defined by many traits. They should be modular, reusable, and ideally adhere to the single responsibility principle. But great components are also composable.

This means that many components can be thought of as simple containers, into which any other component or number of components can be injected. In the previous blog post in this series, I raised the example of a Panel component. This component is just a box with a title. And like any box, it can contain literally anything: text, buttons, a table, or even other Panels. A component is composable when it exhibits this level of flexibility.

You can put anything inside of a Panel

The process of componentizing our user interfaces largely consists of figuring out how to compose them out of components. When we look at a complex user interface that needs to be broken apart into components, we start by looking for the containers. And typically, these are the UI elements that become we can extract into components in our UI Framework. By following this simple process, we end up with an ecosystem of components which can be reused and composed together to create complex UIs.

React emphasizes composition as a first-class concept. It encourages component developers to design components to be stateless and functional. This results in components which accept dependencies, most notably the children dependency. React supports composition by allowing you to provide components as the children dependencies of other components. The React team has written a terrific article on how they designed React with composition in mind, entitled “Composition vs Inheritance”. If you’re getting started with React, I highly recommend you check it out.

I should mention that I also love stateless, functional React components because they’re so easy to demonstrate in the UI Framework. In fact, I copied the above KuiButton code snippet from our UI Framework documentation site. Unlike an AngularJS directive, which would require creating an Angular app, registering the directive to it, and creating a template in which to declare an instance of the directive, there’s very little overhead required to instantiate a React component. You just import it and instantiate it with whatever JavaScript dependencies it requires.

This also makes React components very easy to unit test. We use AirBnB’s Enzyme to render an HTML string of the component, and then we use Facebook's Jest to save this string as a snapshot, and compare it against subsequent renders.

Here’s what a test would look like:

import React from 'react';
import { render } from 'enzyme';

import { KuiButton } from './button';

describe('KuiButton is rendered', () => {
  const $button = render(
    <KuiButton>
      Hello
    </KuiButton>
  );

  expect($button)
    .toMatchSnapshot();
});

This would render a snapshot which looks like this:

exports[`KuiButton Baseline is rendered 1`] = `
<button
  class="kuiButton"
>
  <span
    class="kuiButton__inner"
  >
    <span>
      Hello
    </span>
  </span>
</button>
`;

This snapshot is committed to the repo. During subsequent executions of our UI Framework test suite, Jest will detect that the snapshot exists and compare Enzyme’s output against it. If the output doesn’t match the snapshot, we’ll see a diff in the terminal with some helpful error messages. When this happens, we can either decide that our changes to the component are ones we want to preserve, in which case we’ll update the snapshot with the latest output, or we’ll realize we made a mistake and fix the component so that the test’s pass.

Looking ahead

We’re just getting started with the transition to React. If you’d like to follow our progress, drop in on our React-related GitHub Issues and see what’s in the works. We also created an open-source React Design Workshop to help ourselves and others get started with React. Feel free to run through it and make suggestions on ways we can make it better!

Next up: Designing systems

In the next and final post, I’ll share the thrilling secret of how we’ve applied componentization to our actual design process, so that we end up creating design systems instead of just static mockups.

Interested in joining our team? Well, we’re interested in you, too. Come say hi!