JavaScript - - By Michael Wanyoike

Build a CRUD App Using React, Redux and FeathersJS

Building a modern project requires splitting the logic into front-end and back-end code. The reason behind this move is to promote code re-usability. For example, we may need to build a native mobile application that accesses the back-end API. Or we may be developing a module that will be part of a large modular platform.

The popular way of building a server-side API is to use a library like Express or Restify. These libraries make creating RESTful routes easy. The problem with these libraries is that we’ll find ourselves writing a ton of repeating code. We’ll also need to write code for authorization and other middleware logic.

To escape this dilemma, we can use a framework like Loopback or Feathers to help us generate an API.

At the time of writing, Loopback has more GitHub stars and downloads than Feathers. Loopback is a great library for generating RESTful CRUD endpoints in a short period of time. However, it does have a slight learning curve and the documentation is not easy to get along with. It has stringent framework requirements. For example, all models must inherit one of its built-in model class. If you need real-time capabilities in Loopback, be prepared to do some additional coding to make it work.

An operator, sat at an old-fashioned telephone switchboard - Build a CRUD App Using React, Redux and FeathersJS

FeathersJS, on the other hand, is much easier to get started with and has realtime support built-in. Quite recently, the Auk version was released (because Feathers is so modular, they use bird names for version names) which introduced a vast number of changes and improvements in a number of areas. According to a post they published on their blog, they are now the 4th most popular real-time web framework. It has excellent documentation, and they’ve covered pretty much any area we can think of on building a real-time API.

What makes Feathers amazing is its simplicity. The entire framework is modular and we only need to install the features we need. Feathers itself is a thin wrapper built on top of Express, where they’ve added new features — services and hooks. Feathers also allows us to effortlessly send and receive data over WebSockets.

Prerequisites

Before you get started with the tutorial, you’ll need to have a solid foundation in the following topics:

On your machine, you’ll need to have installed recent versions of:

  • NodeJS 6+
  • Mongodb 3.4+
  • Yarn package manager (optional)
  • Chrome browser

If you’ve never written a database API in JavaScript before, I’d recommend first taking a look at this tutorial on creating RESTful APIs.

Recommended Courses

Wes Bos
A step-by-step training course to get you building real world React.js + Firebase apps and website components in a couple of afternoons. Use coupon code 'SITEPOINT' at checkout to get 25% off.

Scaffold the App

We’e going to build a CRUD contact manager application using React, Redux, Feathers and MongoDB. You can take a look at the completed project here.

In this tutorial, I’ll show you how to build the application from the bottom up. We’ll kick-start our project using the create-react-app tool.

# scaffold a new react project
create-react-app react-contact-manager
cd react-contact-manager

# delete unnecessary files
rm src/logo.svg src/App.css

Use your favorite code editor and remove all the content in index.css. Open App.js and rewrite the code like this:

import React, { Component } from 'react';

class App extends Component {
  render() {
    return (
      <div>
        <h1>Contact Manager</h1>
      </div>
    );
  }
}

export default App;

Make sure to run yarn start to ensure the project is running as expected. Check the console tab to ensure that our project is running cleanly with no warnings or errors. If everything is running smoothly, use Ctrl+C to stop the server.

Build the API Server with Feathers

Let’s proceed with generating the back-end API for our CRUD project using the feathers-cli tool.

# Install Feathers command-line tool
npm install -g feathers-cli

# Create directory for the back-end code
mkdir backend
cd backend

# Generate a feathers back-end API server
feathers generate app

? Project name | backend
? Description | contacts API server
? What folder should the source files live in? | src
? Which package manager are you using (has to be installed globally)? | Yarn
? What type of API are you making? | REST, Realtime via Socket.io

# Generate RESTful routes for Contact Model
feathers generate service

? What kind of service is it? | Mongoose
? What is the name of the service? | contact
? Which path should the service be registered on? | /contacts
? What is the database connection string? | mongodb://localhost:27017/backend


# Install email field type
yarn add mongoose-type-email

# Install the nodemon package
yarn add nodemon --dev

Open backend/package.json and update the start script to use nodemon so that the API server will restart automatically whenever we make changes.

// backend/package.json

…
"scripts": {
  ...
  "start": "nodemon src/",
  …
},
…

Let’s open backend/config/default.json. This is where we can configure MongoDB connection parameters and other settings. I’ve also increased the default paginate value to 50, since in this tutorial we won’t write front-end logic to deal with pagination.

{
  "host": "localhost",
  "port": 3030,
  "public": "../public/",
  "paginate": {
    "default": 50,
    "max": 50
  },
  "mongodb": "mongodb://localhost:27017/backend"
}

Open backend/src/models/contact.model.js and update the code as follows:

// backend/src/models/contact.model.js

require('mongoose-type-email');

module.exports = function (app) {
  const mongooseClient = app.get('mongooseClient');
  const contact = new mongooseClient.Schema({
    name : {
      first: {
        type: String,
        required: [true, 'First Name is required']
      },
      last: {
        type: String,
        required: false
      }
    },
    email : {
      type: mongooseClient.SchemaTypes.Email,
      required: [true, 'Email is required']
    },
    phone : {
      type: String,
      required: [true, 'Phone is required'],
      validate: {
        validator: function(v) {
          return /^\+(?:[0-9] ?){6,14}[0-9]$/.test(v);
        },
        message: '{VALUE} is not a valid international phone number!'
      }
    },
    createdAt: { type: Date, 'default': Date.now },
    updatedAt: { type: Date, 'default': Date.now }
  });

  return mongooseClient.model('contact', contact);
};

In addition to generating the contact service, Feathers has also generated a test case for us. We need to fix the service name first for it to pass:

// backend/test/services/contact.test.js

const assert = require('assert');
const app = require('../../src/app');

describe('\'contact\' service', () => {
  it('registered the service', () => {
    const service = app.service('contacts'); // change contact to contacts

    assert.ok(service, 'Registered the service');
  });
});

Open a new terminal and inside the backend directory, execute yarn test. You should have all the tests running successfully. Go ahead and execute yarn start to start the backend server. Once the server has finished starting it should print the line: 'Feathers application started on localhost:3030'.

Launch your browser and access the url: http://localhost:3030/contacts. You should expect to receive the following JSON response:

{"total":0,"limit":50,"skip":0,"data":[]}

Now let’s use Postman to confirm all CRUD restful routes are working. You can launch Postman using this button:

Run in Postman

If you’re new to Postman, check out this tutorial. When you hit the SEND button, you should get your data back as the response along with three additional fields — _id, createdAt and updatedAt.

Use the following JSON data to make a POST request using Postman. Paste this in the body and set content-type to application/json:

{
  "name": {
    "first": "Tony",
    "last": "Stark"
  },
  "phone": "+18138683770",
  "email": "tony@starkenterprises.com"
}

Build the UI

Let’s start by installing the necessary front-end dependencies. We’ll use semantic-ui css/semantic-ui react to style our pages and react-router-dom to handle route navigation.

Important: Make sure you are installing outside the backend directory

// Install semantic-ui
yarn add semantic-ui-css semantic-ui-react

// Install react-router
yarn add react-router-dom

Update the project structure by adding the following directories and files:

|-- react-contact-manager
    |-- backend
    |-- node_modules
    |-- public
    |-- src
        |-- App.js
        |-- App.test.js
        |-- index.css
        |-- index.js
        |-- components
        |   |-- contact-form.js #(new)
        |   |-- contact-list.js #(new)
        |-- pages
            |-- contact-form-page.js #(new)
            |-- contact-list-page.js #(new)

Let’s quickly populate the JS files with some placeholder code.

For the component contact-list.js, we’ll write it in this syntax since it will be a purely presentational component.

// src/components/contact-list.js

import React from 'react';

export default function ContactList(){
  return (
    <div>
      <p>No contacts here</p>
    </div>
  )
}

For the top-level containers, I use pages. Let’s provide some code for the contact-list-page.js

// src/pages/contact-list-page.js

import React, { Component} from 'react';
import ContactList from '../components/contact-list';

class ContactListPage extends Component {
  render() {
    return (
      <div>
        <h1>List of Contacts</h1>
        <ContactList/>
      </div>
    )
  }
}

export default ContactListPage;

For the contact-form component, it needs to be smart, since it’s required to manage its own state, specifically form fields. For now, we’ll place this placeholder code.

// src/components/contact-form.js
import React, { Component } from 'react';

class ContactForm extends Component {
  render() {
    return (
      <div>
        <p>Form under construction</p>
      </div>
    )
  }
}

export default ContactForm;

Populate the contact-form-page with this code:

// src/pages/contact-form-page.js

import React, { Component} from 'react';
import ContactForm from '../components/contact-form';

class ContactFormPage extends Component {
  render() {
    return (
      <div>
        <ContactForm/>
      </div>
    )
  }
}

export default ContactFormPage;

Now, let’s create the navigation menu and define the routes for our App. App.js is often referred to as the ‘layout template’ for the Single Page Application.

// src/App.js

import React, { Component } from 'react';
import { NavLink, Route } from 'react-router-dom';
import { Container } from 'semantic-ui-react';
import ContactListPage from './pages/contact-list-page';
import ContactFormPage from './pages/contact-form-page';

class App extends Component {
  render() {
    return (
      <Container>
        <div className="ui two item menu">
          <NavLink className="item" activeClassName="active" exact to="/">
            Contacts List
          </NavLink>
          <NavLink className="item" activeClassName="active" exact to="/contacts/new">
            Add Contact
          </NavLink>
        </div>
        <Route exact path="/" component={ContactListPage}/>
        <Route path="/contacts/new" component={ContactFormPage}/>
        <Route path="/contacts/edit/:_id" component={ContactFormPage}/>
      </Container>
    );
  }
}

export default App;

Finally, update the index.js file with this code where we import semantic-ui CSS for styling and BrowserRouter for using the HTML5 history API that keeps our app in sync with the URL.

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import 'semantic-ui-css/semantic.min.css';
import './index.css';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

Go back to the terminal and execute yarn start. You should have a similar view to the screenshot below:

Screenshot of the empty list of contacts

Manage React State with Redux

Stop the server with ctrl+c and install the following packages using yarn package manager:

yarn add redux react-redux redux-promise-middleware redux-thunk redux-devtools-extension axios

Phew! That’s a whole bunch of packages for setting up Redux. I assume you are already familiar with Redux if you’re reading this tutorial. Redux-thunk allows writing action creators as async functions while redux-promise-middleware reduces some Redux boilerplate code for us by handling dispatching of pending, fulfilled, and rejected actions on our behalf.

Feathers does include a light-weight client package that helps communicate with the API, but it’s also really easy to use other client packages. For this tutorial, we’ll use the Axios HTTP client.

The redux-devtools-extension an amazing tool that keeps track of dispatched actions and state changes. You’ll need to install its chrome extension for it to work.

Chrome Redux Dev Tool

Next, let’s setup our Redux directory structure as follows:

|-- react-contact-manager
    |-- backend
    |-- node_modules
    |-- public
    |-- src
        |-- App.js
        |-- App.test.js
        |-- index.css
        |-- index.js
        |-- contact-data.js #new
        |-- store.js #new
        |-- actions #new
            |-- contact-actions.js #new
            |-- index.js #new
        |-- components
        |-- pages
        |-- reducers #new
            |-- contact-reducer.js #new
            |-- index.js #new

Let’s start by populating contacts-data.js with some test data:

// src/contact-data.js

export const contacts = [
  {
    _id: "1",
    name: {
      first:"John",
      last:"Doe"
    },
    phone:"555",
    email:"john@gmail.com"
  },
  {
    _id: "2",
    name: {
      first:"Bruce",
      last:"Wayne"
    },
    phone:"777",
    email:"bruce.wayne@gmail.com"
  }
];

Define contact-actions.js with the following code. For now, we’ll fetch data from the contacts-data.js file.

// src/actions/contact-actions.js

import { contacts } from '../contacts-data';

export function fetchContacts(){
  return dispatch => {
    dispatch({
      type: 'FETCH_CONTACTS',
      payload: contacts
    })
  }
}

In contact-reducer.js, let’s write our handler for the 'FETCH_CONTACT' action. We’ll store the contacts data in an array called 'contacts'.

// src/reducers/contact-reducer.js

const defaultState = {
  contacts: []
}

export default (state=defaultState, action={}) => {
  switch (action.type) {
    case 'FETCH_CONTACTS': {
      return {
        ...state,
        contacts: action.payload
      }
    }
    default:
      return state;
  }
}

In reducers/index.js, we’ll combine all reducers here for easy export to our Redux store.

// src/reducers/index.js

import { combineReducers } from 'redux';
import ContactReducer from './contact-reducer';

const reducers = {
  contactStore: ContactReducer
}

const rootReducer = combineReducers(reducers);

export default rootReducer;

In store.js, we’ll import the necessary dependencies to construct our Redux store. We’ll also set up the redux-devtools-extension here to enable us to monitor the Redux store using the Chrome extension.

// src/store.js

import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from "./reducers";

const middleware = composeWithDevTools(applyMiddleware(promise(), thunk));

export default createStore(rootReducer, middleware);

Open index.js and update the render method where we inject the store using Redux’s Provider class.

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from "./store"
import 'semantic-ui-css/semantic.min.css';
import './index.css';

ReactDOM.render(
  <BrowserRouter>
    <Provider store={store}>
      <App />
    </Provider>
  </BrowserRouter>,
  document.getElementById('root')
);

Let’s run yarn start to make sure everything is running so far.

Next, we’ll connect our component contact-list with the Redux store we just created. Open contact-list-page and update the code as follows:

// src/pages/contact-list-page

import React, { Component} from 'react';
import { connect } from 'react-redux';
import ContactList from '../components/contact-list';
import { fetchContacts } from '../actions/contact-actions';

class ContactListPage extends Component {

  componentDidMount() {
    this.props.fetchContacts();
  }

  render() {
    return (
      <div>
        <h1>List of Contacts</h1>
        <ContactList contacts={this.props.contacts}/>
      </div>
    )
  }
}

// Make contacts  array available in  props
function mapStateToProps(state) {
  return {
      contacts : state.contactStore.contacts
  }
}

export default connect(mapStateToProps, {fetchContacts})(ContactListPage);

We’ve made the contacts array in store and the fetchContacts function available to ContactListPage component via this.props variable. We can now pass the contacts array down to the ContactList component.

For now, let’s update the code such that we can display a list of contacts.

// src/components/contact-list

import React from 'react';

export default function ContactList({contacts}){

  const list = () => {
    return contacts.map(contact => {
      return (
        <li key={contact._id}>{contact.name.first} {contact.name.last}</li>
      )
    })
  }

  return (
    <div>
      <ul>
        { list() }
      </ul>
    </div>
  )
}

If you go back to the browser, you should have something like this:

Screenshot of the contact list showing two contacts

Let’s make the list UI look more attractive by using semantic-ui’s Card component. In the components folder, create a new file contact-card.js and paste this code:

// src/components/contact-card.js

import React from 'react';
import { Card, Button, Icon } from 'semantic-ui-react'

export default function ContactCard({contact, deleteContact}) {
  return (
    <Card>
      <Card.Content>
        <Card.Header>
          <Icon name='user outline'/> {contact.name.first} {contact.name.last}
        </Card.Header>
        <Card.Description>
          <p><Icon name='phone'/> {contact.phone}</p>
          <p><Icon name='mail outline'/> {contact.email}</p>
        </Card.Description>
      </Card.Content>
      <Card.Content extra>
        <div className="ui two buttons">
          <Button basic color="green">Edit</Button>
          <Button basic color="red">Delete</Button>
        </div>
      </Card.Content>
    </Card>
  )
}

ContactCard.propTypes = {
  contact: React.PropTypes.object.isRequired
}

Update contact-list component to use the new ContactCard component

// src/components/contact-list.js

import React from 'react';
import { Card } from 'semantic-ui-react';
import ContactCard from './contact-card';

export default function ContactList({contacts}){

  const cards = () => {
    return contacts.map(contact => {
      return (
        <ContactCard key={contact._id} contact={contact}/>
      )
    })
  }

  return (
    <Card.Group>
      { cards() }
    </Card.Group>
  )
}

The list page should now look like this:

The two contacts rendered with the semantic-ui styles

Server-side Validation with Redux-Form

Now that we know the Redux store is properly linked up with the React components, we can now make a real fetch request to the database and use the data populate our contact list page. There are several ways to do this, but the way I’ll show is surprisingly simple.

First, we need to configure an Axios client that can connect to the back-end server.

// src/actions/index.js
import axios from "axios";

export const client = axios.create({
  baseURL: "http://localhost:3030",
  headers: {
    "Content-Type": "application/json"
  }
})

Next, we’ll update the contact-actions.js code to fetch contacts from the database via a GET request using the Axios client.

// src/actions/contact-actions.js

import { client } from './';

const url = '/contacts';

export function fetchContacts(){
  return dispatch => {
    dispatch({
      type: 'FETCH_CONTACTS',
      payload: client.get(url)
    })
  }
}

Update contact-reducer.js as well since the action and the payload being dispatched is now different.

// src/reducers/contact-reducer.js

…
    case "FETCH_CONTACTS_FULFILLED": {
      return {
        ...state,
        contacts: action.payload.data.data || action.payload.data // in case pagination is disabled
      }
    }
…

After saving, refresh your browser, and ensure the back-end server is running at localhost:3030. The contact list page should now be displaying data from the database.

Handle Create and Update Requests using Redux-Form

Next, let’s look at how to add new contacts, and to do that we need forms. At first, building a form looks quite easy. But when we start thinking about client-side validation and controlling when errors should be displayed, it becomes tricky. In addition, the back-end server does its own validation, which we also need to display its errors on the form.

Rather than implement all the form functionality ourselves, we’ll enlist the help of a library called Redux-Form. We’ll also use a nifty package called Classnames that will help us highlight fields with validation errors.

We need to stop the server with ctrl+c before installing the following packages:

yarn add redux-form classnames

We can now start the server after the packages have finished installing.

Let’s first quickly add this css class to the index.css file to style the form errors:

/* src/index.css */

.error {
  color: #9f3a38;
}

Then let’s add redux-form’s reducer to the combineReducers function in reducers/index.js

// src/reducers/index.js

…
import { reducer as formReducer } from 'redux-form';

const reducers = {
  contactStore: ContactReducer,
  form: formReducer
}
…

Next, open contact-form.js and build the form UI with this code:

// src/components/contact-form

import React, { Component } from 'react';
import { Form, Grid, Button } from 'semantic-ui-react';
import { Field, reduxForm } from 'redux-form';
import classnames from 'classnames';

class ContactForm extends Component {

  renderField = ({ input, label, type, meta: { touched, error } }) => (
    <Form.Field className={classnames({error:touched && error})}>
      <label>{label}</label>
      <input {...input} placeholder={label} type={type}/>
      {touched && error && <span className="error">{error.message}</span>}
    </Form.Field>
  )

  render() {
    const { handleSubmit, pristine, submitting, loading } = this.props;

    return (
      <Grid centered columns={2}>
        <Grid.Column>
          <h1 style={{marginTop:"1em"}}>Add New Contact</h1>
          <Form onSubmit={handleSubmit} loading={loading}>
            <Form.Group widths='equal'>
              <Field name="name.first" type="text" component={this.renderField} label="First Name"/>
              <Field name="name.last" type="text" component={this.renderField} label="Last Name"/>
            </Form.Group>
            <Field name="phone" type="text" component={this.renderField} label="Phone"/>
            <Field name="email" type="text" component={this.renderField} label="Email"/>
            <Button primary type='submit' disabled={pristine || submitting}>Save</Button>
          </Form>
        </Grid.Column>
      </Grid>
    )
  }
}

export default reduxForm({form: 'contact'})(ContactForm);

Take the time to examine the code; there’s a lot going on in there. See the reference guide to understand how redux-form works. Also, take a look at semantic-ui-react documentation and read about its elements to understand how they are used in this context.

Next, we’ll define the actions necessary for adding a new contact to the database. The first action will provide a new contact object to the Redux form. While the second action will post the contact data to the API server.

Append the following code to contact-actions.js

// src/actions/contact-actions.js

…

export function newContact() {
  return dispatch => {
    dispatch({
      type: 'NEW_CONTACT'
    })
  }
}

export function saveContact(contact) {
  return dispatch => {
    return dispatch({
      type: 'SAVE_CONTACT',
      payload: client.post(url, contact)
    })
  }
}

In the contact-reducer, we need to handle actions for 'NEW_CONTACT', 'SAVE_CONTACT_PENDING', 'SAVE_CONTACT_FULFILLED', and 'SAVE_CONTACT_REJECTED'. We need to declare the following variables:

  • contact – initialize empty object
  • loading – update ui with progress info
  • errors – store server validation errors in case something goes wrong

Add this code inside contact-reducer‘s switch statement:

// src/reducers/contact-reducer.js

…
const defaultState = {
  contacts: [],
  contact: {name:{}},
  loading: false,
  errors: {}
}
…
case 'NEW_CONTACT': {
      return {
        ...state,
        contact: {name:{}}
      }
    }

    case 'SAVE_CONTACT_PENDING': {
      return {
        ...state,
        loading: true
      }
    }

    case 'SAVE_CONTACT_FULFILLED': {
      return {
        ...state,
        contacts: [...state.contacts, action.payload.data],
        errors: {},
        loading: false
      }
    }

    case 'SAVE_CONTACT_REJECTED': {
      const data = action.payload.response.data;
      // convert feathers error formatting to match client-side error formatting
      const { "name.first":first, "name.last":last, phone, email } = data.errors;
      const errors = { global: data.message, name: { first,last }, phone, email };
      return {
        ...state,
        errors: errors,
        loading: false
      }
    }
  …

Open contact-form-page.js and update the code as follows:

// src/pages/contact-form-page

import React, { Component} from 'react';
import { Redirect } from 'react-router';
import { SubmissionError } from 'redux-form';
import { connect } from 'react-redux';
import { newContact, saveContact } from '../actions/contact-actions';
import ContactForm from '../components/contact-form';


class ContactFormPage extends Component {

  state = {
    redirect: false
  }

  componentDidMount() {
    this.props.newContact();
  }

  submit = (contact) => {
    return this.props.saveContact(contact)
      .then(response => this.setState({ redirect:true }))
      .catch(err => {
         throw new SubmissionError(this.props.errors)
       })
  }

  render() {
    return (
      <div>
        {
          this.state.redirect ?
          <Redirect to="/" /> :
          <ContactForm contact={this.props.contact} loading={this.props.loading} onSubmit={this.submit} />
        }
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    contact: state.contactStore.contact,
    errors: state.contactStore.errors
  }
}

export default connect(mapStateToProps, {newContact, saveContact})(ContactFormPage);

Let’s now go back to the browser and try to intentionally save an incomplete form

New contact form showing validation errors

As you can see, server-side validation prevents us from saving an incomplete contact. We’re using the SubmissionError class to pass this.props.errors to the form, just in case you’re wondering.

Now, finish filling in the form completely. After clicking save, we should be directed to the list page.

Close-up of the contact cards

Client-side Validation with Redux Form

Let’s take a look at how client-side validation can be implemented. Open contact-form and paste this code outside the ContactForm class. Also, update the default export as shown:

// src/components/contact-form.js

…
const validate = (values) => {
  const errors = {name:{}};
  if(!values.name || !values.name.first) {
    errors.name.first = {
      message: 'You need to provide First Name'
    }
  }
  if(!values.phone) {
    errors.phone = {
      message: 'You need to provide a Phone number'
    }
  } else if(!/^\+(?:[0-9] ?){6,14}[0-9]$/.test(values.phone)) {
    errors.phone = {
      message: 'Phone number must be in International format'
    }
  }
  if(!values.email) {
    errors.email = {
      message: 'You need to provide an Email address'
    }
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
    errors.email = {
      message: 'Invalid email address'
    }
  }
  return errors;
}
…

export default reduxForm({form: 'contact', validate})(ContactForm);

After saving the file, go back to the browser and try adding invalid data. This time, the client side validation blocks submitting of data to the server.

Client-side validation errors

Now, go ahead and input valid data. We should have at least three new contacts by now.

contact list with three contact cards

Implement Contact Updates

Now that we can add new contacts, let’s see how we can update existing contacts. We’ll start with the contact-actions.js file, where we need to define two actions — one for fetching a single contact, and another for updating the contact.

// src/actions/contact-actions.js

…
export function fetchContact(_id) {
  return dispatch => {
    return dispatch({
      type: 'FETCH_CONTACT',
      payload: client.get(`${url}/${_id}`)
    })
  }
}

export function updateContact(contact) {
  return dispatch => {
    return dispatch({
      type: 'UPDATE_CONTACT',
      payload: client.put(`${url}/${contact._id}`, contact)
    })
  }
}

Let’s add the following cases to contact-reducer to update state when a contact is being fetched from the database and when it’s being updated.

// src/reducers/contact-reducer.js

…
case 'FETCH_CONTACT_PENDING': {
  return {
    ...state,
    loading: true,
    contact: {name:{}}
  }
}

case 'FETCH_CONTACT_FULFILLED': {
  return {
    ...state,
    contact: action.payload.data,
    errors: {},
    loading: false
  }
}

case 'UPDATE_CONTACT_PENDING': {
  return {
    ...state,
    loading: true
  }
}

case 'UPDATE_CONTACT_FULFILLED': {
  const contact = action.payload.data;
  return {
    ...state,
    contacts: state.contacts.map(item => item._id === contact._id ? contact : item),
    errors: {},
    loading: false
  }
}

case 'UPDATE_CONTACT_REJECTED': {
  const data = action.payload.response.data;
  const { "name.first":first, "name.last":last, phone, email } = data.errors;
  const errors = { global: data.message, name: { first,last }, phone, email };
  return {
    ...state,
    errors: errors,
    loading: false
  }
}
…

Next, let’s pass the new fetch and save actions to the contact-form-page.js. We’ll also change the componentDidMount() and submit() logic to handle both create and update scenarios. Be sure to update each section of code as indicated below.

// src/pages/contact-form-page.js

…
import { newContact, saveContact, fetchContact, updateContact } from '../actions/contact-actions';

…

componentDidMount = () => {
  const { _id } = this.props.match.params;
  if(_id){
    this.props.fetchContact(_id)
  } else {
    this.props.newContact();
  }
}

submit = (contact) => {
  if(!contact._id) {
    return this.props.saveContact(contact)
      .then(response => this.setState({ redirect:true }))
      .catch(err => {
         throw new SubmissionError(this.props.errors)
       })
  } else {
    return this.props.updateContact(contact)
      .then(response => this.setState({ redirect:true }))
      .catch(err => {
         throw new SubmissionError(this.props.errors)
       })
  }
}

…

export default connect(
  mapStateToProps, {newContact, saveContact, fetchContact, updateContact})(ContactFormPage);

We’ll enable contact-form to asynchronously receive data from the fetchContact() action. To populate a Redux Form, we use its initialize function that’s been made available to us via the props. We’ll also update the page title with a script to reflect whether we are editing or adding new a contact.

// src/components/contact-form.js

…
componentWillReceiveProps = (nextProps) => { // Receive Contact data Asynchronously
  const { contact } = nextProps;
  if(contact._id !== this.props.contact._id) { // Initialize form only once
    this.props.initialize(contact)
  }
}
…

  <h1 style={{marginTop:"1em"}}>{this.props.contact._id ? 'Edit Contact' : 'Add New Contact'}</h1>

…

Now, let’s convert the Edit button in contact-card.js to a link that will direct the user to the form.

// src/components/contact-card.js

…
import { Link } from 'react-router-dom';

…
  <div className="ui two buttons">
    <Link to={`/contacts/edit/${contact._id}`} className="ui basic button green">Edit</Link>
    <Button basic color="red">Delete</Button>
  </div>
…

Once the list page has finished refreshing, choose any contact and hit the Edit button.

Edit form displaying an existing contact

Finish making your changes and hit save.

List of edited contacts

By now, your application should be able to allow users to add new contacts and update existing ones.

Implement Delete Request

Let’s now look at the final CRUD operation: delete. This one is much simpler to code. We start at the contact-actions.js file.

// src/actions/contact-actions.js

…
export function deleteContact(_id) {
  return dispatch => {
    return dispatch({
      type: 'DELETE_CONTACT',
      payload: client.delete(`${url}/${_id}`)
    })
  }
}

By now, you should have gotten the drill. Define a case for the deleteContact() action in contact-reducer.js.

// src/reducers/contact-reducer.js

…
case 'DELETE_CONTACT_FULFILLED': {
    const _id = action.payload.data._id;
    return {
      ...state,
      contacts: state.contacts.filter(item => item._id !== _id)
    }
  }
…

Next, we import the deleteContact() action to contact-list-page.js and pass it to the ContactList component.

// src/pages/contact-list-page.js

…
import { fetchContacts, deleteContact } from '../actions/contact-actions';
…

<ContactList contacts={this.props.contacts} deleteContact={this.props.deleteContact}/>

…

export default connect(mapStateToProps, {fetchContacts, deleteContact})(ContactListPage);

The ContactList component, in turn, passes the deleteContact() action to the ContactCard component

// src/components/contact-list.js

…
export default function ContactList({contacts, deleteContact}){ // replace this line

const cards = () => {
  return contacts.map(contact => {
    return (
      <ContactCard
      key={contact._id}
      contact={contact}
      deleteContact={deleteContact} /> // and this one
    )
  })
}
…

Finally, we update Delete button in ContactCard to execute the deleteContact() action, via the onClick attribute.

// src/components/contact-card.js

…
<Button basic color="red" onClick={() => deleteContact(contact._id)} >Delete</Button>
…

Wait for the browser to refresh, then try to delete one or more contacts. The delete button should work as expected.

As a challenge, try to modify the delete button’s onClick handler so that it asks the user to confirm or cancel the delete action. Paste your solution in the comments below.

Conclusion

By now, you should have learned the basics of creating a CRUD web app in JavaScript. It may seem we’ve written quite a lot of code to manage only one model. We could have done less work if we had used an MVC framework. The problem with these frameworks is that they become harder to maintain as the code grows.

A Flux-based framework, such as Redux, allows us to build large complex projects that are easy to manage. If you don’t like the verbose code that Redux requires you to write, then you could also look at Mobx as an alternative.

At least I hope you now have a good impression of FeathersJS. With little effort, we were able to generate a database API with only a few commands and a bit of coding. Although we have only scratched the surface in exploring its capabilities, you will at least agree with me that it is a robust solution for creating APIs.

This article was peer reviewed by Marshall Thompson and Sebastian Seitz. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!