Developing a component library with React, Enzyme and Chai

As we approach moving our monolithic software into a Microservice Architecture, we face a challenge of visual consistency across apps. In the past, we have treated each app as it’s own and as a result, have ended up with visual inconsistencies. Some apps used Angular Material, some Bootstrap, etc. Due to the component nature of React, we decided to build a custom React library that can be easily loaded in and extended for each application. This also allows for anyone at ndustrial.io who needs to develop an app to leverage our library and contribute to it’s development. The goal of this is to be able to reuse our tested components throughout and customizing the styles quickly and easily for each application.

Our React component library allows us to develop small, contained, tested (Enzyme and Chai) and styled components that we are quickly able to add to any project simply by loading in the library from our private npm repository. Building components this way offers us the assurance that adding these items to our projects allows for quick and stable development where we can focus on the larger features while still maintaining control over our components. By hosting on npm, it also allows for us to properly version our library, so that any potential breaking changes are isolated until the package manifest is updated in the parent application.

You can see our header for example is small, self contained and portable.

import React from 'react';
import Dropdown from './dropdown';
import UserDropdown from './userDropdown';
import { applications } from './../../test/fixtures/applications';
import { throttle } from 'underscore';
export default class Header extends React.Component {
constructor() {
super();
this.checkDirection = this.checkDirection.bind(this);
}
componentWillMount() {
this.setState({applications: applications});
}
componentDidMount() {
window.addEventListener('resize', throttle(this.checkDirection, 200));
if (window.outerWidth > 700) {
this.setState({direction: 'down'});
} else {
this.setState({direction: 'up'});
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.checkDirection);
}
checkDirection(e) {
if (e.target.outerWidth > 700) {
this.setState({direction: 'down'});
} else {
this.setState({direction: 'up'});
}
}
render() {
return (
<div className='nd-header'>
<div className='nd-header__logo'></div>
<div className='nd-header__content'>
<div className='nd-header__app-selector'>
<Dropdown data={this.state.applications} filterable={false} direction={this.state.direction} name='Applications'/>
</div>
<div className='nd-header__user-information'>
<UserDropdown direction={this.state.direction} {...this.props}/>
</div>
<div className='nd-header__alerts'></div>
</div>
</div>
);
}
}

Then we can test.

import React from 'react';
import ReactDOM from 'react-dom';
import { shallow, mount, render } from 'enzyme';
import { expect } from 'chai';
import Header from '../../src/components/header';
import { JSDOM } from 'jsdom';
// Setup JSDOM
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;
function copyProps(src, target) {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === 'undefined')
.map(prop => Object.getOwnPropertyDescriptor(src, prop));
Object.defineProperties(target, props);
}
let auth = {
logout: () => {
return false;
}
}
global.window = window;
global.document = window.document;
global.navigator = { userAgent: 'node.js' };
copyProps(window, global);
let wrapper;
describe('Header', () => {
before (() => {
wrapper = mount(<Header auth={auth} />);
});
it('should render', () => {
expect(wrapper.find('.nd-header')).to.have.length(1);
expect(wrapper.find('.nd-header__logo')).to.have.length(1);
expect(wrapper.find('.nd-header__content')).to.have.length(1);
});
it('should reverse direction on resize', () => {
wrapper.instance().checkDirection({target: {outerWidth: 500}});
expect(wrapper.state().direction).to.equal('up');
});
it('should reverse direction back on larger resize', () => {
wrapper.instance().checkDirection({target: {outerWidth: 800}});
expect(wrapper.state().direction).to.equal('down');
});
});
Tests for the header using Enzyme and Chai

While having a component library is great and all, we also had to handle styling of components across applications. Application #1 might have 1 color scheme, while Application#2 needs a different one. We need to be able to visually differentiate across Applications and have them themed to a customers color palette.

Enter CSS Variables. All of our components are built off of CSS variables. They basically allow for you to dynamically set variables in your CSS that can be used throughout and also read and written via JS. The difference between this and SASS variables is that SASS variables are compiled into flat CSS while CSS vars are maintained as vars allowing for realtime updating.

This does present some stumbling points in the way that we can’t manipulate the colors with lighten() and darken() like you can traditionally with SASS but we are able to get around some of this with pseudo elements like ::after and ::before. These workarounds aren’t ideal but the benefits of CSS variables far outweigh the short comings.

You can see how we are using ::before to add a gradient to our header where we would normally use darken($nd-color-primary, 10)

Using pseudo elements to build a gradient

This creates:

Header with gradient.

This allows for us to change just one CSS variable to update our gradient, thus minimizing the amount of effort needed to quickly spin up new apps with new color schemes.

Once we have our colors defined in our actual components, we now have the option to overwrite them in the actual application that is loading them. This gives the new app the control to tell all of it’s components how to style.

Now that we have our color structure set up, we can change one value and it will be inherited throughout.

Default colors
Edited colors

There is some setup, some initial thought required on how to architect this properly and continued thought to ensure we are following our own rules, but if we do it right, we can create new apps quickly and effectively with minimal setup. So far, it has proven to work well and allows for us to really focus on tightening up our components and adding nice extras, such as animations, that once setup will work wherever they are added.