React + Redux Tutorial Part V: The Show Feature

The Show Feature: The CatPage Component


This is Part V of a eight part series on building a CRUD application with React + Redux. You can view the code for this project here. You can view the table of contents here


We're aiming to dynamically render the details of each cat, via some sort of "show" page, next to the index list of cats. Something like this:

In order to retain the index list of cats on the left, even as we render each cat's details on the right, we'll build the CatPage component as a child of the CatsPage component.

We need to do two things to for that to happen:

  • Define the /cats:id route as a child of the /cats route.
  • Call {this.props.children} inside the CatsPage component's render function.

Defining The Route

We'll revisit our routes and add the cat's show route:

import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './components/App';
import HomePage from './components/home/HomePage';
import CatsPage from './components/cats/CatsPage';
import CatPage from './components/cats/CatPage';

export default (
  <Route path="/" component={App}>
    <IndexRoute component={HomePage} />
    <Route path="/cats" component={CatsPage} >
      <Route path="/cats/:id" component={CatPage} />
    </Route>
  </Route>
);

Here, we're mapping the cats/:id route to the CatPage component, which we'll build in a moment. And we're making sure to import that component at the top of the file.

Calling Children

Next up, we need to tell our CatsPage container component to render its children. We'll change the render function of our CatsPage component in the following way:

...
render() {
    const cats = this.props.cats;
    return (
      <div className="col-md-12">
        <h1>Cats</h1>
        <div className="col-md-4">
          <CatList cats={cats} />
        </div>
        <div className="col-md-8">
          {this.props.children}
        </div>
      </div>
    );
  }

Now we're ready to define our CatPage Component.

Defining the Component

Our component needs to subscribe to the store, in order to receive the cats payload and find the correct cat to display. So, we'll start with our mapStateToProps function:

// src/components/cats/CatPage.js

