Choosing Technologies: Lerna, TypeScript and React

Check out the Git repo: https://github.com/alexnitta/woodshed

About a year and a half ago, I transitioned from a job at a mature company to a job at a new startup. For the most part, I’m the only frontend developer at my company, and I essentially started building our web application from scratch. This was exciting and daunting at the same time. Exciting in terms of the freedom to choose technologies that I was interested in working with, and daunting in terms of the possibility that I could make choices I would later regret – or worse, that other developers or users would blame me for.

This post will go into the reasons why I chose particular technologies when starting from scratch, and how I feel about those choices now that I’ve been working with them for a while. I’m hoping to write several posts in this vein.

Choosing Lerna

I was coming from a frontend team of around five in-house engineers, and the main bottleneck in our workflow was how the Git repos were structured. We had over thirty frontend repos, and most tickets required making changes in several of them. This turned merging in a pull request into an exercise that was time-consuming at best, and at worst, prone to merge conflicts and regressions. These problems only got worse when we brought on a team of contractors to increase our velocity. We spent many hours on-boarding them to our esoteric workflows, and more time still on merging in their changes, which typically lived in long-running feature branches that drifted apart from our development branches across our many repos. One of the our oldest tickets, which was always getting pushed aside in favor of fixing more urgent problems, was migrating to a monorepo. We had started doing this with Lerna, and then re-started the effort using plain npm and Git commands, but neither of these projects had succeeded by the time I left.

Not surprisingly, this pain was fresh in my mind when I started from scratch at my new job. Things would be different, I vowed, and I committed myself to building a monorepo from the start with Lerna. There were other options, but Lerna had the most promising community of active users and maintainers, and I was already somewhat familiar with it, so it was an easy choice amongst the available tools.

Choosing TypeScript

I also chose to use TypeScript from the start, without having prior experience with it. This was a choice driven by a desire to write fewer bugs, as well as personal interest in learning a new technology. I had worked with Flow in an earlier job, and TypeScript felt familiar in some ways, but was clearly gaining more traction in the frontend community. More and more new projects were being written in TypeScript, which meant I could take advantage of their typings in my own code. Also, it just seemed to make career sense; I was seeing it mentioned in more job listings over the years.

Choosing React

I’ve become so used to writing code in React that it’s second nature to reach for it as the main frontend library, and build off of that choice. Plus, I already had decided to use two new tools (Lerna and TypeScript), and I needed to make sure I could still ship code at a reasonable pace. Besides, the company had hired me with the expectation that I would leverage my prior React experience – so it made a lot of practical sense.

Lerna, TypeScript and React: benefits

There is a lot to gain from using Lerna, TypeScript and React together. The most obvious benefit is code portability. This is why we split our applications into multiple packages in the first place, and it can be a great advantage when working within a startup which may need to pivot quickly. At my new company, we started out building an Electron app, then later switched to building a web app. Since I had already set aside much of the React components and hooks into packages, I could simply start another package for the web app and import the existing code there.

Using a monorepo makes it easy to enforce code conventions with shared config files for tools like ESLint and Prettier. These tools take time to configure; the good news is that in a monorepo you can maximize the benefits of that time you spent by applying your configs across all of your packages. And Lerna’s CLI gives you an easy way to run package.json scripts in parallel, so you can lint and test your code before submitting a PR and during continuous integration.

Using TypeScript in across your entire frontend codebase is a major win in the ongoing battle against bugs. As the solo developer on this project, I don’t have the luxury of code reviews where someone else will catch my bugs. TypeScript is a solid defense against an entire class of bugs endemic to JavaScript, and since I can have a computer catch those for me, it saves a lot of time I can then spend on more important issues. A monorepo helps with using TypeScript a uniform way, because tsconfig files for each package can be extended from a root tsconfig file, which helps keep your usage of TypeScript more consistent.

Lerna, TypeScript and React: drawbacks

The unpleasant reality is that these tools are not necessarily built to work smoothly together. That’s why there are several new frameworks that promise to simplify frontend development workflows, such as RedwoodJS and Blitz. However, these frameworks make some choices that may not be appropriate for your project. If that’s the case, keep reading – there are ways to make it all work; it just took some good old learning by experience to uncover the gotchas.

tsc won’t save you, but Webpack and Babel can help

