Cooking a Strudel with React & Mobx

Setting up a React & MobX project is not different from cooking a Strudel.

Max Gallo
DAZN Engineering
10 min readJul 31, 2017

--

Basic ingredients for creating 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:

A perfect Strudel

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.

The mix 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.

Breadcrumbs on frying pan

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.

Folding the Strudel

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.

2 tests passed after adding MobX

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.

In the oven 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

Broken 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

Latest .babelrc with conditional configuration

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!

--

--

Max Gallo
DAZN Engineering

Principal Engineer @ DAZN. Addicted to Technology, Design, Music, Motorbikes, Photography and Travels. I use spaces, not tabs.