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.

Using this.setState() in React

Interactive forms

I was recently working on a form in React that looked something like this:

Shipping Address:
User Name
123 My Street
This Town, USA 45678

[ ] Use shipping address as billing address.

Billing Address:
User Name
456 Other Ave
That City, USA 56789

I wanted the form to have the following traits:

  • If the Use shipping address as billing address box is checked, copy over the data from the shipping address to the billing address
  • If there was previously data in the billing address and the user un-checks the box, bring back the old billing address

To accomplish these goals, I needed to implement some state management by using a class component and this.state. This is idiomatic React; once you understand how this.state works, you’d probably see this problem as trivial.

First approach

Here’s how you might instinctively use this.setState() in a class method on your component:

export default class OrderForm extends Component {

    constructor(props) {
        super();

        this.state = {
            selectedShippingAddress: null,
            savedBillingAddress: null,
            selectedBillingAddress: null,
            useShippingAsBilling: false,
        };

        this.handleToggleUseShippingAsBilling.bind(this);
    }

    // Version 1

    handleToggleUseShippingAsBilling() {

        const {
            useShippingAsBilling,
            selectedShippingAddress,
            savedBillingAddress,
        } = this.state;

        // If useShippingAsBilling is currently false, set it to `true`
        // and copy `selectedShippingAddress` to `selectedBillingAddress`

        if (!useShippingAsBilling) {
            return this.setState({
                useShippingAsBilling: true,
                selectedBillingAddress: selectedShippingAddress,
            });
        }

        // If useShippingAsBilling is currently true, set it to `false` 
        // and reset `selectedBillingAddress` to the `savedBillingAddress`

        return this.setState({
            useShippingAsBilling: false,
            selectedBillingAddress: savedBillingAddress,
        });
    }

    render() {
        // ... return JSX markup here
    }

As I found out by experience, when saving properties on this.state via this.setState(), React sometimes delays the operation:

Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.

This makes things a bit tricky. The handleToggleUseShippingAsBilling() method in Version 1 first reads the current value of this.state in order to determine what should happen next. If that happens before React has completed a previous this.setState() call, then we will end up with an undesired program state. We need to know that we are starting from the version of state that directly precedes the new state. Fortunately, there is a usage pattern for this.setState() that does exactly that. This is pretty clearly described in the React docs, but I always find that a concrete example helps me remember how things work.

Two kinds of this.setState()

You can use this.setState() in two ways:

  1. If you give it an object, it will overwrite this.state with that object’s properties. Let’s imagine we want to toggle a current state value called useShippingAsBilling. To use this form of this.setState(), we would read the value first, and then use this.setState to write the new value.
    const {
        useShippingAsBilling: prevValue, 
    } = this.state;
    
    this.setState({
        useShippingAsBilling: !prevValue;
    });
    
  2. If you give it a function, it will invoke the function and use its return value to overwrite this.state. The function signature will accept two arguments:
    1. The previous state value, or prevState
    2. The current props

These arguments are ensured by React to be up to date when the function is run. We won’t be using props here, only the prevState. Here’s how it looks with this form of this.setState():

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

A better way

To round out the example, here’s how I used the second form of this.setState to solve this particular problem.

{
    // ... same as above

    // Version 2
    // Reads the version of this.state that we want.

    handleToggleUseShippingAsBilling() {

        return this.setState(prevState => {

            // If useShippingAsBilling is currently false, set it to `true` 
            // and copy `selectedShippingAddress` to `selectedBillingAddress`

            if (!prevState.useShippingAsBilling) {
                return {
                    useShippingAsBilling: true,
                    selectedBillingAddress: prevState.selectedShippingAddress,
                };
            }

            // If useShippingAsBilling is currently true, set it to `false` 
            // and reset `selectedBillingAddress` to `savedBillingAddress`

            return {
                useShippingAsBilling: false,
                selectedBillingAddress: prevState.savedBillingAddress,
            };
        });
    }

    render() {
        // ... return JSX markup here
    }
}

By using the right form of this.setState(), we can reliably read the prevState value and use that to compute the next state value. Success!

Setting up HTTPS in Node.js with Let’s Encrypt

Note: this post was updated on 10/21/16, in the crontab entry for Setting up automatic certificate renewal

Check out my GitHub repo for detailed examples. Also, see: Deploying a Node.js app to DigitalOcean.

In our application amblr, my team and I are using geolocation data from the browser to place the user on a map. As of Chrome version 50, an encrypted connection is required for using location services, per this announcement:

Starting with Chrome 50, Chrome no longer supports obtaining the user’s location using the HTML5 Geolocation API from pages delivered by non-secure connections. This means that the page that’s making the Geolocation API call must be served from a secure context such as HTTPS.

To get HTTPS working in the browser, we used Let’s Encrypt, a new certificate authority that is ‘free, automated, and open.’ I took on the task of setting up the SSL certificates, with some help from a team member who has more command line chops than I do. I learned some useful things along the way.

How Web Servers Work

Servers have both an operating system and server software. Generally, the OS is some fork of Linux; Ubuntu is a popular example which is an option during setup if you use DigitalOcean for hosting. In addition to the OS, server software like Apache or Nginx allows you to serve files and perform routing and other tasks. Node takes the place of Apache/Nginx in simple setups. There may be some situations where you have both Apache/Nginx and Node running. For example, one way to set up SSL is to use Apache as a reverse proxy server that sits between the open Internet and your Node server. We tried this approach, but ran into roadblocks with the configuration file.

Instead, we chose to serve our site directly from Node, so we didn’t want Apache/Nginx to be running at the same time as Node.

Configuring up your Node server

There are a few ways to set up your Node server to use SSL; one of the most straightforward is to use the https module. You’ll need to do the usual routing and set up a static file server. Here’s a simplified server.js file using Express that shows how we set up https with an http server forwarding to it. This allows you to forward requests that come in on port 80 (the default port for http) to port 443 (the default port for https).

For more details, check out my GitHub repo for this post.

Note that you have created a regular HTTP server as a temporary way to set up Let’s Encrypt with the --webroot plugin. You’ll modify the server to switch to HTTPS once you’ve created the certificate files.

path-to-static-files is the directory where you serve static files from with your Node server. This will be important when using the webroot method to renew your certificates.

path-to-privkey is the location of your private key file. path-to-cert is the location of your certificate file. If you follow the directions above, they will be in the following locations –  replace ‘node-https-example.com’ with your domain:

path-to-privkey:

/etc/letsencrypt/live/www.node-https-example.com/privkey.pem

path-to-cert:

/etc/letsencrypt/live/www.node-https-example.com/cert.pem

Configuring Let’s Encrypt

To configure Let’s Encrypt, I referenced this tutorial, but modified the steps to work with a Node-only setup. Instead of using --apache, I used --webroot to set up and renew the certificates. You’ll be installing the Let’s Encrypt Certbot, an automated command line tool that helps you get things configured.

These directions were tested with Ubuntu 16.04.1 on DigitalOcean; other setups may require slightly different steps. Before you start, you’ll need to set up your server with a non-root user that has sudo privileges and configure your domain name.

  1. SSH into the server.
  2. If your Node server is not running already, start it: cd into the directory where your server file is, then do:
    sudo node server.js

    Using sudo is necessary because root privileges are required to expose ports below 1024. If you see an EACCESS error when you try to start Node on a server, it’s usually because you didn’t use sudo or because Node is already running.

  3. In another terminal window, SSH into your server and update the package manager cache:
    sudo apt-get update
  4. Install git:
    sudo apt-get install git
  5. Install Let’s Encrypt:
    sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
  6. Navigate to the Let’s Encrypt repository:
    cd /opt/letsencrypt
  7. Run the configuration, inserting the folder where your site lives instead of node-https-example and your SSH user instead of user. The second flag for -d www.node-https-example.com is optional, but recommended for encrypting traffic at both the http:// and http://www domains.
    ./letsencrypt-auto certonly --webroot -w /home/user/node-https-example/client/www -d www.node-https-example.com -d node-https-example.com
  8. Follow the steps provided and be sure to include an admin email address, in case there are any issues renewing your certificates.
  9. Go to the terminal window where Node is running and enter CTRL + C to stop the Node server.
  10. Using vi server.js or another bash text editor, comment out the lines in the first block, starting from:
    http.createServer(app).listen(80);
  11. Uncomment the bottom portion of the file, starting from:
    // // set up path to key and certificate files
  12. Save and close the file. This will effectively swap out your HTTP server for an HTTPS server.
  13. Start the Node server with sudo node server.js. Note that the server will stop once you close your terminal window. See this post to keep Node running forever.
  14. To see if it worked, go to this URL, replacing ‘node-https-example.com’ with your domain:
    https://www.ssllabs.com/ssltest/analyze.html?d=node-https-example.com&latest

Setting up automatic certificate renewal

To set up automatic certificate renewal, use the cron job as described below. It will try to update the certificate every Monday morning at 2:30 am. If the certificates are less than 30 days away from expiring, they will be renewed.:

  1. SSH in to the server if you aren’t already logged in.
  2. Edit the crontab file for the root user. If necessary, go ahead and create a new crontab for root:
    sudo crontab -e
  3. Add this line to the file to set up the cron job. It will save a log of the results to /var/log/le-renewal.log so you can check back later and see if the renewal is working. Replace user with your SSH user and node-https-example with the folder where your site lives.
    30 2 * * 1 /opt/letsencrypt/letsencrypt-auto certonly --webroot -w /home/user/node-https-example/client/www -d www.node-https-example.com -d node-https-example.com >> /var/log/le-renew.log
  4. Save and exit.

Conclusion

That’s it! You should now have valid SSL certificates that auto-renew, and your site should be accessible via HTTPS.

For more reading, check out the Certbot documentation.