Helping Families Find an Au Pair

By ozzyaaron

Wednesday, Aug 24, 2016

We finally had time to solve a problem! We knew that host families wanted to know more about our available au pairs before they filled out an application. We thought, what if we improved our featured au pairs page to allow the searching and sorting that you’d want to use when finding somebody to look after your kids.

[we] all wanted to see families find an amazing au pair to look after their children whilst having a fantastic cross cultural experience.”

The story was simple. Find out the types of things that our hosts might want to know when choosing an au pair and make it easy to filter and sort on those reasons. Once our hosts find an amazing au pair they’ll be motivated to complete the complex forms… and one day we’ll make those forms beautiful too.

Let’s Not Wait

You can see the featured au pairs right now, and we have a short video!


Choices

The main choice we made here was to implement this in React using Redux as the datastore. This isn’t even a difficult choice these days. Redux is fantastic conceptually and in practice we like React. We have Angular in the App, and engineers with Ember experience, but we’ve been writing React the last couple of years and it’s great.

The choice of Redux was mainly to get experience using it in a green field environment. Previously we had been using a Flux implementation (Reflux if you care) and personally I always thought it was too complex. Conceptually I’ve found Redux far simpler to understand, once you figure out what the boilerplate is doing (more on that later) it’s simple to add features to a component.

Obstacles

Caching

Performance of the API was a concern. We figured the data would clock in around 200 items and production testing showed this request to be quite slow due to a number of server-side calculated fields.

We added file caching to the API endpoint and the performance seemed appropriate for the expected traffic. We’ll need to maintain an eye on this and perhaps add some performance tweaks later.

Asset Building

We’re using Middleman v3 as the latest Middleman introduced far too many performance regressions. Recently we’ve seen that even with parallel builds Middleman v4 is perhaps as much as four!! times slower than Middleman v3 and we have far too large of a site to allow this regression.

Eventually we found documentation for Middleman v3 and realized we needed to manually include the built assets using sprockets. Once we figured out how to get this working things were actually fairly simple. It’s not as nice as Middleman v4 where we can provide an external command to build the webpacked assets as required, but it’ll do for now.

The Work

Redux

I wanted to start with describing my take on Redux because it was the newest part of this work and probably the piece that I was most impressed with and found most useful throughout. I mean React is pretty great but I’m used to that…

There are plenty of tutorials on Redux and what it is so I’m not going to deep dive on that. The best I’ve found are at redux.js.org and I found it very important to do this reading first before diving in. There are some questionable examples using Redux+React out there so it’s important to be able to evaluate these examples against best practices.

Once you’ve setup your Store the way I continued to look at Redux is that you generate Actions which pass through your Reducers to modify the State and your Store contains the State. There is a lot of terminology that Redux introduces for itself but this understanding did me pretty well throughout. If you read the examples and documentation you’ll see there is more to it but I’m not sure I found my understanding to break down yet.

I basically see it this way:

Action
An event - what happened
Reducer
Event handler that is responsible for modifying state
State
The source of truth
Store
The concept that brings all of these together and allows subscriptions to state changes

A good way of thinking about this is probably with a step through of retrieving participants for our search component. For this we’ll want to retrieve participants when the component mounts that requires the data. We’ll see later more about this sequence.

  1. Component mounts dispatching an action requesting participants
  2. Action dispatches an action that participants are being requested
  3. Action starts to fetch participants
  4. Reducer sees that participants are being requested
    1. Updates state to indicate participants are being fetched
  5. Action receives participants it requested above
    1. Action is generated that participants are being received with participants in payload
  6. Reducer sees participants are being received
    1. Updates state to say fetching is complete
    2. Updates state with received participants
  7. We now have a Store containing state with the participants and that fetching is over

As you can see throughout this exchange we generate a number of actions, which in turn can generate their own actions. Then reducers take these actions and act upon the state.

React + Redux

To me Redux is the above data flow concept, at this point we need a way to map the Store’s state to props or state on the React components such that they will update like any normal state or prop change. The way to achieve this is both easy and complex. It’s easy because the complexity is hidden in a number of decoration type methods and a component that the Redux-React bindings offer.

Again there are so many tutorials on this topic and the redux.js.org ones are probably the best I found. That being said the amount of magic involved can make things even harder to understand. As with most good ‘magic’ that you find in the framework space you can skate by without understanding it very well but it’s incredibly useful in the long run to jump in. That being said I was able to build a scaffold that anyone with React knowledge can jump into and make changes. I know because our CTO did nips and tucks after demonstrations to staff.