The main thing to remember when building a monorepo with TypeScript is that the TypeScript CLI (or tsc) is mostly written for people developing a library as a single npm package. The assumption is that you will want to write your library in TypeScript, then use tsc to compile it to JavaScript, as well as sourcemaps and .d.ts type definitions. That way, users of your library can import it to either a JavaScript project or a TypeScript project. Most of the examples in the TypeScript docs point toward this pattern, but it’s not what we want to do in a monorepo situation. That’s because it’s slow and cumbersome. Imagine you are working across five packages, which exist in some manner of dependency tree. To use tsc to compile your code, you would need to run it in --watch mode in every package, so that it can listen for changes in the TypeScript files and transpile them to .js and .d.ts files for other packages to import. Even if you use Lerna to run these commands for you in a single package.json script, each of those running tsc commands will eat up some resources and take time to do its part of the work.

A better approach is to use the top-level application to transpile the entire codebase. By top-level, I mean the package that exists at the root of the dependency tree. In a typical web app built with Create React App, this means that the Webpack config needs to be modified so that Babel knows how to handle TypeScript appropriately across all the monorepo packages. This is where things get tricky. There are several tools out there for modifying your Webpack / Babel config without ejecting from Create React App. I found a combination that works, but not without a lot of tinkering.

Beware the silent any

It’s commonly accepted as best practice to avoid using any types in a TypeScript project. Let’s imagine you are depending on tsc and eslint to enforce this practice. So far, so good. Eventually, you might discover that you can declare global type definitions and use them across your monorepo by adding them to the compilerOptions.typeRoots section of your tsconfig.json files. Let’s also imagine you write a global type definition that uses type defined in a library, such as React. If you are more experienced with TypeScript, you might know how to write an inline import statement to bring that type into your global definition. But, if you are relatively new to TypeScript, as I was, you might just refer to that type without importing it. In this case – for some crazy reason – TypeScript will just happily accept your declaration that uses a type that it has not actually imported, and silently treat it as an any type, despite your rules that don’t allow anys. And that can create some nasty hidden bugs, as I discovered. Lesson learned: understand how the TypeScript import keyword works, and use it in your global type definitions.

See an example boilerplate

If you search for examples of Lerna / TypeScript / Create React App projects, you will find lots of unmaintained boilerplates. I have probably tried my hand at leveraging most of them. In the end, I decided to share my own version, which replicates the methods that are working for me in production. I hope you find it useful: https://github.com/alexnitta/woodshed

Overall impression

My overall take on the choice to use Lerna, TypeScript and React together is that the learning curve was worth it. In terms of my own growth, I feel I have a solid understanding of these tools and how to leverage them together in a way that speeds up common workflows. I now have a strong foundation in TypeScript and can bring that to any project. Even if I’m working in a JavaScript project, the coding patterns that TypeScript enforces will be helpful in avoiding common problems. And solving issues by configuring Webpack and Babel is always a good reminder of how these core tools work, such that I am less afraid to tweak a setting when it might serve me.

Where this choice really shines, though, is when working on a team. I mentioned that I’m usually the only frontend developer on my team, but from time to time, other people do contribute. When they do, they only have one repo to git clone and they only have to submit one PR when it’s time to merge in their changes. When that happens, I take a moment to breath deeply and appreciate how much time I’m not spending on the process.

Understanding reselect and re-reselect

TL; DR

If you’re pretty comfortable with what Redux selection is and how it works, remember these tips:

Keep simple selectors simple

For simple use cases where we want to do a straight property lookup, all we need to do in mapStateToProps is drill down to the property we need. For example:

const mapStateToProps = (state, ownProps) => ({
    locationChanging: state.grid.locationChanging,
})

Property lookups on objects are fast. They allow us to select the data we care about, and they don’t require any further optimization.

If you have a unique ID, you probably should use re-reselect

When you have access to a unique ID that you’re using to calculate some data, or display a UI component in a list of items by ID, you would probably benefit from using re-reselect. This is because you have an easy way to get a dependable cache key that refers to the same entity over time.

For more details on why this is true, see the reselect docs.

You don’t have to use a memoized selector with redux-saga’s select

When writing sagas with redux-saga that interact with the Redux store, you will commonly use the select effect. It performs the same task as any other selector: targeting a specific piece of the Redux state that we want to get updates on. The select effect accepts a selector function and any number of optional (...args) (see the docs. Essentially the same ideas apply here as with mapStateToProps, except that the selector does not necessarily get ownProps as its second argument.

If you are doing a straight property lookup, you do not need to use a memoized selector. You can pass a regular function as the selector argument:

const isGridReady = yield select(state => state.grid.isGridReady)

In many cases, you don’t need anything more complex than this.


Defining ‘selection’

The core issue we’re dealing with is how to drill down to data that we need to get from the Redux state tree. Selection in this context is the act of targeting a specific piece of the Redux state that we want to get updates on. If our selector works correctly, the component that uses it will update when – and only when – there is a state update that it needs.

Connecting to the store with mapStateToProps

The connect function provided by react-redux gives us access to the Redux store with its first argument, mapStateToProps. mapStateToProps is a function that takes in two arguments, state and ownProps. state is the current state of the Redux store. ownProps are the props that are passed in to the instance of the component. mapStateToProps returns an object of props that is merged with the passed-in props to provide the final props object to the component.

Computing derived data with selectors

There are some cases when we need to do some computation to derive the data we need, based on what is in state and ownProps.

Consider this example:

const ChosenItemsList = props => {
    // ... more component code
};

const mapStateToProps = (state, ownProps) => {

    // `state.chosenItems` is a list of items that have been clicked on by the user.
    const { chosenItems } = state.chosenItems;

    // `itemType` is a given description, such as 'image' or 'zipFile'
    const { itemType } = ownProps;

    const chosenItemsByType =
        chosenItems.filter(chosenItem => chosenItem.type === itemType);

    return { chosenItemsByType };
};

export default connect(mapStateToProps)(ChosenItemsList);

The selection logic requires us to iterate through the state.chosenItems array. This is a potentially expensive (linear time complexity) operation when state.chosenItems contains a lot of elements.

Memoizing selection logic

Rather than putting that expensive array.filter() in mapStateToProps, we can use a selector to memoize the operation. Reselect gives us the createSelector function that does exactly this.

createSelector takes any number of function arguments. The first to n-1 functions are used as “input selectors”, and the nth function is turned into a memoized selector.
– input selectors do not transform the data they select; they merely select the inputs for the memoized selector.
– the memoized selector takes any number of selected inputs and computes the derived data.

const ChosenItemsList = props => {
    // ... more component code
};

const chosenItemsSelector = state => state.chosenItems;
const itemTypeSelector = (state, ownProps) => ownProps.itemType;

const chosenItemsByTypeSelector = createSelector(
    chosenItemsSelector,
    itemTypeSelector,
    (chosenItems, itemType) => {
        // This function will be memoized; it will only be called when the arguments change.
        // If the arguments have not changed, the result from the last time it was called will
        // be returned.
        const chosenItemsByType =
            chosenItems.filter(chosenItem => chosenItem.type === itemType);

        return { chosenItemsByType };
    });

const mapStateToProps = (state, ownProps) => ({
    chosenItemsByType: chosenItemsByTypeSelector(state, ownProps),
});

export default connect(mapStateToProps)(ChosenItemsList);

We’re able to avoid that expensive array iteration if our selector is used when neither chosenItems nor itemType have changed since the last time it was called.

Warnings about reselect

=== diffs

Reselect checks for changes using reference equality (===). This means that you have to respect the Redux expectation that your store’s properties are immutable. If your reducers modify some data within the state.items array without returning a new object reference, the selector will not detect a new argument, and you will receive a cached item when you expect to receive the newest one.

Cache size of 1

Per the reselect docs:

Selectors created with createSelector have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector.

This means that your expensive calculations will always run when an input changes. This applies across all call sites for a given selector. Let’s assume you have two instances of <ChosenItemsList>, each with different itemType props.

<div>
    <MainContent>
        <ChosenItemsList itemType={'image'} />
    </MainContent>
    <Sidebar>
        <ChosenItemsList itemType={'zipFile'} />
    </Sidebar>
</div>

Let’s assume that <MainContent> and <Sidebar> are receiving props updates at different points in time, causing us to alternate between rendering the <ChosenItemsList> components. If this happens, the memoized function will always run, because it is receiving a different itemType argument. This is explained in the reselect docs.

Using re-reselect

How can we avoid this? By changing our caching logic. Re-reselect give us the ability to set up more than one cache for our selector. Its main export is a function called createCachedSelector, which is actually a wrapper around createSelector from reselect. It creates a separate cache for each cache key you create. The first portion of the function signature for createCachedSelector is the same as for createSelector. However, it adds a final argument: a keySelector that returns the cache key.

The keySelector function receives the same arguments as the final memoized selector – in the example below, it gets chosenItems and itemType.

const ChosenItemsList = props => {
    // ... more component code
};

// `state.chosenItems` is a list of items that have been clicked on by the user.
const chosenItemsSelector = state => state.chosenItems;

// `itemType` is a given description, such as 'image' or 'zipFile'
const itemTypeSelector = (state, ownProps) => ownProps.itemType;

const chosenItemsByTypeSelector = createCachedSelector(
    chosenItemsSelector,
    itemTypeSelector,
    (chosenItems, itemType) => {
        // A copy of this selector function will be memoized for each different `itemType`.
        const chosenItemsByType =
            chosenItems.filter(chosenItem => chosenItem.type === itemType);

        return { chosenItemsByType };
    })
    (
        // This is the `keySelector` function. It returns a number or string that is used as the
        // cache key for each copy of the memoized selector function.
        (chosenItems, itemType) => itemType;
    );

const mapStateToProps = (state, ownProps) => ({
    chosenItemsByType: chosenItemsByTypeSelector(state, ownProps),
});

export default connect(mapStateToProps)(ChosenItemsList);

This means that all instances of <ChosenItemsList> that have the same itemType will share the same selector cache. In the situation above where we have two <ChosenItemsList>s with different itemType props, we can alternate between rendering the components and be sure that the memoized selector will only run when chosenItems has changed since the last selector call.

Using re-re-reselect

And if you’ve made it this far, you might just want to check out re-re-reselect. Or not, your choice 🙂

Testing state mutations with Enzyme.js and wrapper.update()

When testing React components, I often reach for Enzyme.js, a library of testing utilities for React components developed by the team at Airbnb. It’s become a standard tool among React developers. Recently, I was testing a component that used state to determine the className given to a child component. Here’s the general idea in code:

import React from 'react';

export default class FormWithCollapsableRows extends Component {

    constructor(props: Props) {
        this.state = {
            hidden: false,
        };
    }

    toggleHidden() {

        /**
         * Toggles the state for form rows.
         */

        this.setState(prevState => ({
            hidden: !prevState.hidden,
        }));
    }

    setFormRowClass(rowType: string) {

        /**
         * Sets the class names for each form row, based on the
         * row type and whether `state.hidden` is true or false.
         */

        const {
            hidden,
        } = this.state;
        const partyRowClassNames = hidden ? ['hidden'] : [];

        return partyRowClassNames.concat([rowType]).join(' ');
    }

    render() {

        return <tbody className='expandable-table'>

            <tr className={this.setFormRowClass('row-type-0')}>
                <td>
                    Row 0 content goes here
                </td>
            </tr>

            <tr className={this.setFormRowClass('row-type-1')}>
                <td>
                    Row 1 content goes here
                </td>
            </tr>

        </tbody>;
    }
}

In the LESS style sheet, I have something like this going on:

.expandable-table {
    tr {
        .hidden {
            visibility: collapse;
        }
    }
}

When the component’s has state.expanded === false, the hidden class is applied to the rows, hiding them with the style visibility: collapse. I wanted to test whether the rendered component was correctly applying my class naming logic, so I wrote up a test like this:

import { shallow } from 'enzyme';
import test from 'tape';

import FormWithCollapsableRows from './FormWithCollapsableRows';

test('When `state.hidden === false`:', t => {

    const wrapper = shallow(<FormWithCollapsableRows />);

    // The component mounts with `state.hidden ==== false`,
    // so the class names should not include "hidden" to
    // begin with.

    t.notOk(
        wrapper.containsMatchingElement(
            <tr className='hidden row-type-0' />),
        'the rows should not be hidden');

    t.end();
});

test('When `state.hidden === true`:', t => {

    const wrapper = shallow(<FormWithCollapsableRows />);

    // Once we toggle `state.hidden` to `true`, the class
    // names should include "hidden"

    wrapper.instance().toggleHidden();

    t.ok(
        wrapper.containsMatchingElement(
            <tr className='hidden row-type-0' />),
        'the rows should be hidden');

    t.end();
});

However, I found that my second test case was failing. After I made the call to wrapper.instance().toggleHidden(), I expected the class name to update according to my render function, which called this.setFormRowClass(). Instead, it seemed that the wrapper I was testing had not received the state update.

It took some perusal of the Enzyme docs to find update(), a method on shallow wrappers that allows you to force a re-render on the component. This turned out to be exactly what I needed. I modified the second test case like this:

test('When `state.hidden === true`:', t => {

    const wrapper = shallow(<FormWithCollapsableRows />);

    // Once we toggle `state.hidden` to `true`, the class
    // names should include "hidden"

    wrapper.instance().toggleHidden();

    // Triggers a re-render, so we know that `this.setFormRowClass` 
    // will be called:

    wrapper.update();

    t.ok(
        wrapper.containsMatchingElement(
            <tr className='hidden row-type-0' />),
        'the rows should be hidden');

    t.end();
});

Now the test passes, as expected.