React - Create (POST)


In this video we are going to add the Create functionality to our React CRUD application. The real heavy lifting here will be done by POST'ing to our API, so here we will only be concerned with creating and displaying a form using React, and then handling the submit functionality.

As we added React router to our app in the previous video, I am going to start by creating a new route for handling creation of new blog posts:

// /src/App.js

import React, { Component } from 'react';
import { Router, browserHistory, Route, IndexRedirect } from 'react-router'
import List from './containers/list';
import Create from './containers/create';
import NotFoundPage from './components/NotFoundPage';

export default class App extends Component {

  render() {
    return (
      <Router history={browserHistory}>
        <Route path="/">
            <IndexRedirect to="/posts"/>
        </Route>
        <Route path="/posts" component={List}/>
        <Route path="/posts/create" component={Create}/>
        <Route path="*" component={NotFoundPage}/>
      </Router>
    );
  }
}

In case it's not immediately obvious, the new line here is:

<Route path="/posts/create" component={Create}/>

And the associated import statement:

import Create from './containers/create';

Ok, so far, so good. We've added a route and told it to display the Create component when we hit that route. But that component doesn't actually exist, so let's add that also:

import React, { Component } from 'react';

export default class Create extends Component {

    render() {
        return (
            <div>
                <form name="blog_post" className="form-horizontal">
                    <div id="blog_post">
                        <div className="form-group">
                            <label className="col-sm-2 control-label required" htmlFor="blog_post_title">Title</label>
                            <div className="col-sm-10">
                                <input type="text"
                                       id="blog_post_title"
                                       required="required"
                                       className="form-control"/>
                            </div>
                        </div>
                        <div className="form-group">
                            <label className="col-sm-2 control-label required" htmlFor="blog_post_body">Body</label>
                            <div className="col-sm-10">
                                <input type="text"
                                       id="blog_post_body"
                                       required="required"
                                       className="form-control"/>
                            </div>
                        </div>
                        <div className="form-group">
                            <div className="col-sm-2"></div>
                            <div className="col-sm-10">
                                <button type="submit"
                                        id="blog_post_submit"
                                        className="btn-default btn">
                                    Submit
                                </button>
                            </div>
                        </div>
                    </div>
                </form>
            </div>
        );
    }
}

All I have done here is copy / paste the form from the Twig implementation and change up a few things.

Firstly, React will have a benny if you use the for attribute on a label - so I have changed for to htmlFor.

Likewise, React also wants you to use className instead of class - for any CSS references.

I've also removed the method="post" from the form element. The reasoning for this is that this form isn't going to POST directly - we will handle this ourselves. Also, we don't have an action in our form element, for the same reason.

This does raise a question though - if our form is to do something when we click submit, what should it do?

Well, the interesting thing is - that depends :)

In our Create work flow, we might want to add a little flash message, redirect the user to the list view, and of course, actually send in the POST request to add the new content.

However, in our Update work flow, we might want a different flash message, we maybe don't want to redirect the user anywhere - just show the updated page - and this time we would need to make either a PUT or a PATCH request.

Fortunately, React makes this easy for us.

Handling Form Submissions

As mentioned, depending on which 'screen' we are on, depends on what should happen when the form is submitted.

In the case of our 'Create' screen we want to POST off the form's data to the API, and then ideally, redirect the user to the List view. Redirecting is outside the scope of this video, so for now, we will just content ourselves with the POST'ing of data.

To do this, whereever we use the form, we are going to pass the form an as-yet-unrun function (a callback) that will only be called / run whenever the submit button is pressed.

The way we will set this up is to tell the form which function we want to run by way of the onSubmit function:

<form name="blog_post" className="form-horizontal">

becomes:

<form name="blog_post" className="form-horizontal" onSubmit={this.handleSubmit}>

There's two important things happening here:

First, we haven't actually created the handleSubmit function yet, we will do that in a moment.

Second, we are passing in the function itself, not the outcome of running this function - note, no () brackets at the end:

  • onSubmit={this.handleSubmit} - this
  • onSubmit={this.handleSubmit()} - not this