As you’ll see later Redux exposed a Provider component and a connect() function. By wrapping your component in their Provider component you can use their connect() decorator with mappings you provide from the store state to generate a new component that will have that state fed into it as props. The important thing to understand is that the connect() method provided by the react-redux bindings generates a new component based on your component that has had a number of modifications to it for performance reasons. All your component needs to know is it’s rendering with props set to certain values - you’re in normal React-land now.

The Good Bits

Adding Features with Redux

I’m not sure I’ve been able to add functionality to a component as simply as I’ve been able to do with Redux+React. Conceptually I find Redux so much easier to understand than Flux and bringing another developer on board to understand the work I’d done proved this out. Previously with Flux it seemed like a hodge podge of hackery. I also feel like the data flow of Redux might lend itself more to parallel operations and better performance due to the decoupling of actions from reducers and state changes.

I’m not sure I’ve been able to add functionality to a component as simply as I’ve been able to do with Redux+React.”

Late in developing the feature one of the members suggested that we should add pagination. It was a feature we’d originally put in the wait-and-see bucket but now that we had waited we could see it needed pagination. I thought a nice pagination component that supported the sorting and filtering we had already implemented was going to be difficult as I thought I’d need to share state into the pagination component. My initial thought was that the pagination component needs to know the visible participants to work out how many pages of data there were and I hadn’t provisioned for this at all.

It was actually very easy in the end as when the container that was responsible for calculating and sorting visible participants did so it would trigger an action that would tell the store how many visible participants there were. Eventually this would appear as a state change and then a prop change to the pagination component. You aren’t really sharing state directly as you are via a consistent global state and I found this much easier to reason about. I was also happier about this as it seemed that it was exactly how Redux might prescribe the implementation - global state is somewhat baked in to a Redux solution.

In the end the pagination component only really deals with events and state about pagination. It knows nothing about the participants, the filter or the sorting and that feels great.

Testing with Enzyme and React

The testing frameworks have come a long way for React since our previous run at testing. I found Sinon, Chai and Enzyme to be very nice tools that offered all the assertion styles and tools I needed. There really isn’t much more to say here except that it was easy to setup and besides a few issues I found writing automated tests for this work to be really simple.

The Enzyme documentation is all I needed here and I expect all that you’ll need too!

The Code

Configuring the Store

import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import participantReducer from './reducers/participantSearch';

const middlewares = [thunkMiddleware];

if (process.env.NODE_ENV === 'development') {
  const createLogger = require('redux-logger');
  const logger = createLogger();
  middlewares.push(logger);
}

export default function configureStore(preloadedState) {
  return createStore(
    participantReducer,
    preloadedState,
    applyMiddleware(...middlewares)
  );
}

Welp, there sure is a lot going on here but let’s break it down.

We’re using thunk which is a middleware that allows actions to return a function. You’ll see later we use it to retrieve the participants and that without it actions generally just return an object.

We’re also using some logging middleware that will output every action as well as the before and after state for that action. It’s really handy to make sure that your actions and reducers are hooked up in the way you expect and an action is updating your state appropriately.

The NODE_ENV is something we populate in our webpack configuration. This appears to be pretty standard in most webpack examples. We obviously don’t want such verbose logging in production.

You’ll notice above that we talked about reducers and how they interact with actions and the store. This is where we’re configuring a Store and telling it which reducers will be listening and which middleware to use.

So long story short this is our store setup, how the reducers are linked to it and the middleware we use.

Participants Reducer

import { combineReducers } from 'redux';
import {
  REQUEST_PARTICIPANTS,
  RECEIVE_PARTICIPANTS,
  RECEIVE_PARTICIPANTS_FAILURE
} from '../actions/participantSearch';
import participantSearchFilter from './participantSearchFilter';
import pagination from './pagination';

function participants(state = {
  isFetching: false,
  errorFetching: false,
  items: [],
}, action) {
  switch(action.type) {
    case REQUEST_PARTICIPANTS:
      return Object.assign({}, state, {
        items: [],
        isFetching: true,
        errorFetching: false
      });
    case RECEIVE_PARTICIPANTS:
      return Object.assign({}, state, {
        items: action.participants,
        isFetching: false,
        errorFetching: false
      });
    case RECEIVE_PARTICIPANTS_FAILURE:
      return Object.assign({}, state, {
        isFetching: false,
        errorFetching: true
      });
    default:
      return state;
  }
}

const participantReducer = combineReducers({
  participantSearchFilter,
  participants,
  pagination
});

export default participantReducer;

Haha now that I’m reading this I’d probably make a small change to move the combined reducers into a separate file for clarity sake but this will do for example sake :)

