Table of Contents#
Introduction: How to share React Components between Applications?#
Check TLDR if you donât want all the details and just want the recommended solution.
There are diverse situation in which we want to create an NPM package that contains a React Component with CSS and images.
This situation is particularly common inside companies that want to share components, and pieces of code in general, between apps usually inside a private NPM repository, but it also applies when authoring Open Source software like a single Component package such as a Date Picker or a Multi Select and also when authoring Component Libraries or UI Frameworks like React Bootstrap, Ant Design, Semantic UI, Material UI, etc (more on these later).
This problem might seem simple enough but it turns out that it has a lot of nuances and trade-offs.
The main challenge in packaging a React Component is surprisingly not Javascript, but all the Assets that might be related to that component such as CSS, Images, SVGs and fonts. These assets are directly related to our Javascript code via some of the following statements:
imports image from './image.{png,jpg,svg}'
in Javascriptimport classNames from './styles.{css,scss,less)'
in Javascripturl(image.{png,jpg,svg})
in CSSfont-family and @import
in CSS
Webpack can understand all these dependency statements via loaders and completely analyse the cross-language and cross-file-type dependency graph, which for a very simple component might look something like this.
--> styles.css --> otherImage.svg
MyComponent --> image.svg
It turns out that there are a lot of schemes for representing this graph in a Packaged NPM module, each with its own trade-offs. Analysing each alternative is the main purpose of this post.
Unfortunately in early 2020 there is still no universal standard way of packaging a Front End module with all its assets, but a lot of progress has been made and there are actually lots of promising solutions.
We are going to explore how this problem is tackled in the wild by studying each technique and providing examples of real world, production-ready libraries that use them and in the end of this post we are going to recommend a particular solution to this problem based of my experience.
Additionally we are going to explore how each of these packaging schemes enable different Theming schemes and, at the end, recommend one (Theming is a way of modifying the styles of a set of components to fit your use case i.e. changing the buttons colors in a UI framework such as Bootstrap to fit your companyâs graphics design spec).
Most of this post is also applicable to any piece of Front End Javascript code that is related to CSS, images, etc, such as Angular Components / Directives, Vue Components, etc.
To Inline or not to Inline#
In the context of Front End assets, inlining means to bake those assets directly into the Javascript code (bundle) generated by our bundlers or compilers such as Webpack or Babel.
CSS can be inlined without any third party library by using the style
HTML attribute,
but this has some limitations such as not being able to use pseudo selectors like :hover
and media queries so, to enable all these regular CSS features
a third party CSS-in-JS library is recommended such as Emotion
or Styled-components (more on this later).
Images can be inlined by Webpackâs url-loader in the form of Data URL with Base64 encoding.
SVG can be inlined by turning an .svg
file into a React Component (An SVG file is just a regular HTML tree)
or by using Webpackâs inline-svg-loader
For a deeper exploration of how to deal with svg
in React checkout this post
The main advantage of inlining is that after you bundle all your code base you end up with a single Javascript file that contains all the dependencies it needs to run, even CSS and Images.
This makes it very easy to package and publish to NPM and it makes very easy for Component Consumers to use your module.
Using CSS-in-JS libraries like Emotion or Styled-components makes up for a very nice Run-Time Theming API. Since everything is Javascript, your Themes are regular Javascript object that can be composed or broken down into parts as any other regular JS object, and these can be altered at Run-Time, Themes can be composed, and they provide a much simpler strategy than Build-Time CSS generated Themes like Bootstrap or Ant Design. Material UI React uses a Run-Time Theme via a custom CSS-in-JS solution.
Inlining is particularly useful for Component libraries or even UI Frameworks because they tend to be mostly CSS heavy and with a couple of SVGs at most, both of which are efficiently inlined.
Images are a bit more problematic because they are inlined in Base64 making them 30% bigger.
In my experience, most Component Libraries do not tend to use an overabundance of images and if they do they usually are rather small.
Most high resolution images are dynamic content that is usually dealt by Applications and not libraries, although libraries can render images, they do not ship with those images since they are Content provided by APIs or static files in the App.
The main drawback of inlining is that you turn multiple files that can be fetched in parallel by the browser to a single larger file that cannot, this will become more noticeable in larger code bases such as big Component Libraries or Applications.
If you have a big enough library of components you might be able to break it into independent chunks that can be dynamically imported by Applications, allowing more parallelism. Dynamic imports is a new Javascript feature that lets you express a dynamically loaded dependency that will be fetched and interpreted only when needed if needed at all. This feature allows bigger and complex applications to reduce the initial load time by loading only what is necessary for the particular portion of the app that the user is interacting with. This is actually not a super far fetched solution since dynamic imports are becoming widely available and Webpack has been making good progress towards an easier configuration for multiple chunks.
Not-Inline#
As Component Authors#
The configuration is pretty standard and you can use already made solutions such as create-react-library.
You will end up with a bundle made of different files.
A typical output might look like this:
/dist
- bundle.js
- bundle.css
images/
- dog.jpg
This is what you as an Author would publish to NPM.
bundle.css
will use the image by doing url(./images/dog.jpg)
, so
both files need to be served on the following URLs
yourSite.com/public/bundle.css
yourSite.com/public/images/dog.jpg
public
can be any path with a trivial length, the important part is
that they are both served from the same path
Aside from properly documenting how the files inside your package relate to each other there is not much else you can do.
You can also use this strategy by allowing access to your module via a CDN (Content Delivery Network use to load Javascript, CSS, etc from specialized servers, checkout UNPKG), by which Component Consumers can simply reference the module.
As Component Consumers#
This is where this approach has a hard time providing a good Consumer experience.
Consumers need to:
import module from 'module'
for Javascript, in every file that uses the module-
Import the CSS by using either:
- A bundler like Webpack:
import "module.css"
only once in your Application- HTML link:
cp ./node_modules/module/module.css ./public/images/dog.jpg
: copy the CSS file to your public directory<link type="text/css" rel="stylesheet" href="public/module.css">
: load the CSS in your page.
-
Serve images at the path the moduleâs CSS and/or Javascript files expect them, in this case at
./images/dog.jpg
cp ./node_modules/module/images/dog.jpg ./public/images/dog.jpg
- Serve
public/images/dog.jpg
by properly configuring your Application
We assume that public
is your Appâs public directory from where you serve your
static files and assets, for convenience the directory is called public
and the
URL path is also called public
.
Notice how to Consume a simple module you need to do a lot of steps and while there are tools to automate the process such as Webpack, Grunt, Gulp or a package like npm-assets, the overall experience is not optimal.
An obvious additional problem is potential name conflicts,
for example if two libraries need to serve images/dog.jpg
.
Component Authors might make the module available via a CDN and that might remove the need to fiddle around with CSS and Images but you will need to access the library via a Browser Global like the good old days, and by doing this you are removing the possibility of bundling optimizations such as Tree Shaking that will become more and more common in the future.
Inline#
As Component Authors#
The Webpack configuration is about the same as with Not-inline and is actually not that hard and a working example will be provided at the end of this post.
You also will be able to use create-react-library since it inlines by default.
The key Webpack configuration is the inclusion of url-loader to inline images and files right into the final Javascript bundle.
If you use CSS-in-JS libraries such as Emotion or Styled-components then you will be able to use all the CSS features, plus compose them via Javascript and the Theming experience will be awesome. You can easily create a Run-Time default Theme and provide hooks for Component Consumers to easily change it at Run-Time.
For larger Component Libraries you will have a greater experience with writing and maintaining CSS. You will avoid completely some CSS exclusive problems such as:
- Global Scope and rule collisions.
- Specificity (inline-styles have maximum specificity, CSS-in-JS libraries simulate this).
- Style composition. I believe that regular Javascript object composition is better than CSS cascading global composition model, plus you can create a custom system that fits your team and your business logic (remember, itâs regular Javascript). You can easily publish your Themes as private NPM packages internally to shared them among Applications.
While regular inline-styles will go a long way, you will most likely
need features that inline-styles do not provide and that regular CSS do such
as pseudo selectors like :hover
and media queries.
To solve this problem and provide better Developer Experience overall is that
CSS-in-JS exists.
I recommend going for Emotion or Styled-components since they are very mature and feature rich, have good documentation and an active community.
As Component Consumers#
Here is where the Inline approach shines. To use a module with all its styles and assets such as images you simply need to write a single line of code:
import module from "module";
That is all!
If you want to import the module dynamically (to parallelize and defer its loading in the client) the experience is very similar:
// Inside some function or component
const module = await import("module");
If you have Webpack configured correctly to create chunks that can be
dynamically loaded, this is called Code Splitting
, then module
will be an independent file lazy loaded, only when it is needed.
Latest version of Webpack make this configuration almost trivial, check out their docs.
This approach also works with the Classic Way of using dependencies via Browser Globals.
Theming#
For Component Libraries or UI Frameworks Theming is a must. They need to provide a way for Component Consumers to change the default look of the components to fit their brand or their business.
Letâs inspect different strategies of providing Theming and Customization hooks.
Regular CSS Class Names#
This approach works great for single or few component libraries but falls very short for larger Component Libraries or UI Frameworks.
The basic premise is that Consumers use well defined CSS class names to alter the Componentâs looks.
This approach works only at Run-Time.
As Component Author#
- You write regular CSS Classes (without CSS modules) and you relate them to your components via
className
. - You can use LESS or SASS or any other CSS preprocessor.
- You should use Class Names naming conventions such as BEM or SMACSS among others.
- You cannot use CSS Modules since they generate pseudo random Class Names that cannot be used consistently by Consumers to customize or theme your components.
- You structure your classes carefully to allow Component Consumers to Theme or Customize your components styles by overriding some of those classes.
As Component Consumer#
You use the Component Author provided class names to alter the componentâs looks.
CSS Theme variables#
This approach is much more robust. The main premise is that Component Authors use a set of well defined CSS, LESS or SASS variables to derive the rest of the styles. These variables conform the Theme. Component Consumers can alter these variables to generate different looking themes.
Examples of these are
This approach works only at Build-Time and may require a build system, depending on whether you use CSS variables and target only supported browsers or you use SASS, LESS or a transpiled version of CSS variables (i.e. using this PostCss plugin)
As Component Authors#
- Carefully abstract the core theme variables and derive the rest of the styles from it.
- Provides a build system or build system integration documentation for Component Consumers to alter theme variables and compile new stylesheets.
- Works with or without CSS Modules.
As Component Consumers#
-
You will need to either:
- build an out of band StyleSheet with the Component Authorâs provided build system ala Bootstrap or Ant Design.
- integrate Componentâs Author build system into yours and build it in-band with the rest of your app.
- Run-Time build is not possible and you must work around it.
Note
Out of band and in band are terms I got from signal processing theory and in this case Iâm using it to describe whether you build the StyleSheet in a separate build process and codebase (out of band) or you integrate the necessary build configurations into your own build process and codebase (in band).
Inline CSS (through CSS-in-JS)#
This approach uses Core Theme Variables defined at Run-Time and passed through Javascript to components to alter their looks.
It works well with the Inline Paradigm
This approach works at Run-Time and does not require a build system.
As Component Authors#
- Carefully abstract the core theme variables and derive the rest of the styles from it.
- Use CSS-in-JS libraries such as Emotion or styled-components.
- Use those libraries to provide theming hooks to Component Consumers.
As Component Consumers#
- You must use the same CSS-in-JS libraries that the Component Author used.
- You simply define regular Javascript objects with core theme variables and pass them to Components.
TLDR: How to package and publish Front End libraries in 2020#
This is my ideal setup for packaging and publishing Front End libraries such as React Components in 2020.
This approach uses an Inline, Actively Compiled (see Appendix 2), Run-Time Theming model, that uses the following technologies:
- React as the main component framework
- CSS-in-JS via Emotion
- Inline CSS via CSS-in-JS and the rest of the assets are inlined via Webpackâs
url-loader
- Webpack as the bundler
Check the full working source here
To publish it to NPM:
- make sure you have the
package.json#main
file pointing to the output of your compilation, in this casedist/index.js
- run
npm version patch | minor | major
to bump the version inpackage.json
and create git tags - run
npm publish
Make sure you publish to the right NPM repository before doing it as most of the time companies do not want private packages to be publicly available by mistake. Read more here
Webpack configuration
const path = require("path");
// This is where we define the Inline magic.
// This loader will turn all .svg, .jpg and .png files
// into something that can be inlined in the final bundle
const fileRules = {
test: /\.(svg|jpg|png)$/,
use: [
{
loader: "url-loader",
options: {
// All files no matter what size
limit: Infinity,
},
},
],
};
// Pretty standard babel configurations for modern react apps
const jsRules = {
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
};
module.exports = {
entry: "./src/index.js",
// Will defer minifaction to Applications bundler
mode: "development",
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"),
libraryTarget: "umd",
},
// Manually tell webpack about our "peerDependencies"
// that should not be included in the final bundle and
// will be provided by the Component Consumer, like an Application
externals: [
"react",
"react-dom",
"@emotion/core",
"@emotion/styled",
"emotion-theming",
],
module: {
rules: [jsRules, fileRules],
},
};
Example Component with CSS-in-JS, inline assets and Theming
import React from "react";
import styled from "@emotion/styled";
import { ThemeProvider } from "emotion-theming";
import dog1 from "./dog.jpg";
import dog2 from "./dog2.jpg";
const defaultTheme = {
colors: {
primary: "blue",
},
};
// Use regular CSS features through CSS-in-JS
// instrumented by Emotion
const Container = styled.div`
margin: 10px;
padding: 10px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
& > div {
margin: 10px;
}
`;
const Name = styled.span`
font-weight: bold;
color: ${(props) => props.theme.colors.primary};
`;
// Use the image from CSS-in-JS
const Dog2 = styled.div`
background: center / contain no-repeat url(${dog2});
width: 100px;
height: 100px;
`;
// Use the image from Javascript code
const Dog1 = () => (
<div>
<img src={dog1} width="100" />
</div>
);
export default function MyComponent(props) {
return (
<ThemeProvider
theme={(ancestorTheme = {}) => {
console.log("theme", ancestorTheme);
return { ...defaultTheme, ...ancestorTheme };
}}
>
<Container>
<div>
Im MyComponent, hi <Name>{props.name}</Name>, and
my color comes from a theme
<br />
Here are some pictures of dogs
</div>
<Dog1 />
<Dog2 />
</Container>
</ThemeProvider>
);
}
Component Consumer
import React from "react";
// In _real_ applications you would use it like this
import MyComponent from "my-lib";
// Theming API!
import { ThemeProvider } from "emotion-theming";
const appTheme = {
colors: {
primary: "green",
},
};
export default function App() {
return (
<div>
<h1>We are using MyComponent</h1>
<p>
It was Actively Compiled and now we can use it from
a Create React App Application
</p>
{/* App defined theme at runtime */}
<ThemeProvider theme={appTheme}>
<MyComponent name="Luke Skywalker" />
</ThemeProvider>
</div>
);
}
Notes
- make sure you have a single copy of
@emotion/core
in your final Application bundle to enable Theming. This is done by properly managingpeerDependencies
(more on this later). - see it working in action in the linked repo.
Using create-react-library#
We can achieve the same by using create-react-library, the library code is the same and we remove the need for manually configuring Webpack, the output bundle is slightly different since create-react-library uses Rollup instead of Webpack.
Checkout the code here, I am using the vanilla configurations that create-react-library provides with one exception:
- url(),
+ url({limit: Infinity}),
This forces everything to be inlined no matter what is its size.
Closing#
Congratulations reading this far!
You now know how to share code between different consumers such as Applications and when to apply each of the studied solutions.
If you have any questions let me know!
Appendix 1: How to use Peer Dependencies#
TODO double check how we are handling externals
As a Component Author you need to manage Peer Dependencies correctly to allow
Component Consumers make the best possible final bundle avoiding duplication of dependencies
but also some libraries such as React
and Emotion
donât work correctly if there are multiple versions
of them running at the same time.
What are Peer Dependencies?#
Every dependency that might or should be shared by multiple libraries used by a single Component Consumer should be treated as Peer Dependencies.
Examples of obvious Peer Dependencies are
react
: use by lots of components, breaks if there are more than one copy of it runninglodash
: used by lots of libraries and componentsredux
@emotion/core
: Theming breaks if you have more than one copy at the same time. ref
How do I mark a dependency as a Peer Dependency?#
In your package.json
// These are dependencies your lib expects to be present in your
// Component Consumer's context (i.e. the bundle)
"peerDependencies": {
"react": "~16.0.0",
}
// Most of the time you will need your peer dependencies
// to be installed when testing your library.
"devDependencies": {
"react": "~16.0.0",
}
Important make sure you are as wide as possible with the version ranges of your peer dependencies, this will allow your package, as a Component Author, to be used without frictions in more situations.
In your webpack.config.js
// This tells webpack to not include these dependencies in the final bundle
// since you expect them to be provided in the Component Consumer's context.
externals: ["react"];
Appendix 2: Active vs Passive compilation#
TLDR: Do not use Passively compiled dependecies ever!
I have seen situations in which packages are distributed raw,
without being compiled before being published, this means Component Authors
package and distribute the raw sources like Typescript, ES2054, JSX, CSS Modules, etc.,
and rely on Component Consumers, like Applications, to compile them usually
by configuring Webpack to also treat node_modules/my-raw-dependency
as part of the Application source.
This is what I call Passive compilation because Component Authors delegate their package compilation to the Component Consumers, instead of Active compilation in which each package is distributed already compiled by its Component Author.
Today you can go a long way with Webpack abstractions like Create React App and Create React Library, but you need to do things in a fairly standard way and Passive Compilation is not at all a standard way of distributing code.
Check what the team behind Create React App has to say about it.
It wouldnât surprise me if other standardized high level build tools take a similar stand for reasons we are going to talk about more later.
In my experience Webpack configurations are something that most developers do not know or do not want to touch so having these sorts of abstractions inside your company will save you a lot of trouble and maintenance, which translates to less operation costs. These Webpack abstractions have lots of smart people working on them and pouring their past experiences in creating something that will cover lots of use cases you probably wonât ever think of, so you are getting even more things for free than you anticipate.
Additionally, having custom Webpack configurations creates a potential opportunity to tightly couple other parts of your system like configurations, Services URLs, CDNs, PORT assignment, etc. to your Webpack build and configuration which makes it potentially brittle to any tweaks since it might affect some tightly coupled relationship in some obscure place. Relying on standard Webpack abstractions provides a very clear interface where you can pass configuration values and custom behaviors effectively reducing this coupling.
Finally, and with a more of a âbusinessâ look, it is much more efficient and effective to rely on pre-made solutions for Application bundling and compilation. Webpack and the other bundlers are hard to configure exactly right and as a company you will need to maintain and bug fix your hand made configurations if you do them yourself. It is effectively one more source of bugs you will have to deal with, so rely as much as possible on third party open source solutions and for that you must avoid Passive Compilation.
Passive Compilation#
As Component Authors#
Advantages#
It is trivial to setup since you simply distribute your raw files. Some sort of an attempt at Active Compilation will be required if you want to unit test your library, but it could be a subset of the real configuration that Actively Compiling your library for distribution would require.
It removes the need to think about distributing CSS because each Component Consumer (Applications) will compile raw CSS modules or SASS or LESS into a single final bundle that will include your Passively Compiled dependency styles.
Same goes for other assets such as fonts and images. Since Applications are treating
dependencies as sources then all these things will be covered by whatever
strategy the Applicationâs Webpack configuration uses, like file-loader
, etc.
Providing a Theming API is also pretty simple as long as you have SASS or LESS core variables that define your theme and from which the rest of the libraryâs styles depend on and as long as the Component Consumers support these type of preprocessing.
Disadvantages#
You must either specify or follow a specification of what Webpack loaders, loader configuration, CSS configuration, Babel configuration, etc. your library requires in order for Apps to compile it successfully. You could also construct some sort of Webpack composition hook but that can get really complicated really soon without the proper care. An obvious problem with config composition is conflicts with a given loader configuration, such as Babel, where the App uses certain configurations and the library uses others. In general I would say that the library and the consumer App cannot have too different bundling configurations if they want to work properly together, which means of course tight coupling.
You have less encouragement to create a library public API and
simply let Component Consumers use any of your library files
directly i.e. import something from 'A/src/some/deep/nested/file.js'
.
This will make even harder to migrate your library to an Actively Compiled strategy down the road because Component Consumer Apps will be tightly coupled to your libraryâs internal file structure and providing that same per-file API in Webpack is not trivial and has caveats (sharing code and such).
By far, the biggest disadvantage with Passive compilation is that it will infect everything that it touches. If you are creating a new package that is Actively compiled but it uses some of the Passively compiled packages then BINGO, you need to customize your package build to compile those passively compiled packages and you end up in a very similar situation than simply making your new package Passively compiled. Which locks you down into this paradigm and all its flaws.
I have been able to partially solve this situation by making those
Passively compiled dependencies be peerDependencies
, but you still need to
handle them somehow in a test environment (probably by mocking them entirely,
Jest does make this fairly easy)
and if you have any sort of playground or documentation page where
you display your component then BINGO again, you need to compile
Passively compiled dependencies to make it work.
If this seems crazy to you I can already tell you that I have experienced this in my day to day Job and is super frustrating.
As Component Consumers#
Advantages#
It is fairly easy to setup with some tweaks to the inclusion paths in your Webpack config.
Theming is easy because you just need to overwrite some SASS or LESS variables and your build system will do the rest.
Disadvantages#
You cannot use Webpack abstractions such as Create React App because you always need to customize the sources you include, and since this is a rather rare use case, most abstractions wonât and probably should not provide this functionality.
Active Compilation#
As Component Authors#
You actually need to write a full Webpack configuration to generate the bundle, but it is really not that bad (more on this later).
Alternatively, you can use already made solutions such as create-react-library et al.
You will need to make decisions about how to package and distribute your code which is exactly the main point of this post.
As Component Consumers#
You donât need to customize your Webpack config to use the library, this results in simpler configuration. You can also use standard pre-made solutions such as create-react-app
Theming will depend exclusively on what the Component Author sets up to be the Theming API. If a Component Author use something like CSS-in-JS then the experience will be pretty good. Material-UI React uses this API.
If the Component Author provides a build to compile custom Themes out of band (outside your Apps build system) then it will be less optimal in my opinion. Ant Design and React Bootstrap use this API.