That's really important. In JavaScript you can pass un-run functions around just like you do with any other type of variable. Pretty awesome. This is because functions are first class in JavaScript.

Defining the handleSubmit function is simple enough:

export default class Create extends Component {

    handleSubmit(data) {
        console.log('form submission data', data);
    }

And this should work now, in some sense anyway.

The thing is, the output from the console.log is likely not what you are expecting. You will end up with a SyntheticEvent, which is a bit confusing.

Unfortunately getting the form data is a little more involved than this. We will sort this out, but for the moment, let's quickly refactor out the form HTML into its own class.

Form Component

We want to be able to re-use the form. By passing in the function to run whenever the form is submitted, we have greatly increased the re-usability of this form component. But as it's currently hardcoded into the create.js file, it needs extracting to be properly re-usable.

// /src/components/form.js

import React from 'react';

const Form = React.createClass({

    handleSubmit(e) {
        e.preventDefault();
        this.props.onSubmit(this.state);
    },

    render() {
        return (
            <form name="blog_post" className="form-horizontal" onSubmit={this.handleSubmit}>
                <div id="blog_post">
                    <div className="form-group">
                        <label className="col-sm-2 control-label required" htmlFor="blog_post_title">Title</label>
                        <div className="col-sm-10">
                            <input type="text"
                                   id="blog_post_title"
                                   required="required"
                                   className="form-control"/>
                        </div>
                    </div>
                    <div className="form-group">
                        <label className="col-sm-2 control-label required" htmlFor="blog_post_body">Body</label>
                        <div className="col-sm-10">
                            <input type="text"
                                   id="blog_post_body"
                                   required="required"
                                   className="form-control"/>
                        </div>
                    </div>
                    <div className="form-group">
                        <div className="col-sm-2"></div>
                        <div className="col-sm-10">
                            <button type="submit"
                                    id="blog_post_submit"
                                    className="btn-default btn">
                                Submit
                            </button>
                        </div>
                    </div>
                </div>
            </form>
        );
    }
}

export default Form;

Ok, a few changes here.

The easiest one - we removed the <div> wrapper. As React can only render out one top level element per render function, and all this component containing is a form, we can remove the div nest and just return the form instead.

Next, we've switched to React.createClass, rather than extending component. To the very best of my knowledge, there is no real difference between createClass and extends Component, but sometimes you see one or the other, and it's helpful to know about both.

For more on this, I recommend reading this very information blog post by Todd Motto.

The idea here is that we are going to pass in the real implementation to be called on submit. But we still need to handle the initial triggering of the submit from this component / class.

As such the process will be:

  • Define an onSubmit function inside our Create component
  • Create component renders a new Form component, passing in the onSubmit function as a prop
  • Form component has its own internal handleSubmit method which actually calls the onSubmit method

It's a little confusing, I admit, but it makes sense the more you use it.

Another reason for doing it this way is that using the SyntheticElement, we can call e.preventDefault();, which stops the annoying 'bug' where the browser jumps back to the top of the page on form submit.

We must now update our Create component to use (import) this Form, and also pass in the 'real' implementation of our submission handler to the Form via props:

// /src/containers/create.js

import React, { Component } from 'react';
import Form from '../../components/form';

export default class Create extends Component {

    handleSubmit(data) {
        console.log('form submission data', data);
    }

    render() {
        return (
            <div>
                <Form onSubmit={this.handleSubmit}></Form>
            </div>
        );
    }
}

Even so, we still won't be getting sent the expected data when the form is submitted. Let's fix that.

Form State

Dealing with forms is hard. I'd generally recommend you don't bother doing this manually on anything but the most trivial of applications. Instead, I'd recommend using a dedicated form library such as React Forms.

However, we are doing this manually, so we need to start keeping track of our form's state.

import React, { Component } from 'react';

const Form = React.createClass({

    getInitialState() {
        return {
            body: this.props.body || 'some body',
            title: this.props.title || 'some title'
        }
    },

    handleBodyChange(e) {
        this.setState({
            body: e.target.value
        });
    },

    handleTitleChange(e) {
        this.setState({
            title: e.target.value
        });
    },

    handleSubmit(e) {
        e.preventDefault();
        this.props.onSubmit(this.state);
    },

    render() {
        return (
            <form name="blog_post" className="form-horizontal" onSubmit={this.handleSubmit}>
                <div id="blog_post">
                    <div className="form-group">
                        <label className="col-sm-2 control-label required" htmlFor="blog_post_title">Title</label>
                        <div className="col-sm-10">
                            <input type="text"
                                   id="blog_post_title"
                                   required="required"
                                   value={this.state.title}
                                   onChange={this.handleTitleChange}
                                   className="form-control"/>
                        </div>
                    </div>
                    <div className="form-group">
                        <label className="col-sm-2 control-label required" htmlFor="blog_post_body">Body</label>
                        <div className="col-sm-10">
                            <input type="text"
                                   id="blog_post_body"
                                   required="required"
                                   value={this.state.body}
                                   onChange={this.handleBodyChange}
                                   className="form-control"/>
                        </div>
                    </div>
                    <div className="form-group">
                        <div className="col-sm-2"></div>
                        <div className="col-sm-10">
                            <button type="submit"
                                    id="blog_post_submit"
                                    className="btn-default btn">
                                Submit
                            </button>
                        </div>
                    </div>
                </div>
            </form>
        );
    }
});

export default Form;

By setting the initial state we can make sure we definitely have 'something' in the component's state when the component first loads. If we don't provide any values (by way of props) then we set some sensible defaults. However, we will have some values when we come to Update, so this is a little foresight on our part.

Also, here I have added an onChange listener to both of the input fields, and also set their respective values to the current state, e.g.:

<input type="text"
       id="blog_post_body"
       required="required"
       value={this.state.body}
       onChange={this.handleBodyChange}
       className="form-control"/>

Now, whenever a user types in some data into the form field, it will automatically call the defined function - handleBodyChange in this case:

    handleBodyChange(e) {
        this.setState({
            body: e.target.value
        });
    },

Which simply updates the state for the current field.

Now, when the user clicks on the submit button, the state is passed back through:

    handleSubmit(e) {
        e.preventDefault();
        this.props.onSubmit(this.state);
    },

Which should output the JavaScript object in the console log output. Phew, we are nearly there.

Although this is quite a lot of indirection, it is also much more modular than our equivalent Angular code.

Submitting Data To The API

The next problem is that not only do we not have a function to actually POST data to our API, but that we are also sending data in the form of a JavaScript object, rather than JSON which our API expects.

// /src/actions/blogPostActions.js

import fetch from 'isomorphic-fetch';

// * snip *

export function createBlogPost(data) {
    return fetch('http://api.symfony-3.dev/app_dev.php/posts', {
        method: 'POST',
        mode: 'CORS',
        body: JSON.stringify(data),
        headers: {
            'Content-Type': 'application/json'
        }
    }).then(res => {
        return res;
    }).catch(err => err);
}

Adding the createBlogPost function in is straightforward enough.

We need to make sure we convert the given object to JSON by way of JSON.stringify(data), and also to set the Content-Type to application/json.

Aside from that, this function is largely similar to the GET function we created earlier.

To tie this all together we need a way to actually use this new createBlogPost action when the submit button is pressed:

// /src/containers/create.js

import React, { Component } from 'react';
import Form from '../../components/form';
import { createBlogPost } from '../actions/blogPostActions';

export default class Create extends Component {

    handleSubmit(data) {
        createBlogPost(data);
    }

    render() {
        return (
            <div>
                <Form onSubmit={this.handleSubmit}></Form>
            </div>
        );
    }
}

And that's that.

At this stage we have the working basis of our application. Adding in the Update functionality is a derivation of what we have already done. Deleting is different entirely.

So long as you are at least somewhat comfortable with what you see here, the rest should come together nicely. If not, I strongly suggest checking out the code and playing around with it. And as always, feel free to ask any questions, or provide feedback, suggestions, and improvements :)

Code For This Video

Get the code for this video.

Episodes