function mapStateToProps(state, ownProps) {
  let cat = {name: '', breed: '', weight: '', temperament: '', hobby_ids: []};
  const catId = ownProps.params.id;
  if (state.cats.length > 0) {
    cat = Object.assign({}, state.cats.find(cat => cat.id == id)
  }
  return {cat: cat};
}
Setting the cat with mapStateToProps

Our function needs to introspect on the state's collection of cats and find the correct one to pass to our component for rendering. We can identify the requested cat by looking at the ownProps argument that gets passed to mapStateToProps.

ownProps represents any props passed to our component. ownProps has an attribute, params, which, thanks to the way we defined our route with the dynamic segment, will look like this:

ownProps.params.id 
// => {id: "1"}

We can use this ID number to find and retrieve the correct cat from state.

However, we have to include the if condition to check for the presence of any cats in state. This is because the component may render before the collection of cats is finished being retrieved from the API.

If that's the case, we don't want our component to break. We'll simply set an empty cat placeholder object until the state changes to include the cats payload. When that happens, it will trigger the mapStateToProps function to run again, followed by a re-render of our component.

Now that we are successfully grabbing the right cat and passing it to the component as props, let's build out our render function.

import React, {PropTypes} from 'react';
import {connect} from 'react-redux';

class CatPage extends React.Component {
  render() {
    return (
      <div className="col-md-8 col-md-offset-2">
        <h1>{this.props.cat.name}</h1>
        <p>breed: {this.props.cat.breed}</p>
        <p>weight: {this.props.cat.weight}</p>
        <p>temperament: {this.props.cat.temperament}</p>
      </div>
    );
  }
};

CatPage.propTypes = {
  cat: PropTypes.object.isRequired,
};

function mapStateToProps(state, ownProps) {
  let cat = {name: '', breed: '', weight: '', temperament: '', 
    hobby_ids: []};
  const catId = ownProps.params.id;
  if (state.cats.length > 0) {
    cat = Object.assign({}, state.cats.find(cat => cat.id == 
      id)
  }
  return {cat: cat};
};

export default connect(mapStateToProps)(CatPage);

So far so good. The only problem is that we're not displaying the collection of hobbies associated with the given cat. In order to do that, we need to retrieve the hobbies from our API, add them to the application's state, and pull them into the component in our mapStateToProps function.

We'll follow the same pattern we used to load all the cats from the API to build out this functionality.

Loading Hobbies from the API

We'll start by building out or HobbyApi class.

// src/api/hobbyApi.js

class HobbyApi {
  static getAllHobbies() {
    return fetch('http://localhost:5000/api/v1/hobbies').then(response => {
      return response.json()
    }).catch(error => {
      return error
    });
  }
};

export default HobbyApi;

Next up, we'll build out some hobby-specific actions for making this API call and dispatching a success action to the store.

// src/actions/hobbyAction.js

import * as types from './actionTypes';
import hobbyApi from '../api/HobbyApi';

export function loadHobbiesSuccess(hobbies) {
  return {type: types.LOAD_HOBBIES_SUCCESS, hobbies};
}

export function loadHobbies() {
  return function(dispatch) {
    return hobbyApi.getAllHobbies().then(hobbies => {
      dispatch(loadHobbiesSuccess(hobbies));
    }).catch(error => {
      throw(error);
    });
  };
}

We'll add the LOAD_HOBBIES_SUCCESS action type to our list of constants:

// src/actions/actionTypes.js

export const LOAD_CATS_SUCCESS = 'LOAD_CATS_SUCCESS';
export const LOAD_HOBBIES_SUCCESS = 'LOAD_HOBBIES_SUCCESS';

Lastly, we'll build a hobbies reducer to handle the dispatch of the loadHobbiesSuccess action.

// src/reducers/hobbyReducer.js

import * as types from '../actions/actionTypes';
import initialState from './initialState';

export default function courseReducer(state = initialState.hobbies, action) {
  switch(action.type) {
    case types.LOAD_HOBBIES_SUCCESS:
      return action.hobbies; 
    default: 
      return state;
  }
}

Next, we need to add the hobbies reducer to our root reducer:

// src/reducers/rootReducer.js

import {combineReducers} from 'redux';
import cats from './catReducer';
import hobbies from './hobbyReducer';

const rootReducer = combineReducers({
  cats,
  hobbies
})

export default rootReducer;

Now, let's tell the store to dispatch our loadHobbies action when it dispatches the loadCats action--in our app's entry point, index.js.

/*eslint-disable import/default */
import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import configureStore from './store/configureStore';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import routes from './routes';
import {loadCats} from './actions/catActions';
import {loadHobbies} from './actions/hobbyActions';

const store = configureStore();

store.dispatch(loadCats());
store.dispatch(loadHobbies());

render(
  <Provider store={store}>
    <Router history={browserHistory} routes={routes} />
  </Provider>,
  document.getElementById('app')
);

Great! Now the state that get's passed to our container components will look something like this:

{cats: [some cats], hobbies: [some hobbies]}

We're ready to revisit our CatPage component and grab the cat hobbies to display!

Displaying Associated Data

In our Rails API, we serialized our data such that each cat has a property, hobby_ids, pointing to an array of the IDs of the hobbies to which it is associated.

[
  {
   id: 1, 
   name: "Moe", 
   weight: "heavy", 
   temperament: "protective", 
   hobby_ids: [1, 2]
  },
  {
   id: 2, 
   name: "Mini", 
   weight: "heavy", 
   temperament: "sweet", 
   hobby_ids: [1]
  }, 

]

So, we can use this hobby_ids attribute to collect the given cat's hobbies in the mapStateToProps function of our CatPage component. We'll expand upon on mapStateToProps function and build a helper function to help us collect the correct hobbies:

// src/components/cats/CatPage.js

...
function collectCatHobbies(hobbies, cat) {
  let selected = hobbies.map(hobby => {
    if (cat.hobby_ids.filter(hobbyId => hobbyId == hobby.id).length > 0) {
      return hobby;
    }
  })
  return selected.filter(el => el != undefined)
}

function mapStateToProps(state, ownProps) {
  let cat = {name: '', breed: '', weight: '', temperament: '', hobby_ids: []};
  let Cathobbies = []
  const catId = ownProps.params.id;
  if (state.cats.length > 0 && state.hobbies > 0) {
    cat = Object.assign({}, state.cats.find(cat => cat.id == id)
    if (cat.hobby_ids.length > 0) {
      hobbies = collectCatHobbies(state.hobbies, cat)
    }
  }
  return {cat: cat, catHobbies: catHobbies};
};

Let's update our CatPage Prop Type validations accordingly:

CatPage.propTypes = {
  cat: PropTypes.object.isRequired,
  catHobbies: PropTypes.array.isRequired
};

Now we can update our render method to display the hobbies we've collected.

// src/components/cat/CatPage.js;
import HobbList from './HobbyList';
...
render() {
    return (
      <div className="col-md-8 col-md-offset-2">
        <h1>{this.props.cat.name}</h1>
        <p>breed: {this.props.cat.breed}</p>
        <p>weight: {this.props.cat.weight}</p>
        <p>temperament: {this.props.cat.temperament}</p>
        <HobbyList hobbies={this.props.catHobbies} />
      </div>
    );
}

...

In line with the pattern of using container components pass data to presentation components, we'll call on a new component, HobbyList, instead of dealing with all the presentational details here.

The HobbyList Component

We'll set up our HobbyList component similarly to how we configured our CatList component earlier:

import React, {PropTypes} from 'react';

const HobbyList = ({hobbies}) => {
  return (
    <div>
      <h3>Hobbies</h3>
      <ul>
        {hobbies.map(hobby => 
            <li key={hobby.id}>{hobby.name}</li>
          )}
      </ul>
    </div>
  );
};

HobbyList.propTypes = {
  hobbies: PropTypes.array.isRequired
};

export default HobbyList;

Now our CatPage component should be fully up and running--displaying all of a cat's details, including that cat's hobbies.

Part VI: The Edit Feature >>

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus