React - Update (PUT / PATCH)


In this video we are going to implement the Update functionality into our React CRUD implementation. If you have not already done so, I strongly suggest you watch (or at least, read the write up for) the previous video where we covered the Create side of things. This video builds on that content.

The first thing we need that is different to the Create side of things is that to Update, we need an existing ID. We will grab this from the URL, and we do that by using React Router's route params.

This will allow us to define our Update route, like so:

// /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 Update from './containers/update';
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="/posts/update/:postId" component={Update}/>
        <Route path="*" component={NotFoundPage}/>
      </Router>
    );
  }
}

With the new route being:

<Route path="/posts/update/:postId" component={Update}/>

And the interesting part being the :postId, which will then be available to us inside the Route's designated component (our Update component) as:

this.props.params.postId

We will need this again shortly.

We have also stated that we want to render the Update component whenever we hit this /posts/update/:postId route, so let's define that:

// /src/containers/blogPosts/update.js

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

const Update = React.createClass ({

    getInitialState() {
        return {
            blogPost: {}
        };
    },

    componentDidMount() {
        fetchBlogPost(this.props.params.postId)
            .then((data) => {
                this.setState(state => {
                    state.blogPost = data;
                    return state;
                });
            })
            .catch((err) => {
                console.error('err', err);
            });
    },

    handleSubmit(data) {
        updateBlogPost(this.state.blogPost.id, data);
    },

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

export default Update;

Ok, so there's a lot going on here. Let's cover each part individually.

Firstly, we have our import statements. There's a couple of new ones here - fetchBlogPost, and updateBlogPost - both the actions that we expect to need to complete to make this part of the project behave as expected. We will come back to these shortly.

We define some initial state. This is as simple as creating an object with a single key - blogPost, which itself points at an empty object - {}. We already have some config on our form to display some default data if not passed in any data (via props), so the empty object is good enough for the moment.

Here's the relevant part of the Form component for reference:

import React from 'react';

const Form = React.createClass({

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

Populating The Form With Existing Data

As this is an Update, we need a way to grab the existing record in order to re-display it.

We're going to use componentDidMount for this. We've already seen this pattern back in the first video in this React section.

As soon as the component is mounted, it will make a query by trying to fetch a blog post matching the ID in the URL:

fetchBlogPost(this.props.params.postId)

We haven't defined that function, so let's quickly do so now:

/src/actions/blogPostActions.js

// * snip *

export function fetchBlogPost(id) {
    return fetch('http://api.symfony-3.dev/app_dev.php/posts/' + id, {
        method: 'GET',
        mode: 'CORS'
    }).then(res => res.json())
    .catch(err => err);
}

This is very similar to the existing fetchBlogPosts function - except this one is singular, and needs to be passed an id.

We aren't really handling the error / bad path here, other than showing an error in the browser's console log.

Bu assuming the lookup went well, the we want to update the state of the Update component, setting the contents of blogPost to be equal to the returned blog post from the API:

    componentDidMount() {
        fetchBlogPost(this.props.params.postId)
            .then((data) => {
                this.setState(state => {
                    state.blogPost = data;
                    return state;
                });
            })
            .catch((err) => {
                console.error('err', err);
            });
    },

Honestly, this is not the world's greatest implementation. I'd strongly recommend going with a proper form library, but I am by no means a React expert, so use your own judgement here also.

Next, we define our handleSubmit function. This is exactly like in the previous video, and is one of the key benefits of component re-use. Our form implementation is going to stay the same, but we can declare a different implementation for what happens when the format is submitted. Very cool.

    handleSubmit(data) {
        updateBlogPost(this.state.blogPost.id, data);
    },

In this instance, again, we call a new action - which we will define now:

/src/actions/blogPostActions.js

// * snip *

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

Again, very similar to the createBlogPost function we defined in the previous video, except this time we need to pass in an id, and use the PUT (or PATCH) method.

Lastly, we call the render method and pass in the initial values as props:

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

Form Updates

All this is good, but we will hit on a strange head-scratcher (at least, it was to me) in that our Form component won't actually update when it's given some new props.

Which actually kinda makes sense - as we aren't explicitly telling it to update.

For this, we need to use the componentWillReceiveProps method:

// /src/components/form.js

import React, { Component } from 'react';

const Form = React.createClass({

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

    componentWillReceiveProps(props) {
        this.setState(props);
    },

The nice thing here is that the props that this method receives will be in the exact 'shape' that matches our state:

{ title: "some title", body: "some body" }

So we can immediately set this as state. This is likely bad practice, it's probably much better to be explicit.

At this stage we have a working update functionality.

I want to stress - again - that this is not intended to be a tutorial on how to properly use React / React best practice, but instead be about how to communicate with your Symfony 3 API from a React application.

Code For This Video

Get the code for this video.

Episodes