Using Repertoire

Repertoire works together with React and Redux, so make sure you have the following packages installed in your application:

Creating Controllers

The main purpose of a controller is to encapsulate actions which will eventually update the Redux store. The constructor is where we define the store properties that are to be mapped to the React component.

A controller will receive one React component as argument.

import {BaseController} from 'repertoire'

export default class AdminController extends BaseController {
  constructor(component) {
    super(component);
    
    this.state = {
      // ...
    };
    
    this.connect();
  }
}

Adding Actions

The essence of the controller are the actions. These actions, along with the state properties, are mapped automatically to the React component. You can also map them yourself to another component which is not linked to the controller, by using the connect() utility from react-redux package. More on that later.

Defining a new action is as simple as adding a new method to the controller. Every action has an implicit reducer which will updated the Redux store.

The action needs to return a Promise. The result of the promise will be merged with the existing state of the Redux store.

import {BaseController} from 'repertoire'

export default class AdminController extends BaseController {
  fetchUsers () {
    return AdminApi.getUsers().then(result => {
    
      // the result of this Promise will update the Redux store,
      // by calling an implicit reducer internally
      return {
        users: result
      };
    });
  }
  
  constructor(component) {
    super(component);
    
    this.state = {
      // ...
    };
    
    this.connect();
  }
}

Repertoire uses redux-saga internally as the Redux middleware, but you'll most likely not have to care about it.

Mapping Store State to React

In order to map the Redux store state to React props, you will need to define the controllerstate. This will automatically be made available to the React component as properties, along with the controller actions.

In the previous step there is an action which eventually will resolve with the object {users: result} and this will update the store.

The following will result in a React prop containing the users store value.

import {BaseController} from 'repertoire'

export default class AdminController extends BaseController {
  fetchUsers () {
    return AdminApi.getUsers().then(result => {
    
      // the result of this Promise will update the Redux store,
      // by calling an implicit reducer internally
      return {
        users: result
      };
    });
  }
   
  constructor(component) {
    super(component);
    
    this.state = {
      users(store) {
        return store.users || [];
      }
    };
    
    this.connect();
  }
}

Controller Namespace

If you would like to use a specific namespace on the Redux store, most likely because you want two or more controllers to operate on the same store segment, you can use the stateNamespace property.

import {BaseController} from 'repertoire'

export default class AdminController extends BaseController {
  // the section of the redux store which this controller will operate on
  get stateNamespace() {
    return 'admin';
  }

  setCurrentUser(currentUser) {
    return {
      currentUser
    }
  }
    
  fetchUsers () {
    // ...
  }
  
  constructor(component) {
    super(component);
    
    // ...
  }
}

Connecting Store Updates Manually

Sooner or later you will probably need to manually connect the Redux store state or dispatch actions to a component which is not directly linked to a controller, by using the connect() utility.

To do so, you can reference existing actions using dispatchActions and bind them using the bindActionCreators utility from Redux.

import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {dispatchActions} from 'repertoire'

class User extends Component { 
  componentWillMount() {
    this.props.setCurrentUser(this.state.selectedUser);
  }
  
  render() {
    const {users} = this.props;
    
    //...
  } 
}

const mapStateToProps = state => ({
  users: state.admin.users
});

const mapDispatchToProps = dispatch => bindActionCreators({
  setCurrentUser: dispatchActions.setCurrentUser
}, dispatch);

export default connect(mapStateToProps, mapDispatchToProps)(User);
  

Error Handling

Repertoire is handling errors thrown from Controller actions automatically and adding the last error on the Redux store, as the lastThrownError property. This is available automatically.

Whenever the promise from a Controller action throws or rejects, the store will be updated with the error object automatically.

The error object will also contain an initiator property, which will reflect the action which has caused the error.

controller.js
import {BaseController} from 'repertoire'

export default class AdminController extends BaseController {
  // the section of the redux store which this controller will operate on
  get stateNamespace() {
    return 'admin';
  }
   
  fetchUsers () {
    // simulating an exception
    return new Promise((resolve, reject) => {
      reject(new Error('Not Found'));
    });
  }
  
  constructor(component) {
    super(component);
    
    this.state = {
      users(store) {
        return store.users || [];
      }
    };
    
    this.connect();
  }
}
component.js
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import Controller from './controller.js';

class Admin extends Component {
  static propTypes = {
    users: PropTypes.array.isRequired,
    fetchUsers: PropTypes.func.isRequired
  };

  constructor(props) {
    super(props);
    // ...
  }

  componentWillMount() {
    this.props.fetchUsers();
  }

  render() {
    const {users, lastThrownError} = this.props;
    
    // lastThrownError.initiator === 'fetchUsers'
    console.error(lastThrownError);
    
    return users.length > 0 ? <div>
      // ...
    </div> : null;
  }
}

export default new Controller(Admin);

Catching Errors from Actions

You can also catch errors from actions yourself and then export the error object. If you export the error using the lastError property name, it will also contain an initiator property.

controller.js
import {BaseController} from 'repertoire'

export default class AdminController extends BaseController {
  // the section of the redux store which this controller will operate on
  get stateNamespace() {
    return 'admin';
  }
   
  fetchUsers () {
    // simulating an exception
    return new Promise((resolve, reject) => {
      reject(new Error('Not Found'));
    }).catch(err => ({
      users: [],
      lastError: err
    }));
  }
  
  constructor(component) {
    super(component);
    
    this.state = {
      users(store) {
        return store.users || [];
      }
    };
    
    this.connect();
  }
}

Last updated