I've used a number of frameworks and build tools over the last two years. I can't lie: React + Webpack + Bacon is the real deal. Let me show you why.
The React docs completely miss the point. React is cool because it makes it easy to think about code, and natural to separate out components. So basically front-end nirvana.
To summarize the docs your application is a component. Each page is a component. The menu is a component, and each menu item is ... can you guess ... a component! Everything is a component. Simple. A component is one encapsulated logical unit. It keeps track of state, renders itself with a pure function, and cleans up after itself with lifecycle hooks.
The key mental shift is never change (v) the DOM. Instead set component state and trust that the render function will update the DOM to reflect it was changed (adj). What does this look like?
onClick={ function } -> setState({ hidden: true })
render -> state.hidden? -> element
You get to separate logic and display. When the user clicks the button, what fact changes? The menu should be hidden. Ok, now given the menu should be hidden, how can the view reflect that fact? No menu element in output.
Read more about React: https://facebook.github.io/react/index.html
Why walk when you can fly? Webpack is pretty flawless when it comes to parsing and transforming files. Here's an example project structure.
project
├── README.md
├── app
│ └── index.js
├── dist
├── public
│ └── index.html
├── node_modules
├── webpack.config.js
└── package.json
Install webpack and the clean plugin to remove old files.
$ npm install --save-dev webpack clean-webpack-plugin
Webpack config file.
// webpack.config.js
module.exports = {
// The main app file
entry: [
__dirname + '/app/index.js',
],
// Where should the bundle go?
output: {
path: __dirname + '/dist',
filename: 'bundle.js',
},
// Where are the modules? For npm its node_modules
resolve: {
modulesDirectories: [ 'node_modules' ],
},
// If you want ES6 / ES7 syntax sugar and modern web APIs
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' },
],
},
// We don't want old unused files lying around
plugins: [
new (require('clean-webpack-plugin'))([ 'dist' ]),
],
}
Webpack works with loaders. You can register them based on file extension in the module
part of the config (like we did for all .js
files and babel-loader
) or prefix require statements.
$ npm install --save-dev babel-loader jsx-loader file-loader
When you require react components, use the jsx loader so you can put html-lookin stuff in the render function.
var Header = require('jsx!./components/header.js)
Alright build and watch.
$ webpack --watch
Read more about Webpack: http://webpack.github.io/docs/
It took me a few minutes to search for how to set this up, so here you go.
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<div id="view"></div>
<script src="/bundle.js"></script>
</body>
</html>
The main js file requires the others.
// app/index.js
// Copy index.html to dist
require('file?name=index.html!../public/index.html')
var React = require('react')
var ApplicationView = React.createClass({
render: function () {
return <p>Hello world</p>
},
})
React.render(
<ApplicationView></ApplicationView>,
document.getElementById('view'),
function () {
console.log('application started')
}
)
Ok, a blank canvas. Read on for structure.
Application state is not fixed, it depends on time. The best way to model a value changing over time is with an event stream. What does that look like?
$ npm install --save baconjs
var { Bus } = require('baconjs')
var stream = new Bus()
var defaultValue = 1
var property = stream.asProperty(defaultValue)
property.onValue(function (x) {
console.log('coffee: ' + x)
})
// logs coffee: 1
stream.push(2)
// logs coffee: 2
property.onValue(function (x) {
console.log('tea: ' + x)
})
// logs tea: 2
stream.push(3)
// logs coffee: 3
// logs tea: 3
You push values to an event stream and listen for new values. Value and time. Nice.
Read more about Bacon: http://baconjs.github.io/api.html
For example, let's model the current page as an event stream. Values could be 'HOME'
or 'SIGN_IN'
. I usually put application data streams into the app/stores
folder, but you're an elite coder so do your thing.
// stores/views.js
var viewStream = new Bus()
var currentView = viewStream.asProperty()
exports.navigate = function (view) {
return viewStream.push(view)
}
exports.eventStream = function () {
return currentView.skipDuplicates()
}
Anywhere in the application you can listen for view changes or cause them.
var views = require('./stores/views')
// check auth
if (!user) {
views.navigate('SIGN_IN')
}
// watch for page change
views.eventStream().onValue(function (view) {
console.log('current view is', view)
})
The nice part about an event stream is that it doesn't care where changes come from. For example let's start with the last view from local storage.
var lastPage = localStorage.getItem('lastPage')
viewStream.push(lastPage)
exports.navigate = function (view) {
localStorage.setItem('lastPage', view)
return viewStream.push(view)
}
That's it. How about a full router.
var page = require('page')
page('/', function () {
viewStream.push('HOME')
})
page('/login', function () {
viewStream.push('SIGN_IN')
})
page()
Done. Streams make things easy.
We have values that change over time, now to render stuff. The basic lifecycle is as follows.
componentDidMount
: start listening for new valuescomponentWillUnmount
: stop listening and clean upTo keep things DRY use a mixin to bind reactive properties (streams) to application state.
var KEY = '_baconUnsubscribeFunctions'
var BaconMixin = {
componentWillMount: function () {
this[KEY] = []
},
componentWillUnmount: function () {
this[KEY].forEach(f => f())
},
bindStreamValue: function (name, stream) {
var self = this
var off = stream.onValue(function (value) {
self.setState({ [name]: value })
})
this[KEY].push(off)
},
}
So much boilerplate, right? Oh calm down, we're here now.
var Component = React.createClass({
mixins: [ BaconMixin ],
componentDidMount: function () {
this.bindStreamValue('view', views.eventStream())
},
render: function () {
switch (this.state.view) {
case 'HOME':
return <Home></Home>
case 'SIGN_IN':
return <SignIn></SignIn>
default:
return <NotFound></NotFound>
}
},
})
All you have to do to change and re-render the entire application is push a new value to the views event stream. Hopefully you see the ease in event streams, pure render functions, and components.
I don't really do comments, tweet @aj0strow.