React without bundlers, compilers and package managers
TL;DR - there are ways to make React
applications without build systems.
Scroll to the bottom of this post for a example of such an application.
Modern frontend development is a little crazy.
I’ve recently had a chance to peruse the world of modern frontend development,
spending some time getting acquainted with React
and its functional approach
to managing UIs.
Although it had been years since my previous foray into serious frontend coding, I was not prepared for how complicated frontend development has become.
Back then I used to work on single-page applications built with libraries like
underscore
, backbone
and a templating engine such as doT
. The concept of
modular JavaScript was a relatively new, hot topic and tools such as
require.js
were starting to bring the concept of bundling to the masses.
Hip colleagues had just started transitioning to lodash
, bower
had recently
appeared on the horizon and we were all beginning to see some early indications
of what a post-jQuery
world would look like. Ext.js
was making strides in
the corporate world.
From a methodology perspective, we would generally build applications
from the ground up, often starting from a single .html
file and introducing
new dependencies as needed in order to sustain the growing complexity of our
products. Dependencies were generally hand-picked and well-understood in both
their scope and inner workings. Build systems were rarely used and almost
solely for production releases.
What a stark contrast to today’s approach!
Most current development workflows require, at the very least, one or more of the following:
- a package manager (
NPM
,Yarn
); - a compiler / transpiler (
Babel
,node-scss
); - a bundler (
Webpack
,Rollup
).
Developing competence in each of these tools is, in itself, a non-trivial
effort. In fact, even the official React tutorial
doesn’t teach developers how to manage their own development environment
but merely suggest the use of an additional tool, create-react-app
, to
automate such management.
Imagine my surprise when I found out that, as of today, bootstrapping a project
with create-react-app
introduces 4211
dependencies.
Four-thousand-two-hundred-eleven! And this is before adding any
project-related dependency.
This is insane, if only for the long-term stability of the resulting codebase. Furthermore, it’s even more insane considering that there are alternatives. Let’s see:
For package management, a CDN such as unpkg.com
makes it trivial to
incorporate any package published on NPM
using <script>
tags. If
referencing an external CDN is not an option, source files can simply be
shipped as a part of the application.
For bundling and minification, the size of an application and/or the number of HTTP requests made by the browser, especially considering caching, are often not as critical as one might think. They are in some contexts, of course, but not in every context.
For transpilation of ES6+
code into ES5
, I would argue that what cannot
be supported using polyfills should not be used until the relevant browsers are
eliminated from the list of supported browsers that a project needs to target.
For compilation of JSX
into JavaScript
, there are alternatives to JSX
that support compilation at run time at the cost of slightly less efficient
template rendering.
Whether each of these concerns would be better addressed by introducing a build system, adding a new step to an existing build system or adopting a different solution should always be a matter of discussion on a case-by-case basis. Defaulting to a complete, all-encompassing build system just because that is how a given framework is normally used seems myopic at best and irresponsible at worst, no matter how good such framework and/or build system might be.
All of this said, I personally find React
to be a delightful framework to
work with. I appreciate its focus on a more functional approach to UI
management and I appreciate how the community has come together to produce and
share so many reusable components. Whereas I’m not sure that React
is here to
stay for good - diffing and reconciling are expensive operations, after all -
it has definitely left an impression that will shape future frameworks.
So, can we use React
without a build system? Yes, absolutely!
What follows is a simple React
/ Redux
application that just works. No
build system, just pure, client-side JavaScript. Enjoy!
<!doctype html>
<html>
<head>
</head>
<body>
<div id="app"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/redux@4.0.1/dist/redux.js"></script>
<script crossorigin src="https://unpkg.com/react-redux@5.0.6/dist/react-redux.js"></script>
<script crossorigin src="https://unpkg.com/htm@2.1.1/dist/htm.js"></script>
<script>
/**
* Bind `htm` to `React.createElement()`
*
* Since htm is a generic library, we need to tell it what to "compile"
* our templates to. The target should be a function of the form
* `h(type, props, ...children)` and can return anything.
*
* @see https://github.com/developit/htm
*/
const html = htm.bind(React.createElement);
/*
* ======================================================================
* Store
* ======================================================================
*/
/**
* Initial state
*/
const initialState = {
counter: 0,
};
/**
* Reducer
*/
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_ONE':
return { ...state, counter: state.counter + 1 };
default:
return state;
}
};
/**
* Redux store
*/
const store = Redux.createStore(rootReducer);
/**
* Action creator
*/
const addOne = () => ({ type: 'ADD_ONE' });
/*
* ======================================================================
* State - props connectors
* ======================================================================
*/
const mapStateToProps = (state) => {
return { counter: state.counter };
};
const mapDispatchToProps = {
addOne,
};
/**
* Wraps a dumb component and populates the `counter` and the `addOne`
* props with the state's `counter` property and the `addOne` action
* creator.
*/
const connectWithCounter = ReactRedux.connect(
mapStateToProps,
mapDispatchToProps,
);
/*
* ======================================================================
* Store - app connectors
* ======================================================================
*/
/**
* Wraps a component so that all of its children using ReactRedux.connect()
* can access the store.
*/
const wrapWithStoreProvider = (Component) => {
return (props) => {
return html`
<${ReactRedux.Provider} store=${store}>
<${Component} ...${props} />
<//>
`;
};
};
/*
* ======================================================================
* Components
* ======================================================================
*/
const Controls = connectWithCounter((props) => {
const { counter, addOne } = props;
return html`<p>${counter} <button onClick=${addOne}>Add one</button></p>`;
});
const App = wrapWithStoreProvider(() => {
return html`
<${Controls} />
`;
});
ReactDOM.render(App(), document.getElementById('app'));
</script>
</body>
</html>