Towards Explicit State

I recently, at work, made a postcode look-up component that worked in the manner you’d expect: enter a postcode, click “find address”, select the correct address from a list, finally show the parts of the address in textfields. This was written as a component in Angular 1.5, so the controller for it looked something like:

import AddressLookupService from '../../services/address-lookup.service';
import { IAddress } from '../../interfaces/address';

export default class {
  static $inject = ['addressLookupService'];

  // internal state
  postcode: string;
  choices: { id: string; text: string; }[];
  selectedId: string;
  address: IAddress;

  // callback bound to the parent
  onChange: (obj: { address: IAddress }) => void;

  constructor(private addressLookupService: AddressLookupService) {}

  // called when user clicks "enter address manually" button
  manualEntry() {
    this.address = {
      line1: '',
      line2: '',
      townCity: '',
      postcode: '',
    }
  }

  // called when user clicks "find address" button
  findAddress() {
    this.addressLookupService.find(this.postcode)
      .then(response => {
        // clear previous choice
        this.selectedId = null;

        // setting address to null hides the textfields
        this.address = null;

        // setting choices to non-null shows the list of options
        this.choices = response.items.map(x => ({id: x.id, text: x.name}));
      });
  }

  // called when list item is selected
  selectAddress() {
    this.addressLookupService.retrieve(this.selectedId)
      .then(response => {
        // stop showing choices
        this.choices = null;

        // show the chosen address
        this.address = {
          line1: response.line1,
          line2: response.line2,
          townCity: response.townCity,
          postcode: response.postcode,
        }

        this.addressChanged();
      });
  }

  // called when address selected and when textfields change
  addressChanged() {
    this.onChange({ address: this.address });
  }
}

Within this there are a few subtle connections between the template and whether variables are null/non-null. To make them clearer we can break up how this controller stores data in to distinct states.

These states can easily be modelled in Typescript through tagged unions.

interface Start {
  state: 'Start';
}

interface PostcodeEntered {
  state: 'PostcodeEntered';
  choices: { id: string; text: string; }[];
  selectedId: string;
}

interface AddressSelected {
  state: 'AddressSelected';
  address: IAddress;
}

type State = Start | PostcodeEntered | AddressSelected;

Now it is clear that if an address is being displayed we should not show a list of choices, because we don’t have a list to show! The controller can now be refactored to use this State type.

export default class {
  static $inject = ['addressLookupService'];

  // since postcode is always shown it is declared in the controller, not State
  postcode: string;
  state: State;
  onChange: (obj: { address: IAddress }) => void;

  constructor(private addressLookupService: AddressLookupService) {}

  manualEntry() {
    this.state = {
      state: 'AddressSelected',
      address: {
        line1: '',
        line2: '',
        townCity: '',
        postcode: '',
      },
    };
  }

  findAddress() {
    this.addressLookupService.find(this.postcode)
      .then(response => {
        this.state = {
          state: 'PostcodeEntered',
          choices: response.items.map(x => ({id: x.id, text: x.name})),
          selectedId: null,
        };
      })
  }

  selectAddress() {
    if (this.state.state === 'PostcodeEntered') {
      this.addressLookupService.retrieve(this.state.selectedId)
        .then(response => {
          this.state = {
            state: 'AddressSelected',
            address: {
              line1: response.line1,
              line2: response.line2,
              townCity: response.townCity,
              postcode: response.postcode,
            },
          };

          this.addressChanged();
        });
    }
  }

  addressChanged() {
    if (this.state.state === 'AddressSelected') {
      this.onChange({ address: this.state.address });
    }
  }
}

This maybe doesn’t look much clearer than before – and of course there are those two methods that rely on being in a certain state – but the relationship between state and the template is much clearer since we no longer have to change what is displayed depending on if certain variables are null or not… hoping that in the future everyone remembers to set the correct variable to null in the correct place.

<div ng-if="$ctrl.choices !== null">...</div>
<div ng-if="$ctrl.address !== null">...</div>

Instead the template can make use of explicit state names and know which values will be defined.

<div ng-if="$ctrl.state.state === 'PostcodeEntered'">...</div>
<div ng-if="$ctrl.state.state === 'AddressSelected'">...</div>

When you don’t have the reassurance you get when writing TSX or Elm, having obvious looking templates that are not dependent on certain variables makes changing things in the future much easier.