Webpack: Managing Tree Shaking

PROBLEM

Sometimes, Webpack’s tree-shaking may accidentally eliminate imported code from import statements.

For example, we may have a root JS file that imports a CSS file:-

import React from 'react';
import ReactDOM from 'react-dom';
import '../css/index.css';

...

… and for some reason, the CSS file will never appear in the final bundle even if the Webpack config contains proper rules to handle CSS files.

SOLUTION

To prevent Webpack from removing any “unreferenced” code (ex: global CSS files, JS polyfills, etc), list these side effects under package.json, for example:-

{
  "name": "front-end-stack",
  "sideEffects": [
    "*.css"
  ]
}

React: Debugging Layout Thrashing

PROBLEM

When the React app grows larger over time, it is highly likely to run into situations where the component keeps re-rendering for no apparent reason.

There are many reasons why this is happening… to name a few…. parent component re-renders, this.props or this.state has changed, etc.

The key is to quickly find out what causes the component to re-render constantly.

SOLUTION

The simplest solution, in my opinion, is to paste the following block into the troublesome component:-

componentDidUpdate(prevProps, prevState) {
  const debug = (label, currObject = {}, prevObject = {}) => {
    Object.entries(currObject).forEach(([key, currValue]) => {
      if (prevObject[key] !== currValue) {
        console.log(`[DEBUG] ${label} has changed: `, key);
        console.log('[DEBUG] - BEFORE : ', prevObject[key]);
        console.log('[DEBUG] -  AFTER : ', currValue);
      }
    });
  };

  debug('Prop', this.props, prevProps);
  debug('State', this.state, prevState);
}

Adding this componentDidUpdate(..) lifecycle allows us to quickly find out which property or state has changed.

When running the app, the console may display something like this:-

Console Log

The key is to look for identical object or array displayed in both “BEFORE” and “AFTER” statements. This shows that while the values look similar, they fail on strict equality check (‘===’), which causes the component to re-render.

React + Recompose: Calling Multiple HOC Wrappers

PROBLEM

Sometimes, wrapping a React component with multiple High Order Components (HOC) can get rather unwieldy and unreadable.

For example:-

import React from 'react';
import { withRouter } from 'react-router-dom';
import { withStyles } from 'material-ui/styles';
import withWidth from 'material-ui/utils/withWidth';

class MyComponent extends React.PureComponent {
	// ...
}

export default withRouter(withStyles(styles)(withWidth()(MyComponent)));

SOLUTION

To fix this, we can leverage recompose library.

Now, we can rewrite the above example like this:-

import React from 'react';
import { withRouter } from 'react-router-dom';
import { withStyles } from 'material-ui/styles';
import withWidth from 'material-ui/utils/withWidth';
import compose from 'recompose/compose';

class MyComponent extends React.PureComponent {
	// ...
}

export default compose(
  withRouter,
  withStyles(styles),
  withWidth(),
)(MyComponent);

Keep in mind, the HOC order defined in compose(..) is important.

Webpack + ESLint: Automatically Fix ESLint Errors

PROBLEM

Given the following webpack.config.js

module.exports = {
  ...
  module: {
    rules: [
      {
        enforce: 'pre',
        test: /\.js?$/,
        loader: 'eslint-loader',
        exclude: /node_modules/,
      },
	  ...
    ],
  },
  ...
};

When running any Webpack command, ESLint may find violations and halt the entire process with the following error message:-

/path/to/front-end-stack/src/js/components/home/Home.js
  43:11  error  Expected indentation of 6 space characters but found 10  react/jsx-indent
  44:14  error  Expected indentation of 6 space characters but found 13  react/jsx-indent

x 2 problems (2 errors, 0 warnings)
  2 errors, 0 warnings potentially fixable with the `--fix` option.

SOLUTION

Certain errors (ex: trailing commas, wrong indentation, extra semicolon) are easily fixable.

There’s no need to halt the process and wait for developers to fix these obvious errors.

To configure ESLint to automatically fix these “soft” errors, add the following options block to the above rule:-

module.exports = {
  ...
  module: {
    rules: [
      {
        enforce: 'pre',
        test: /\.js?$/,
        loader: 'eslint-loader',
        exclude: /node_modules/,
        options: {
          fix: true,
        },		
      },
	  ...
    ],
  },
  ...
};

If you are using any VCS, remember to commit any file changes.

ES6 + Mocha + Sinon: Mocking Imported Dependency

PROBLEM

Let’s assume we have the following 2 files:-

apis.js

import fetch from 'isomorphic-fetch';

export const logout = () => (
  fetch('/logout')
    .then(resp => resp.json())
    .catch(err => err)
);

service.js

import { logout } from './apis';

export const kickUserOut = activeSession => (
  activeSession ? logout() : undefined
);

Let’s assume we want to test the logic in service.js without using nock to mock the HTTP call in apis.js.

While proxyquireify allows us to mock out the apis.js dependency in service.js, sometimes it is a little more complicated than needed.

SOLUTION

A simpler approach is to use sinon to stub out logout() defined in apis.js.

service-spec.js

import { beforeEach, afterEach, describe, it } from 'mocha';
import { expect } from 'chai';
import sinon from 'sinon';
import { kickUserOut } from './service';

// import everything as an object
import * as apis from './apis';

describe('service => kickUserOut', () => {
  let logoutStub;

  // before running each test, stub out `logout()`
  beforeEach(() => {
    logoutStub = sinon.stub(apis, 'logout').returns('success');
  });

  // after running each test, restore to the original method to
  // prevent "TypeError: Attempted to wrap logout which is already wrapped"
  // error when executing subsequent specs.
  afterEach(() => {
    apis.logout.restore();
  });

  it('given active session, should invoke logout API', () => {
    expect(kickUserOut(true)).to.deep.equal('success');
    expect(logoutStub.calledOnce).to.equal(true);
  });

  it('given expired session, should not invoke logout API', () => {
    expect(kickUserOut(false)).to.equal(undefined);
    expect(logoutStub.calledOnce).to.equal(false);
  });
});