This is one of our reducers, there are two others. One works on the pagination part of the state and the other works on the filtering and sorting part of the state. Best practice is to keep these separated as long as they’re working on separate parts of the state. Having two reducers operate on the same state is a recipe for disaster. In our case we were able to easily separate pagination, participants and filtering parts of the state so three reducers worked nicely. Having an action generated that kicks of multiple reducers is completely fine as long as your reducers are segregated.

One thing that isn’t initially clear is that each reducer’s output is into a key of the same name in the overall state. By this I mean the output of the participants reducer will appear in state.participants and the output from the pagination reducer will appear in state.pagination. You’ll see later when we map the state to component props how this plays out.

The documentation talks about thinking about the ‘shape’ of your state. I like this way of thinking and that talking about data shape is becoming a thing. What they’re meaning here is essentially what sort of things do you need to know about participants? What do you need to know about the participantSearchFilter? In the end we had a structure like:

{
  participants: {
    items: [],
    isFetching: ...,
    errorFetching: ...
  },
  participantSearchFilter: {
    sortBy: "..",
    infantQualified: ...,
    nativeLanguage: ".."
  },
  pagination: {
    number: ...,
    pageCount: ...,
  }
}

The first argument to the reducer is the current state which we’ve provided a default value for. Importantly if the reducer doesn’t see an action that it should reduce then it passes the state straight through. I thought of reducers as a pipeline of pure functions. That’s why we never mutate the incoming state, we always return a brand new object as the next state.

Also coming into the reducer is the action that caused the reducers to be invoked. If we look at the first case of the switch statement we’ll see that if the action is to request participants then we should set the participants state to { isFetching: true, items: [], errorFetching: false } which totally makes sense. We have no participants, we’re getting them, no errors (yet)!

When we receive the participants you can see that we essentially do the opposite. We set { isFetching: false, items: action.participants, errorFetching: false } and if we look at that action below you’ll see that the action did return the participants from the API in the items key of the object.

Participants Actions

import fetch from 'isomorphic-fetch';

export const REQUEST_PARTICIPANTS = 'REQUEST_PARTICIPANTS';
export const RECEIVE_PARTICIPANTS = 'RECEIVE_PARTICIPANTS';
export const RECEIVE_PARTICIPANTS_FAILURE = 'RECEIVE_PARTICIPANTS_FAILURE';
export const PAGE_FORWARD = 'PAGE_FORWARD';
export const PAGE_BACKWARD = 'PAGE_BACKWARD';
export const PAGE_SET = 'PAGE_SET';
export const PAGE_COUNT_SET = 'PAGE_COUNT_SET';

export const nextPage = () => (
  {
    type: PAGE_FORWARD,
  }
);

export const previousPage = () => (
  {
    type: PAGE_BACKWARD,
  }
);

export const setPage = (number) => (
  {
    type: PAGE_SET,
    number
  }
);

export const setPageCount = (number) => (
  {
    type: PAGE_COUNT_SET,
    number
  }
);

function receiveParticipants(json) {
  const mapJsonToParticipant = (_json) => {
    const { id, attributes } = _json;

    return Object.assign({}, { id }, attributes);
  };

  return {
    type: RECEIVE_PARTICIPANTS,
    participants: json.map(mapJsonToParticipant)
  };
}

function errorReceivingParticipants() {
  return {
    type: RECEIVE_PARTICIPANTS_FAILURE
  };
}

export function requestParticipants() {
  return {
    type: REQUEST_PARTICIPANTS
  };
}

export function fetchParticipants(url) {
  return dispatch => {
    dispatch(requestParticipants());
    return fetch(url)
      .then(response => response.json())
      .then(json => dispatch(receiveParticipants(json)))
      .catch(() => dispatch(errorReceivingParticipants()));
  };
}

These are our actions. I’ve removed the actions to do with filtering and sorting and left those associated with API access and pagination.

As you can see all of these at some point return an object that will be processed by a reducer.

Some of these are simple and might be viewed more like a typical event. For instance requestParticipants() simply returns an object with one property that a reducer uses to just set the state to say we’re fetching participants. You can see that in the above reducers example.

Others are slightly more complex, let’s look at fetchParticipants(). This is where the thunk middleware comes in as you can see we’re returning a function rather than an object it’s also important to note that the dispatch() method that is used inside the function is injected by the thunk middleware. That being said we’ll see the first call there is to dispatch an action requestParticipants() which we’ve just described. At this point we might say that the component that uses this state believes that data is being fetched if we follow through the reducer above.

dispatch() is an important function that is used to tell the store things and all of these actions are called using dispatch() somewhere. You will later see a call like dispatch(fetchParticipants(feedUrl)) used which eventually dispatches the output from fetchParticipants() to the store via receiveParticipants() or errorReceivingParticipants(). As we outlined before whilst this data fetch is happening we’ve already dispatched an action using the requestParticipants() function. The takeaway is that dispatch() is how we tell the store something happened.

A Connected Component

The Setup Using Provider

class ParticipantSearch extends Component {
  render() {
    const { feedUrl, pageSize } = this.props;

    return (
      <Provider store={store}>
        <ParticipantSearchContainer feedUrl={feedUrl} pageSize={pageSize} />
      </Provider>
    );
  }
}

Your Connected Component

import 'babel-polyfill';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import Set from 'es6-set';
import {
  setInfantQualifiedFilter,
  setNativeLanguageFilter,
  fetchParticipants,
  setSortBy,
  nextPage,
  previousPage,
  setPage,
} from '../actions/participantSearch';
import ParticipantSearchNavbar from '../components/ParticipantSearchNavbar';
import VisibleParticipantPanelList from '../containers/VisibleParticipantPanelList';
import Pagination from '../components/Pagination';

class ParticipantSearchContainer extends Component {
  componentDidMount() {
    const { dispatch, feedUrl } = this.props;

    dispatch(fetchParticipants(feedUrl));
  }

  handleFilterChange(filter) {
    const { dispatch } = this.props;
    const { infantQualified, nativeLanguage, sortBy } = filter;

    if (typeof(infantQualified) !== 'undefined') {
      dispatch(setInfantQualifiedFilter(infantQualified));
    }
    if (typeof(nativeLanguage) !== 'undefined') {
      dispatch(setNativeLanguageFilter(nativeLanguage));
    }
    if (typeof(sortBy) !== 'undefined') {
      dispatch(setSortBy(sortBy));
    }
  }

  onPageForward(e) {
    const { dispatch } = this.props;

    e.preventDefault();
    dispatch(nextPage());
  }

  onPageBackward(e) {
    const { dispatch } = this.props;

    e.preventDefault();
    dispatch(previousPage());
  }

  onPageSet(e, number) {
    const { dispatch } = this.props;

    e.preventDefault();
    dispatch(setPage(number));
  }

  render() {
    const { isFetching, errorFetching, participants, filter, page, pageSize, pageCount, dispatch } = this.props;
    const languages = [... new Set(participants.map(p => p.native_language))];
    const onPageSet = this.onPageSet.bind(this);

    return (
      <div className="participant-search">
        <ParticipantSearchNavbar filter={filter} languages={languages} onChange={this.handleFilterChange.bind(this)} />
        <Pagination page={page} onPageSet={onPageSet} pageCount={pageCount} onPageForward={this.onPageForward.bind(this)} onPageBackward={this.onPageBackward.bind(this)} />
        <VisibleParticipantPanelList errorFetching={errorFetching} pageSize={pageSize} dispatch={dispatch} page={page} participants={participants} isFetching={isFetching} filter={filter} />
        <Pagination page={page} onPageSet={onPageSet} pageCount={pageCount} onPageForward={this.onPageForward.bind(this)} onPageBackward={this.onPageBackward.bind(this)} />
      </div>
    );
  }
}

ParticipantSearchContainer.propTypes = {
  participants: PropTypes.array.isRequired,
  isFetching: PropTypes.bool.isRequired,
  errorFetching: PropTypes.bool.isRequired,
  dispatch: PropTypes.func.isRequired,
  feedUrl: PropTypes.string.isRequired,
  filter: PropTypes.object.isRequired,
  page: PropTypes.number.isRequired,
  pageSize: PropTypes.number.isRequired,
  pageCount: PropTypes.number.isRequired,
};

function mapStateToProps(state) {
  const { participants: participantObject, participantSearchFilter: filter, pagination } = state;
  const { isFetching, items: participants, errorFetching } = participantObject;
  const { number: page, pageCount } = pagination;

  return {
    errorFetching,
    isFetching,
    participants,
    filter,
    page,
    pageCount,
  };
}

export default connect(mapStateToProps)(ParticipantSearchContainer);

ParticipantSearchContainter is what I’ve been calling a Connected Component and I probably read that somewhere by someone else. In any case it’s a component that is using the connect() function to be able to map the Store from the Provider to the enclosed Connected Component. You’ll see in the snippet above that Provider is some magic from react-redux and that’s exactly where connect() comes from too.

By providing a mapping function to connect() it generates a new component where the changes in state are mapped to props of the component and then the component is re-rendered just like a state change in React normally.

You can see from the mapStateToProps function where the reducer output mapping to keys in the Store comes into play. We pull apart the participants part of the store to get the data as well as any fetching and error state. Similarly you can pull out the filter and pagination to pass through to those components. I really liked this part of the Redux+React architecture, it uses the best parts of React and once you get the store talking to the component properly it’s very intuitive to add features and any new state they might require. It also allows you to separate pure presentation concerns from state concerns allowing less technical users to update UI without the more difficult concerns.

This component is also where we’re handling a lot of the events from other components. For example we know a page forward needs to dispatch a page forward action but rather than connecting those components I’m using one component to do most of the store connectivity and data wrangling. I’m not exactly sure if this is the correct way but I know it was incredibly easy to add pagination and sorts after the fact. Usually I’ve found I’m doing it wrong if it’s hard. This seems pretty easy to understand and add to.

The Pagination Component

import 'babel-polyfill';
import React, { Component, PropTypes } from 'react';
import Set from 'es6-set';

export default class Pagination extends Component {
  getPagination() {
    const { page, pageCount } = this.props;

    if (pageCount <= 0) {
      return [];
    }

    let pages = [];

    if (pageCount <= 4) {
      for (let i = 1; i <= pageCount; i++) {
        pages.push(i);
      }
    } else if (page <= 4) {
      pages = [1, 2, 3, 4, 5, 6, pageCount - 1, pageCount];
    } else if (page > pageCount - 4) {
      pages = [1, 2, pageCount - 5, pageCount - 4, pageCount - 3, pageCount - 2, pageCount - 1, pageCount];
    } else {
      pages = [1, 2, page - 1, page, page + 1, pageCount - 1, pageCount];
    }

    return this.makeButtons(pages);
  }

  makeButtons(pageNumbers) {
    const uniquePageNumbers = [...new Set(pageNumbers)];
    const buttons = [];
    let lastPage;
    const { page, onPageSet } = this.props;

    for (let i = 0; i < uniquePageNumbers.length; i++) {
      const currentPage = uniquePageNumbers[i];
      if (lastPage && (currentPage - lastPage) > 1) {
        buttons.push(
          <li className="disabled" key={`ellipsis-${i}`}>
            <a key={`pagination-ellipsis-${currentPage}`}>...</a>
          </li>
        );
      }

      const active = (currentPage === page) ? ' active' : '';
      buttons.push(
        <li className={active} key={`page-${currentPage}`}>
          <a href="" onClick={e => onPageSet(e, currentPage) } key={`pagination-${currentPage}`}>{currentPage}</a>
        </li>
      );
      lastPage = currentPage;
    }

    return buttons;
  }

  render() {
    return (
      <div className="text-right">
        <nav>
          <ul className="pagination">
            {this.getPagination()}
          </ul>
        </nav>
      </div>
    );
  }
}

Pagination.propTypes = {
  onPageForward: PropTypes.func.isRequired,
  onPageBackward: PropTypes.func.isRequired,
  pageCount: PropTypes.number.isRequired,
  onPageSet: PropTypes.func.isRequired,
  page: PropTypes.number.isRequired,
};

This is just our Pagination component and you can see that really all it takes is the current page, a total pageCount and then some injected event handlers.

There isn’t a lot to say here as this is pretty typical React at this point. I’ve included it to demonstrate a component that is purely presentation driven and offloads all other concerns. The participantSearchContainer container concerns itself with store connectivity and this component merely accepts props.

Summary

In closing I hope that this helps somebody and demonstrates that we like to try and keep one step back from the cutting edge when developing solutions for our customers.

Whilst this post is a lot of code and technical talk the main reason that we were able to release this feature so completely and quickly was due to quality communication within the team.”

Whilst this post is a lot of code and technical talk the main reason that we were able to release this feature so completely (there was a lot of functionality added to the API side too) and so quickly was due to quality communication in the team and stakeholder interest. Everybody through the C-level, department head, PM and developers all wanted to help families find an amazing au pair to look after their children whilst having a fantastic cross cultural experience. The ability to integrate feedback so easily throughout the development process is just a testament to the tools we chose here and the team ability to work through the small obstacles as they arose.

I think we delivered performant, intuitive experience that will connect people more easily when making a very important choice. Not only that but any developer could start working on this tomorrow and feel pretty comfortable which is generally one of my top goals.

In short:

  • Redux+React - Amazing. Really love it!
  • Teamwork - Makes the dream work.

Resources

It feels like a shame but I can’t say I’d recommend any other examples here. I must’ve read 20+ blog posts but in the end the above were the most useful and the Redux JS documentation was the most useful by far.