React + Redux Tutorial Part VII: The Create Feature

The Create Cat Feature


This is Part VII 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 want our user to be able to create a new cat by clicking a "new cat" button available to them on the cat index display. We want a user to seamlessly transition to viewing their newly created cat upon submitting the form. Something like this:

The good news is we'll be able to re-use a lot of the code we wrote for editing cats, since we'll borrow the CatForm component that we just built.

Just like we used the CatPage component as a container for the CatForm when we were editing cats, we'll build a container component to wrap up the way in which a user will interact with our form to create a brand new cat.

The NewCatPage Component

Before we define our component, we'll build a route for it, and a button that links to it on the CatsPage component.

Defining the Route
// src/routes.js

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';
import NewCatPage from './components/cats/NewCatPage';

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

We'll add a button that links to the NewCatPage component from the CatsPage component

// src/components/cats/CatsPage.js

...
render() {
    const cats = this.props.cats;
    return (
      <div className="col-md-12">
        <h1>Cats 
          <Link to={'/cats/new'} className="btn btn-primary">
            + cat
          </Link> 
        </h1>
        <div className="col-md-4">
          <CatList cats={cats} />
        </div>
        <div className="col-md-8">
          {this.props.children}
        </div>
      </div>
    );
  }
Defining the Component

Our NewCatPage component shares much with our CatPage component. We still need to set internal state that contains a cat object. We need actions that respond to updating that cat object as the user fills out the form, updates that new cat's hobby collection and to submits the form.

There are a few key differences however:

  • We are not plucking a cat from the application state's collection to display/edit. Instead, we can set a default empty cat object to populate with data as the user fills out the form. We'll do this in the component's constructor function.
  • The hobbies that we pass to our checkbox components should all be unchecked initially, as this form represents a brand new cat.
  • The saveCat function that gets triggered when a user submits the form should trigger a createCat action, which we'll define in a bit.

Let's take a look at our component:

import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as courseActions from '../../actions/catActions';
import CatForm from './CatForm';


class NewCatPage extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      cat: {
        name: '', 
        breed: '', 
        weight: '', 
        temperament: '', 
        hobby_ids: []
      },
      saving: false
    };
    this.redirect = this.redirect.bind(this);
    this.saveCat = this.saveCat.bind(this);
    this.updateCatHobbies = this.updateCatHobbies.bind(this);
    this.updateCatState = this.updateCatState.bind(this);
  }

  updateCatHobbies(event) {
    const cat = this.state.cat;
    const hobbyId = event.target.value;
    const hobby = this.props.checkBoxHobbies.filter(hobby => { 
      hobby.id == hobbyId)
    })[0];
    const checked = !hobby.checked;
    hobby['checked'] = !hobby.checked;
    if (checked) {
      cat.hobby_ids.push(hobby.id);
    } else {  
      cat.hobby_ids.splice(cat.hobby_ids.indexOf(hobby.id));
    }

    this.setState({cat: cat});
  }

  updateCatState(event) {
    const field = event.target.name;
    const cat = this.state.cat;
    cat[field] = event.target.value;
    return this.setState({cat: cat});
  }

  saveCat(event) {
    event.preventDefault();
    this.props.actions.createCat(this.state.cat)
  }
  
  render() {
    return (
      <div>
        <h1>new cat</h1>
        <CatForm 
          cat={this.state.cat} 
          hobbies={this.props.checkBoxHobbies}
          onSave={this.saveCat}
          onChange={this.updateCatState}
          onHobbyChange={this.updateCatHobbies}/>
      </div>
    );
  }
}

function hobbiesForCheckBoxes(hobbies) {
  return hobbies.map(hobby => {
    hobby['checked'] = false;
    return hobby;
  });
}

NewCatPage.propTypes = {
  checkBoxHobbies: PropTypes.array.isRequired, 
  actions: PropTypes.object.isRequired
};

function mapStateToProps(state, ownProps) {
  let checkBoxHobbies = [];
  if (state.hobbies.length > 0) {
    checkBoxHobbies = hobbiesForCheckBoxes(Object.assign([], 
      state.hobbies));
  }

  return {
    checkBoxHobbies: checkBoxHobbies
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(courseActions, dispatch)
  };
}


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

