Skip to content

Integrate back end server to React app

Pamela Argentino edited this page Feb 16, 2017 · 11 revisions

Now that we have a full-fledged API and database persistence ready, we need to wire our app to it. In order to keep both the back-end server and the create-react-app server and our API running at the same time, we will install a lib called concurrently in dev via yarn. The reason we're doing it only in dev is because react-scripts won't be needed in production and the app will be served by our Node.js server.

yarn add concurrently --dev

Once concurrently is installed, we will update the start script in our package.json file to enable starting both the front and back end servers at the same time.

nano package.json
...
"scripts": {
  "start": "concurrently \"react-scripts start\" \"node server.js\"",
...

This way, when we run yarn start, we will be booting up both servers at the same time.

Retrieving data from the database

We've now reached a situation where we will need to make asynchronous calls to the server constantly and update the Redux store with data coming from the MongoDB database. However, Redux as it comes out-of-the-box doesn't support asynchronous calls, so we will need to include a Redux middleware to handle such calls. This middleware is called redux-thunk and can be installed via yarn.

yarn add redux-thunk

redux-thunk allows us to return functions as well as objects in our action-creating functions. The functions returned receive a function as an argument, dispatch, that's much like the mapDispatchToProps function we always create when creating stateful components. Once redux-thunk is installed, we need to include it in our index.js file.

nano src/index.js
...
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
...
const store = createStore(reducer, {}, applyMiddleware(ReduxThunk));
...

Now we can call async functions and dispatch actions inside their callbacks by returning a function instead of an object outright. Let's try creating a CONTACT_FETCH action.

nano src/actions/index.js
...
export const contactFetch = () => dispatch => {
  fetch('http://localhost:8080/contacts')
    .then(response => response.json())
    .then(json => {    
      dispatch({
        type: 'CONTACT_FETCH',
        contacts: json
      });
    });
};
nano src/reducers/contactsReducer.js
...
case 'CONTACT_FETCH':
  return {
      ...state,
    contactList: action.contacts
  };
...

Now that we have an action creator and our reducer is ready to populate contactList, it's time to prep ContactList to list from the database. We will use a lifecycle method called componentWillMount to call the contactFetch action and wait for the server to respond. Once it does, ContactList will re-render with the items currently saved to the database.

nano src/containers/ContactList.js
...
import { contactDelete, contactFetch } from '../actions';
...
class ContactList extends Component {
  componentWillMount() {
    this.props.onLoad();
  }
...
const mapDispatchToProps = dispatch => ({
  onClickDelete: index => dispatch(contactDelete(index)),
  onLoad: () => dispatch(contactFetch())
});
...

Run yarn start, then refresh the app. Nothing should show up, because our database is empty... But we have a problem.

CORS (Cross-Origin Resource Sharing) is a security rule that browsers and servers enforce to avoid unauthorized external access. Then you might ask yourself, "but I'm in localhost!" Well, you are, but you have two different servers running in different ports and that violates the same-origin policy. For this, we are going to proxy all local requests to the back-end server.

Proxying to the back-end server

When we create a React app via create-react-app, it's actually wrapping a Webpack module bundler in a black box that handles all the configuration for us so that we don't have to worry about doing too much setup to start developing our app. And, when we run yarn start in dev, the app is actually running on a Webpack dev server with all the configuration necessary, including hot reloading. That's why we have two servers in development, but only one in production. This is also why we had to implement CORS only in development. But CORS isn't actually needed, since Webpack has a feature called server proxying that points all local AJAX requests to the address we provide.

Since Webpack is in a black box, we will provide the server address via package.json, which Webpack reads and fetches information from.

nano package.json
...
"scripts": {
  ...
},
"proxy": "http://localhost:8080"
...

Change the API endpoint to use port 3000:

nano src/actions/index.js
...
export const contactFetch = () => dispatch => {
  fetch('/contacts')
    .then(response => response.json())
    .then(json => {
      dispatch({
        type: 'CONTACT_FETCH',
        contacts: json
      });
    });
};

Run yarn start again and the changes should reflect now.

Now we shouldn't have any issue regarding CORS. Run yarn start again and refresh the app. Still not listing anything, but this time it's legit about the MongoDB collection being empty. Let's add something to it via curl just so that we can see something listing on our screen.

curl -X POST -H "Content-Type: application/json" --data '{"name": "Jules", "email": "jules@example.com", "phone": "11111"}' http://localhost:8080/contacts

Refresh the app and you'll see a Jules on the list with number 1111 and e-mail jules@example.com. Now that we have real persistence, we won't lose data between refreshes like previously through this guide.

Inserting to the database

The other actions are pretty much straightforward and little has to be done when it comes to modifying the reducers, since they're working as expected. We just have to adjust the actions to perform the requests then dispatch the actions when they're done.

nano src/actions/index.js
...
export const contactFetch = () => dispatch => {
  fetch('/contacts')
    .then(response => response.json())
    .then(json => {
      dispatch({
        type: 'CONTACT_FETCH',
        contacts: json
      });
    });
};

export const contactAdd = contact => dispatch => {
  fetch('/contacts', {
    method: "POST",
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(contact)
  })
    .then(response => response.json())
    .then(() => dispatch({
      type: 'CONTACT_ADD',
      contact
    }));
};

export const contactEdit = contact => dispatch => {
  fetch(`/contacts/${contact.id}`, {
    method: "PUT",
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(contact)
  })
    .then(() => dispatch({
      type: 'CONTACT_EDIT',
      contact
    }));
};

export const contactDelete = id => dispatch => {
  fetch(`/contacts/${id}`, {
    method: "DELETE"
  })
    .then(() => dispatch({
      type: 'CONTACT_DELETE',
      id
    }));
};

Let's also refactor some changes to contactsReducer to reduce code repetition (following the DRY principle).

nano src/reducers/contactsReducer.js
const initialState = {
  contactList: []
};

const findContactIndex = (state, id) => state.contactList.findIndex(c => c.id === id);

export const contactsReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'CONTACT_FETCH':
    return {
        ...state,
      contactList: action.contacts
    };
    case 'CONTACT_ADD':
      return {
        ...state,
        contactList: state.contactList.concat(action.contact)
      }
    case 'CONTACT_EDIT': {
      const index = findContactIndex(state, action.contact.id);
      return {
        ...state,
        contactList: [
          ...state.contactList.slice(0, index),
          action.contact,
          ...state.contactList.slice(index + 1)
        ]
      };
    }
    case 'CONTACT_DELETE': {
      const index = findContactIndex(state, action.id);
      return {
        ...state,
        contactList: [
          ...state.contactList.slice(0, index),
          ...state.contactList.slice(index + 1)
        ]
      };
    }
    default:
      return state;
  }
};

Refresh the app and test it to see if the changes reflect to the database.

Also, we don't need uuid anymore, so you can uninstall it at will.

Refactoring ContactList

Now that we're exclusively using MongoDB IDs instead of indexes, we need to refactor ContactList to pass these keys to the deletion button. The reducer will then search for the array index itself.

nano src/containers/ContactList.js
...
<tbody>
{
  this.props.contactList.map(({ id, name, phone, email }, index) => (
    <tr key={index}>
      <td>{name}</td>
      <td>{phone}</td>
      <td>{email}</td>
      <td>
        <Link to={`/edit/${id}`} className="btn btn-primary">Edit</Link>
      </td>
      <td>
        <Button
          buttonType="btn-danger"
          onClick={() => this.props.onClickDelete(id)}
        >
          Delete
        </Button>
      </td>
    </tr>
  ))
}
</tbody>
...
const mapDispatchToProps = dispatch => ({
  onClickDelete: id => dispatch(contactDelete(id)),
  onLoad: () => dispatch(contactFetch())
});
...

Refactoring ContactEdit

Now that we have an actual database to fetch data from, instead of loading up the entire list of contacts to ContactEdit's state and looking for the desired contact on that list, we will fetch the contact directly from the database. For that, we'll fetch the contact directly on ContactEdit and add it to its component state (without Redux), while removing mapStateToProps altogether.

nano src/containers/ContactEdit.js
...
componentWillMount() {
  const { contactId } = this.props.match.params;

  fetch(`/contacts/${contactId}`)
    .then(response => response.json())
    .then(contact => this.setState({ contact }));
}
...
export default connect(null, mapDispatchToProps)(withRouter(ContactEdit));

In ContactForm, we need to enable state reloading, otherwise it won't load the initial values provided to it when ContactEdit re-renders with the fetched contact.

nano src/containers/ContactForm
...
export default reduxForm({
  form: 'contact',
  enableReinitialize: true // This makes the form initialize again when ContactEdit re-renders
})(ContactForm);

Refresh the app and test it. You'll see that the changes are kept parallel between the Redux state and the database.

git add .
git commit -m "Integrate app's front-end to back-end"

Next step: Configure Form Validation

Clone this wiki locally