Cooking a Strudel with React & Mobx
Setting up a React & MobX project is not different from cooking a Strudel.
I’m more confident with savoury recipes, Lasagna and Tomato-based pastas are in my confort zone. But this time I had some spare apples and I decided to prepare a Strudel.
If you don’t know what a Strudel is:
Before starting with ingredients, here you can find the whole recipe step by step maxgallo/strude-recipe
!
Ingredients
Pastry, apples, raisins, cinnamon, lemon zest, sugar and breadcrumbs.
Every recipe starts with the ingredients. It doesn’t matter if your grandma gave you the secret book of cooking or if you’re following a youtube tutorial, picking them is the first step.
These are the ingredients that I used to cook my recipe/project:
- React 200gr
- Webpack & Babel to taste
- CSS Modules 1tsp
- MobX 100gr
- Hot Reloading 50gr
- AVA 1⁄2 cup
- Enzyme 3 tbsp
1. Preparation
You’ll need a large bowl, a frying pan and a baking tin. Preheat the oven to 190° (170° if is gas).
To setup the project, I’ll use yarn
and git
. As in every recipe, feel free to use your own favourite tools.
$ mkdir strudel-recipe
$ cd strudel-recipe
$ git init
$ yarn init
2. Mix React with Webpack & Babel in a Bowl
Peel and slice apples, then mix them with the cinnamon, sugar, lemon zest, and raisins in the bowl.
You probably already setup a React project many times and you’re brave enough to do it by your self. ( if you’re not, I’d recommend create-react-app).
The starting point is always thesrc/index.js
file
import React from 'react';
import { render } from 'react-dom';import App from './components/App';render(
<App />,
document.getElementById('app')
);
Unfortunately a lot of browsers are not able yet to understand import
and export
statements, and, more important, it’s unlikely that they will ever support <JSX>
syntax.
Webpack and Babel to the rescue.
$ yarn add --dev webpack webpack-dev-server
$ yarn add --dev babel-core babel-loader babel-preset-react babel-preset-env
Why do I need five packages?
You need webpack
because you want to bundle all your JavaScript files. You need webpack-dev-server
because you’re lazy and you don’t want to write your own Express server that restarts every time you change a file while developing. You need babel-core
because you want to transpile your code. You need babel-preset-env
because you want to write ES6 / ES7 cool stuff without thinking if browser X support feature Y. You need babel-loader
because Webpack needs to talk with Babel in some way. You need babel-reset-react
because you want to translate your JSX into something that the browsers understand.
At this stage this is the .babelrc
file
{
"presets": [
"react",
"env"
]
}
and webpack.config.js
looks like this
const path = require("path");module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "public")
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
}
}
]
}
};
Here Babel is starting from ./src/index.js
and it’s resolving all the dependencies, creating an output file atpublic/bundle.js
. Every time it finds a new module, it will use an appropriate loader to handle that kind of file (read more here). In the configuration above we’re telling Webpack that every time it finds a .js
module, it needs to ask babel-loader
how to handle it.
We just need to define a script to run our project in package.json
"start": "webpack-dev-server --content-base public"
3. Fry some CSS Modules
In a clean frying pan, fry the breadcrumbs until golden-brown.
Thanks to CSS Modules we can have scoped selectors (read more here). In React this means that we can import a CSS file from JavaScript and use the selectors defined in the CSS file, directly in the React component.
This is the App.js
component
import React, { Component } from 'react';import style from './app.css';class App extends Component {
render() {
return (
<div className={style.appTitle}>
Strudel Recipe
</div>
)
}
}export default App
And this is the app.css
file
.app-title {
font-size: 20px;
color: coral;
}
In order to achieve that we need two Webpack loaders that are going to take care of the CSS modules: yarn add --dev css-loader style-loader
. This is the new loader to add to the webpack config
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[local]__[hash:base64:5]',
camelCase: true,
},
},
]
}
4. Roll out AVA and place some Enzyme on top
Roll out the puff pastry. Place the filling down the centre. If there is too much fulling, don’t use it all.
To test our very minimal react application, we’re going to add a few libraries
$ yarn add --dev ava enzyme jsdom react-test-renderer
AVA is the Futuristic Test Runner, Enzyme is a collection of testing utilities for React. JSDom and react-test-renderer
are needed by Enzyme.
This is how our App.test.js
file looks like
import test from 'ava';
import React from 'react';
import { shallow } from 'enzyme';import App from './App';test('Renders a div with className and text', t => {
const wrapper = shallow(<App />);
t.true(wrapper.contains(
<div className="appTitle">Strudel Recipe</div>
));
});
This test is written in ES6 syntax and AVA is doing the transpilation for us, but we need to be careful.
4.1 AVA test files and source files
Test files in AVA are all the files *.test.js
. Inside a test file you can import other files (e.g. import App from './App'
). These imported files are called Source Files.
AVA takes care of how to transpile Test files and leave to you how to transpile Source files. There are multiple ways to configure Babel in both cases, you can read more here.
The way AVA uses Source files is a very sensitive topic in the AVA community and there are multiple proposal to improve the experience and the performance in the future.
4.2 Analysis of the AVA configuration
This is our AVA configuration inside package.json
"ava": {
"babel": "inherit",
"require": [
"babel-register",
"./test/helpers/setup-test-env.js",
"mock-css-modules"
]
},
The first row “babel”: “inherit”
means that we’re going to inherit the default Babel configuration to transpile AVA Test files. In our case that configuration is inside .babelrc
. The Source files are always transpiled with the default babel configuration.
babel-register
is an override of node require
function to be able to use babel
on the fly. Thanks to that we’re able to transpile on the fly the Source files of our AVA tests (don’t forget to yarn add --dev babel-register
).
./test/helpers/setup-test-env.js
is just a standard way to properly initialise Enzyme with JSDOM.
4.2 CSS Modules with AVA
When using CSS Modules with Webpack, css-loader
is taking care of generating a unique hash name for each CSS selector. In the tests we’re not using Webpack (you can if you want), but also we’re not really worried about uniqueness of the CSS selectors.
To solve this problem I found a small library calledmock-css-modules
that overrides the require
function behaviour for .css
files. Every time you request a CSS module, it returns you the same key you requested. In a practical example, we can test something like this
<div className={style.appTitle}>
Strudel Recipe
</div>
In this way
t.true(wrapper.contains(
<div className="appTitle">Strudel Recipe</div>
));
5. Fold the pastry with MobX
Fold the top flap and lower flap of pastry over the filling and brush with egg yolk. Seal the ends equally well with the egg-milk mixture. Turn the strudel over so that the nice side is facing up.
I’m about to add MobX and MobX State Tree to this project and to do that I need to import three dependencies.
yarn add mobx mobx-react mobx-state-tree
The first one mobx
is pure MobX, the second one mobx-react
is set of utilities for using MobX in the React world. The third one is a MobX-powered state container that helps you thinking how to manage the state with MobX.
I won’t focus on the code of the store, the updated index.js
and the Recipe.js
component, because this article is about setting up the project.
I want to point out that by using MobX, I just introduced the need to use Decorators (e.g. @inject
@observer
) and Class Properties (e.g. anyIngredients
) in my JavaScript code
import React, { Component } from 'react';
import { computed } from 'mobx';
import { inject, observer } from 'mobx-react';@inject('recipeStore') @observer
class Recipe extends Component {
@computed get anyIngredients() {
return this.props.recipeStore.ingredients.length
} render() {
return (
<div>
...more things
Both are not a standard yet but you can still use them with a couple of Babel plugins
yarn add --dev babel-plugin-transform-decorators-legacy babel-plugin-transform-class-properties
and we should update the .babelrc
in this way
{
"presets": [
"react",
"env"
],
"plugins": [
"transform-decorators-legacy",
"transform-class-properties"
]
}
Is my test still working? Yes, because the babel configuration of the Source files depends on my .babelrc
file that I just updated.
I also added a new test file for the Recipe
component, to show you how to briefly test components that use Provider
to inject stores
.
6. Bake the Strudel with Hot Reloading
Bake the Strudel for 25/30 minutes.
There are a two players involved: webpack-dev-server
and react-hot-loader
here: in the first one we’re enabling module replacement (framework agnostic), in the second we’re apply that feature for the React world. Let’s install the missing dependency with yarn add --dev react-hot-loader@next
(we’re using @next
version of it).
Now we can create a new script in package.json
for the Hot Reloading
"start:hot": "webpack-dev-server --config webpack.config.hot.js --hot --content-base public"
From the previous one we simply added --hot
and we’re loading a different Webpack configuration, that extends the first one
const webpack = require('webpack');
const webpackConfig = require('./webpack.config');webpackConfig.plugins = [
new webpack.NamedModulesPlugin()
];webpackConfig.entry = [
'react-hot-loader/patch',
webpackConfig.entry
];module.exports = webpackConfig;
We’re adding a plugin NamedModulesPlugin
that helps us recognising the reloaded modules with Hot Reloading and we’re adding an entry before the current one, so the first thing Webpack starts to bundle it’s always react-hot-loader
.
Last piece of the puzzle is updating theindex.js
At line 25 you can see that when module.hot
is enabled we’re adding a listener to the module.hot.accept
method. Every time something changes, this listener will be triggered causing the re-rendering of part of the tree.
7. Personal Touch: Three Shaking
I bought some Vanilla Ice Cream to eat the Strudel With, I love the hot-cald feeling in desserts.
With Webpack 2 we got Three Shaking for free out of the box, but is it always enabled? The answer is no, and the main problem is that Babel right now is transforming all our ES6
modules into CommonJS
modules.
Javascript bundlers such as webpack and Rollup can only perform tree-shaking on modules that have a static structure. If a module is static, then the bundler can determine its structure at build time, safely removing code that isn’t being imported anywhere.
CommonJS
modules do not have a static structure. Because of this, webpack won’t be able to tree-shake unused code from the final bundle.
So we just need to tell Babel to stop transforming our modules, that’s easy
{
"presets": [
["env", {
"modules": false
}],
"react"
],
"plugins": [
"transform-decorators-legacy",
"transform-class-properties"
]
}
The only problem is that, we just broke our tests
This is because we’re not transforming ES6
modules anymore in Babel, but we still need to do that for our AVA tests. To solve this problem you can provide different Babel configurations based on your process.env.NODE_ENV
.
So our test
script inside package.json
is now: (to be windows compatible you need to use something like cross-env
)
"test": "NODE_ENV=test ava"
And our new .babelrc
, thanks to the this option, contains two different setups that Babel choose based on the NODE_ENV
variable
Now all our tests could run safely with ES6
transpiled by Babel and our start:hot
task could get the benefit of the Three Shaking.
8. Enjoy ! (Buon Appetito)
I know it doesn’t look like the one in the picture at the beginning, but it was good enough for a first attempt and I learn a couple of things for the next time!