Now we're ready to define our saveCat action creator and its corresponding CatApi function.

The saveCat Action Creator

First, we need a CatApi function that our saveCat action creator can call:

// src/api/catApi.js

class CatsApi {
  ...
  static createCat(cat) {
    const request = new Request('http://localhost:5000/api/v1/cats/', {
      method: 'POST',
      headers: new Headers({
        'Content-Type': 'application/json'
      }), 
      body: JSON.stringify({cat: cat})
    });


    return fetch(request).then(response => {
      return response.json();
    }).catch(error => {
      return error;
    });
  }
}

Next up, our action creator:

// src/actions/catActions.js

export function createCatSuccess(cat) {
  return {type: types.CREATE_CAT_SUCCESS, cat}
}

export function createCat(cat) {
  return function (dispatch) {
    return catApi.createCat(cat).then(responseCat => {
      dispatch(createCatSuccess(responseCat));
      return responseCat;
    }).catch(error => {
      throw(error);
    });
  };
}

Let's add the CREATE_CAT_SUCCESS constant to our action type constants:

// src/actions/actionTypes.js

export const LOAD_CATS_SUCCESS = 'LOAD_CATS_SUCCESS';
export const LOAD_HOBBIES_SUCCESS = 'LOAD_HOBBIES_SUCCESS';
export const UPDATE_CAT_SUCCESS = 'UPDATE_CAT_SUCCESS';
export const CREATE_CAT_SUCCESS = 'CREATE_CAT_SUCCESS';

The createCatSuccess action gets dispatched to our reducers, sending along the new cat object with it. Let's teach our cat reducer to respond to this action by creating a new copy of state that includes our new cat:

import * as types from '../actions/actionTypes';
import initialState from './initialState';
import {browserHistory} from 'react-router';

export default function catReducer(state = initialState.cats, action) {
  switch(action.type) {
    case types.LOAD_CATS_SUCCESS:
      return action.cats
    case types.CREATE_CAT_SUCCESS:
      browserHistory.push(`/cats/${action.cat.id}`)
      return [
        ...state.filter(cat => cat.id !== action.cat.id),
        Object.assign({}, action.cat)
      ]
    case types.UPDATE_CAT_SUCCESS:
      return [
        ...state.filter(cat => cat.id !== action.cat.id),
        Object.assign({}, action.cat)
      ]
  }
}

There is one new thing to pay attention to here. The following line:

browserHistory.push(`/cats/${action.cat.id}`)

Once our new cat is created and saved, and just before it's about to be sent back down to the components as part of the new state, we want to tell our app to "redirect" to that new cat's show page.

I say "redirect" because we're still operating within the environs of single-page application. Just about all of our views are rendered by, or as children of, the CatsPage index component. So, our "redirect", effected by operating on the browserHistory object made available to use by React Router, does two things for us:

  • Tell the CatsPage component that the child is should render is the CatPage component.
  • Set the current URL to something like: /cats/7, making that ID number, 7, available to the CatPage component via ownProps.params.id in the mapStateToProps function.

Thus, the CatPage component will render and display the newly created cat.

There's one problem however. Let's revisit the mapStateToProps function of our CatPage component:

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

We have a couple of if conditions here that amount to the following logic: if the params have an id property, find the cat with that ID from the state's collection of cats. If that cat has any hobby IDs in its hobby_ids collection, collect the hobbies with those IDs.

But, our cat reducer redirects us to this render this component before it returns the new state that includes the newly created cat. So, we get a scenario in which nextProps.params.id exists and return the ID of the new cat, BUT, the state's cats collection doesn't yet contain that cat.

Oh no! Don't worry though. As soon as the cat reducer does return that new copy of state contained our new cat, this mapStateToProps function will simply run again, this time properly fetching the cat to display.

We just have to make sure our function does break by trying to find a cat that isn't yet contained in state.cats. Let's add one more piece to our second if condition:

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

Phew! We fixed it!

Now that we can create cats, its time to build out the delete cat feature.

Part VIII: The Delete Feature >>

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus