Containers

One of the most important principles in developing React applications is the need to separate your aesthetic components (what the user sees) from your container components (that handle the data fetching for aesthetic components). This allows far greater portability of components.

With that in mind, we need a Component which will display an input box that we can type in a search and the users that match this search. We'll also need a Container component to handle firing the actions that search and getting the results from our store. In reality you'd want to break this up further but this will do for now.

Create a file src/Home.js:

import React from 'react';
import Immutable from 'react-immutable-proptypes';
import { Map } from 'immutable';

export default class Home extends React.Component {
  static propTypes = {
    search: React.PropTypes.func.isRequired,
    results: Immutable.map.isRequired,
    failed: Immutable.map.isRequired,
    loading: React.PropTypes.bool.isRequired,
  };

  static defaultProps = {
    loading: false,
    failed: false,
  };

  constructor(props) {
    super(props);
    this.state = {
      query: null,
    };
  }

  onChange = (event) => {
    const query = event.target.value;
    this.setState({
      query,
    }, () => {
      if (query.length > 3) {
        this.props.search(query);
      }
    });
  }

  render() {
    const { loading } = this.props;
    let { results, failed } = this.props;

    results = results.get(this.state.query, new Map({}));
    failed = failed.get(this.state.query, false);

    return (
      <div>
        <h1>Gambit Example App</h1>
        <p>Enter a username to search github</p>
        <input
          type="text"
          onChange={this.onChange}
        />
        {!loading && (
          <div>
            {results.map(user => (
              <p>{user.get('id')} {user.get('login')}</p>
            ))}
          </div>
        )}
        {loading && (
          <p>Loading</p>
        )}
        {failed && (
          <p>Failed to fetch, probably over API limit... wait a few </p>
        )}
      </div>
    );
  }
}

This is our aesthetic component and is the same as any other React component in how it transforms props and state into a component so we won't go into it in detail.

Now create src/HomeContainer.js:

import {
  createContainer,
} from 'gambit';

import {
  getUserSearch,
} from './actions/user';

import Home from './Home';

export default createContainer(Home, {
  fetch: {
    results: {
      as: (state) => state.user.get('userSearches'),
    },
    loading: {
      as: (state) => state.user.get('searching'),
    },
    failed: {
      as: (state) => state.user.get('searchFailed'),
    },
  },
  methods: {
    search: dispatch => query => {
      return dispatch(getUserSearch({ query }));
    },
  },
});

This is the Container component. What this does is provide the component being contained, in this case Home, with the the properties listed in fetch and the methods listed in methods.

Each as will run whenever the state changes (because a reducer has mutated it in response to an action) and if any of the as methods return a different response, the Home component will be re-rendered.

fetch

Each property within fetch can have to sub-properties, as and grab. When the Container component is mounted or receives new props, it will run the as method and then the grab method:


const Container = createContainer(InnerComponent, {
  fetch: {
    as: state => state.user.get('me'),
    grab: dispatch => currentValue => {
      return currentValue || dispatch(getCurrentUser());
    },
  },
});

In the above snippet, when mounted the container will get the value of state.user.get('me') which will be empty. It will then run grab which will fire dispatch(getCurrentUser()). This action will fire an action that calls the API which then alters the state via a reducer that is listening for that action.

The container will then receive new props and so run as again. This time state.user.get('me') will have the value returned by the API and will not run.

as: (state, props) => {} grab: (dispatch, props) => {}

The props passed as the second argument to as and grab are those passed to the Container component. This means you can do things like:

  user: {
    as: (state, props) => state.user.get(props.routeParams.userId),
    grab: (dispatch, props) => asVal => {
      return asVal || dispatch(getSpecificUser({ userId: props.routeParams.userId });
    },
  }

methods

The methods property of a Container describes any functions that you want to pass down to your aesthetic component to fire an ActionCreator. In our app we use it to pass through a method which creates the actions that call the API.

methods: { method } method: (dispatch, props) => () => {}

The props passed to each method are the props passed to the Container and the values returned by fetch.

This means you can do something like:

const UserPageContainer = createContainer(UserPage, {
  fetch: {
    user: {
      as: (state, props) => state.user.get(props.routeParams.userId),
      grab: (dispatch, props) => asVal => {
        return asVal || dispatch(getSpecificUser({ userId: props.routeParams.userId });
      },
    },
  },
  methods: {
    addUser: (dispatch, props) => {
      return () => {
        dispatch(addUser(props.user.get('name'));
      };
    },
  },
};

Done, Pending, Failed

Often you want to be able to change what your user sees dependening on what's happening with the grab methods that are running on the Container. You can do this by specifying a different render function for each step:

const UserPageContainer = createContainer(UserPage, {
  fetch: {
    user: {
      as: (state, props) => state.user.get(props.routeParams.userId),
      grab: (dispatch, props) => asVal => {
        return asVal || dispatch(getSpecificUser({ userId: props.routeParams.userId });
      },
    },
  },
  pending() { return <p>Loading...</p>; },
  failed() { return <p>An Error Occured</p>; },
  done(ContainedUserPage) { return <ContainedUserPage {...this.props} />; },
});

Note that the argument passed to pending, failed and done is the Component passed into createContainer but with the results of the fetch props and methods passed to it. You should therefore always return that in Done.

Note that you should always pass {...this.props} to your aesthetic component otherwise it won't have the methods you've provided.

By the way, you don't have to include done if you're simply passing back the contained component, it is implied. The following are the same:

const FooContainer = createContainer(Foo, {
  fetch: { ... },
  done(ContainedFoo) { return <ContainedFoo {...this.props} />; },
});

const FooContainer = createContainer(Foo, {
  fetch: { ... },
});

A Word of Warning

One thing to note is that because Redux is a single store, the as methods will be ran whenever your store changes at all, whether or not it's the same reducer or reducer property. As such you shouldn't create any data structures within an as return. This is because Redux will perform a deep equality check and decide correctly that the return value has changed, causing your aesthetic component to re-render every time the store changes at all.

Don't do this:

const FooContainer = createContainer(Foo, {
  fetch: {
    user: {
      as: state => {
        return {
          name: state.user.get('name'),
          age: state.user.get('age'),
        };
      },
    },
  },
});

Because the { name, age } is recreated each time and has a different memory reference so Redux will re-render the Foo component whenever anything changes anywhere in your store. Instead do this:

function Foo({ user }) {
  user = { name: user.get('name'), age: user.get('age') };
  ...
}

const FooContainer = createContainer(Foo, {
  fetch: {
    user: {
      as: state => {
        return state.user;
      },
    },
  },
});