Recently, the New Relic Core UI team moved to React v16.0. In support of this move, we’ve had to upgrade a handful of shared UI libraries owned by teams across the company. As most of us know, upgrading cross-company dependencies can be quite the challenge, and it takes a lot of time.

Often, during such work, we find that UI libraries are not always in the best shape for sharing. To that end, I’ve put together this list of some best practices and recommendations for building shareable UI libraries. For clarity, I’ve grouped the recommendations in three critical categories: CSS, JavaScript, and packaging and distribution.

CSS best practices

While many of the following practices are already addressed by some CSS-in-JS libraries, it’s still helpful to keep them in mind when using plain CSS or a pre-processor like Sass.

#1: Use namespaced selectors
It’s easy to mistakenly pick a name for a class that’s already in use by one or more of the consumers. So instead of using a class name like .MyComponent, it’s better to use a namespace; for example: .mylib-MyComponent.

A lengthy class name might not be so appealing, but it’s worth it to be sure the name won’t ever conflict with your consumer’s CSS.

#2: Don’t use global selectors
A global selector selects all available elements and styles them accordingly. But with global selectors, something as simple as

li { padding: 20px; }

in your library’s CSS will visually break any consumer application.

Instead, scope all the styles of your library with CSS class selectors that you own. For example, the following selector would be much safer:

.mylib-MyComponent li { padding: 20px; }

#3: Don’t apply a global reset/normalization
When you start a new project, it’s quite common to include a reset.css or normalize.css file to reset the default styles from the browser. But for shared libraries, this can be quite dangerous.

Once the consumer imports your library’s CSS, it will visually break their application unless your reset file and theirs are exactly the same, which is highly unlikely.

Instead, you can do a reset at the component level. For example:

.mylib-MyComponent {
  margin: 0;
  padding: 0;
  font: inherit;
  ...
}

Keep in mind that consumers of your shared UI library will probably have global CSS and CSS resets in place. So it’s a good idea to add a robust reset file in your components CSS. In this case, the more robust the reset is, the less likely it is that you’ll run into problems.

#4: Provide one main entrypoint
Consumers using your shared library will be pleased if you provide one entrypoint CSS file that includes all the CSS required to use your library.

So for the following CSS files:

@import './components/Icon/styles';
@import './components/Button/styles';
@import './components/Link/styles';
@import './components/Logo/styles';
...

Your consumer can do a single import to use the CSS of your library:

import '@my-scope/my-library/dist/styles.css'

JavaScript best practices

The following best practices can help streamline the use of JavaScript in your libraries.

#1: Think twice before adding a new dependency
We all know that dependencies take up space, and most of the time they also depend on other dependencies. Before you know it, dependencies can totally bloat your production bundles.

When adding a new dependency, always review its package.json file to see how many dependencies that project uses. You can use tools like bundlephobia or package-size to inspect their sizes.

#2: Use selective imports where possible
If your library uses just a couple of items from a giant library, you should consider importing only the items you’ll use. This way only the things your library really needs will end up in the consumer’s production bundle.

For example, instead of:

import { debounce } from 'lodash';
import { Grid } from 'react-virtualized';

Try:

import debounce from 'lodash/debounce';
import Grid from 'react-virtualized/commonjs/Grid'; /code>

This practice will 1) help keep your library size small, and 2) keep you from relying on your consumers having tree shaking enabled in their builds to get rid of the unused code.

#3: Provide one main entrypoint
Similar to providing one main entrypoint for your library’s CSS, it’s a good practice to provide a main index.js entrypoint from where you export all the public modules from your library.

For example, for the following modules:

export { default as Select } from './components/Select';
export { default as Input } from './components/Input';
export { default as SearchInput } from './components/SearchInput';
export { default as Checkbox } from './components/Checkbox';

Your consumers can do a single import to use whatever they need from your library:

import { Select, Input } from '@my-scope/my-library';

#4: Consider using peer dependencies
If your library depends on React or any other large library that is frequently used by your consumers, consider adding the dependency as a peer dependency. With peer dependencies you can specify ranges of versions to give more flexibility to your consumers instead of forcing them to use the version fixed in your library.

For example:

"peerDependencies": {
  "d3": "3.x",
  "react": "^15.5.0 || ^16.0.0"
}

#5: Eliminate development-only code from production
Sometimes your libraries contain code that should run only in development and not in production. This pattern is frequently used for warnings (React.propTypes) and devtools (Redux DevTools).

To wrap code blocks that can be eliminated in production, use process.env.NODE_ENV.

For example, you add the following to your code:

if (process.env.NODE_ENV !== “production”) {
   // This code will only run in development
}

When the consumer of your library builds the project with webpack’s DefinePlugin and sets NODE_ENV to production, the previous code block will be transformed to:

if ("production" !== “production”) {
   // This code will only run in development
}

If the consumer uses UglifyJS, it will be converted to:

if (false) {
   // This code will only run in development
}

And will later be stripped away completely by UglifyJS’s dead-code elimination feature.

Packaging and distribution best practices

Once you have your library working and ready for use by the rest of the world, you need to package and distribute it. This is where a lot of libraries fail. The following best practices are designed to help avoid that fate.

#1: Don’t distribute bundles in npm packages
While it’s easy to create a bundle of your library with webpack, there are some consequences to consider.

When you bundle your library with webpack, all its dependencies that are not webpack externals will end up in the bundle as well. All of your dependencies will be static code in a giant bundle. If all your dependencies are static, consumers won’t get automatic minor or patch updates. Also, static dependencies cannot be deduped with dependencies of the consuming application.

Instead, use a one-to-one transpilation, where each file is transpiled individually, instead of transpiling all files and bundling them together into a single file.

The easiest way to do this in JavaScript is to compile with Babel:

babel src/ --out-dir build/

Or you can  compile with Sass:

node-sass src/ -o build/

#2: Provide CommonJS and ES modules in your library
CommonJS modules can be used with any build tool and don’t require extra configuration. It’s not as common to provide libraries as ECMAScript (ES) modules, but they are needed to enable tree shaking.

When you package your library with Babel, you can set different configurations with the BABEL_ENV variable. A basic .babelrc configuration for CommonJS and ES modules would look like this:

{
"env": {
  "commonjs": {
     "plugins": [
       "transform-runtime",
       ["transform-react-remove-prop-types", { "mode": "wrap" }]
     ],
     "presets": [ "env", "react" ]
 },
 "esmodules": {
    "plugins": [
      "transform-runtime",
      ["transform-react-remove-prop-types", { "mode": "wrap" }]
    ],
    "presets": [
      ["env", { modules: false }],
      "react",
    ]
  }
 }
}

Note that the example sets ["env", { modules: false }] for the esmodules environment. This disables the transformation of ES modules (import/export) to CommonJS modules (require/exports), which is the main requirement for getting tree shaking to work.

Given the example .babelrc config, you could generate both versions of your library by running these two commands:

$ BABEL_ENV=commonjs babel src/ --out-dir build/commonjs
$ BABEL_ENV=esmodules babel src/ --out-dir build/es

#3: Point the npm package main attribute to the transpiled CommonJs version of your library
The package.json#main attribute indicates the path of your library’s main JavaScript entrypoint. It’s important that package.json#main points to the transpiled CommonJs version of your library, so users can use it without extra tooling or configuration.

Given the following package.json:

{
name: “@my-scope/my-library”,
main: “build/commonjs/index.js”
}

When the consumers of your library import it like this:

import myLibrary from '@my-scope/my-library';
@my-scope/my-library

resolves to the path you specified in the package.json#main attribute. Consumers won’t need to perform additional configuration.

#4: Use the npm package module attribute to enable tree shaking
The package.json#module field is not an npm official attribute, but it is the first field webpack checks for resolving a module’s main entrypoint. If you don’t set the module attribute, webpack will default to the main attribute.

Pointing the module attribute to the ES modules version of your library will enable tree shaking out of the box for consumers using webpack:

{
name: “@my-scope/my-library”,
main: “build/commonjs/index.js”,
module: “build/es/index.js”
}

#5: Use .npmignore and the npm package file attribute in published packages
Published packages should contain only the files required to use the library. Jenkins scripts, tests, documentation, and other items should not be included in the package.

Use .npmignore to keep files out of your package. Or use the package.jsonfiles attribute to explicitly include files in your package.

#6: Remove React propTypes
React propTypes are only for development and are not intended for use in production. To remove propTypes in production bundles, use babel-plugin-transform-react-remove-prop-types.

This plugin lets you wrap propTypes into conditional statements, so they can be removed in production.

Consider the following example component:

class MyComponent extends React.Component {
   static propTypes = {
       className: PropTypes.string,
       items: PropTypes.array,
       onClick: PropTypes.func,
}

   render() {
      return <div>...</div>;
   }
}

When you enable the remove propTypes plugin, the propTypes will be wrapped in a conditional ternary like this:

MyComponent.propTypes = process.env.NODE_ENV !== "production" ? {
      className: PropTypes.string,
      items: PropTypes.array,
      onClick: PropTypes.string
} : {};

That code block, after being processed by UglifyJS will be converted to:

MyComponent.propTypes = {};

#7: Use the Babel Runtime transform
This transform will help you avoid Babel helper duplication in your library’s output. You can also use this transform to create a sandboxed environment for experimenting with your code.

Set up your library for success

This is just a small sample of best practices for composing shareable UI libraries. These tips offer a good way to start proactively reducing the friction that can arise in large, cross-team projects where you share your code.

 

Javier Sánchez-Marín is a lead software engineer for New Relic in Barcelona, Spain. View posts by .

Interested in writing for New Relic Blog? Send us a pitch!