ReactGo – Steps to add a new data set to the store.

I’m documenting this because I want to make sure I catch everything for the eventual yeoman generator I intend to build….

Using my Material-UI modded version of ReactGo boilerplate (commit d5f71395f8eca8a0d9a1be162bbd20674d7cdcdd, Feb 16, 2017)

In order to support multiple datasets being loaded into one page from the router I’ve modded the current base to suit my needs summary of changes are on my version of the repo.  These changes were based on the work of @k1tzu & @StefanWerW

Right now I’ve got it down to a 12 step process… (whew!) lets get into it…

Steps:

Server Side

  1. Create the Model

    server > db > mongo > models > classification.js (new file)
    I usually start by duplicating an existing model and modding it.  All you really need to do is change the const value and the exported model name.  Then define your schema items…  http://mongoosejs.com/docs/guide.html

    import mongoose from 'mongoose';
    
    const ClassificationSchema = new mongoose.Schema({
      id: String,
      name: String,
      date: { type: Date, default: Date.now }
    });
    
    export default mongoose.model('Classification', ClassificationSchema);
  2. Create the Controller

    server > db > mongo > controllers > classification.js (new file)
    Next up we duplicate a controller.  I usually target one that has all the basic CRUD in it then remove what won’t be necessary for this and create the additional custom functions as necessary.  Update the import reference… and the response messages…

    import _ from 'lodash';
    import Classification from '../models/classification';
    
    /**
     * List
    */
    export function all(req, res) {
     Classification.find({}).exec((err, classifications) => {
     if (err) {
     console.log('Error in first query');
     return res.status(500).send('Something went wrong getting the data');
     }
    
     return res.json(classifications);
     });
    }
    
    /**
     * Create
     */
    export function create(req, res) {
     Classification.create(req.body, (err) => {
     if (err) {
     console.log(err);
     return res.status(400).send('Couldn\'t create classification');
     }
    
     return res.status(200).send('Classification created.');
     });
    }
    
    /**
     * Update
     */
    export function update(req, res) {
     const query = { id: req.params.id };
     const omitKeys = ['id', '_id', '_v'];
     const data = _.omit(req.body, omitKeys);
    
     Classification.findOneAndUpdate(query, data, (err) => {
     if (err) {
     console.log(err);
     return res.status(500).send('We failed to save the classification update for some reason');
     }
    
     return res.status(200).send('Updated the classification uccessfully');
     });
    }
    
    /**
     * Delete
     */
    export function remove(req, res) {
     const query = { id: req.params.id };
     Classification.findOneAndRemove(query, (err) => {
     if (err) {
     console.log(err);
     return res.status(500).send('We failed to delete the classification for some reason');
     }
    
     return res.status(200).send('Deleted classification successfully');
     });
    }
    
    export default {
     all,
     create,
     update,
     remove
    };
    
  3. Update the indexes…

    server > db > mongo > models > index.js
    + require(‘./classification’);
    server > db > mongo > controllers >index.js

    import users from './users';
    import analysis from './analysis';
    import trainingItem from './trainingItem';
    import classification from './classification';
    
    export { users, analysis, trainingItem, classification};
    
    export default {
      users,
      analysis,
      trainingItem,
      classification
    };
  4. Create the routes

    server > init > routes.js

      // Add to the constants...
      const classificationController = controllers && controllers.classification;
    
      // To the bottom
      if (classificationController) {
        app.get('/classifications', classificationController.all);
        app.post('/classification/:id', classificationController.create);
        app.put('/classification/:id', classificationController.update);
        app.delete('/classification/:id', classificationController.remove);
      } else {
        console.warn(unsupportedMessage('Classification routes not available'));
      }

     

Client Side

Now that we have our server side configured for the new data set we need to prepare to consume it on the client side.

  1. Set up the reducer

    app > reducers > classification.js (new file)
    I’m basing this off the needs I have for basic crud.

    import { combineReducers } from 'redux';
    import * as types from '../types';
    
    const classification = (
      state = {},
      action
    ) => {
      switch (action.type) {
        case types.CREATE_CLASSIFICATION_REQUEST:
          return {
            id: action.id,
            name: action.name
          };
        case types.UPDATE_CLASSIFICATION:
          if (state.id === action.data.id) {
            return {
              ...state,
              name: action.data.name
            };
          }
          return state;
        default:
          return state;
      }
    };
    
    const classifications = (
      state = [],
      action
    ) => {
      switch (action.type) {
        case types.FETCH_CLASSIFICATION_SUCCESS:
          if (action.data) return action.data;
          return state;
        case types.CREATE_CLASSIFICATION_REQUEST:
          return [...state, classification(undefined, action)];
        case types.CREATE_CLASSIFICATION_SUCCESS:
          // We optimistcally posted the change during the request so this is just a place holder really...
          return state;
        case types.CREATE_CLASSIFICATION_FAILURE:
          return state.filter(ti => ti.id !== action.id);
        case types.UPDATE_CLASSIFICATION:
          return state.map(ti => classification(ti, action));
        case types.DELETE_CLASSIFICATION:
          return state.filter(ti => ti.id !== action.id);
        case types.CLASSIFICATION_FAILURE:
          console.log(action.error, action.id);
          return state;
        default:
          return state;
      }
    };
    const classificationReducer = combineReducers({
      classifications
    });
    
    export default classificationReducer;
    
  2. Add the new reducer to the root reducer

    app > reducers > index.js
    Add an import for the new reducer and add it as a property in the combineReducer function as well.

  3. Register the types we just created

    app > types > index.js

    // Classification actions
    export const FETCH_CLASSIFICATIONS_SUCCESS = 'FETCH_CLASSIFICATIONS_SUCCESS';
    export const CREATE_CLASSIFICATION_REQUEST = 'CREATE_CLASSIFICATION_REQUEST';
    export const CREATE_CLASSIFICATION_SUCCESS = 'CREATE_CLASSIFICATION_SUCCESS';
    export const CREATE_CLASSIFICATION_FAILURE = 'CREATE_CLASSIFICATION_FAILURE';
    export const CREATE_CLASSIFICATION_DUPLICATE = 'CREATE_CLASSIFICATION_DUPLICATE';
    export const UPDATE_CLASSIFICATION = 'UPDATE_CLASSIFICATION';
    export const DELETE_CLASSIFICATION = 'DELETE_CLASSIFICATION';
    export const CLASSIFICATION_FAILURE = 'CLASSIFICATION_FAILURE';

     

  4. Create a data fetcher for the ‘items’

    app > fetch-data > fetchClassificationData.js (new file)

     

    import axios from 'axios';
    import * as types from '../types';
    
    const fetchData = () => {
     return {
       type: types.FETCH_DATA_REQUEST,
       promise: axios.get('/classifications')
         .then(res => { return {type: types.FETCH_CLASSIFICATIONS_SUCCESS, data: res.data}; })
         .catch(error => { return {type: types.FETCH_DATA_FAILURE, data: error}; })
     };
    };
    
    export default fetchData;
    

    *Remember to update the app > fetch-data > index.js to export your new data fetcher.

  5. Create the Service (if your following their pattern explicitly…).

    I’m skipping this because services appear to be pointless 8 line wrapper for a simple axois call…  Just put the axios request in the data fetcher directly…. as indicated above.

  6. Put your dataFetcher in your routes “fetchData” property

    app > routes.jsx

    Add “fetchClassificationData” to import statement and appropriate route.
    *NOTE: I’m not showing mine because I’ve modified my fetchDataForRoute utility to accept an array for data… this doesn’t work in the base repo yet.

  7. Create some actions

    app > actions > classifications.js (new file)

    /* eslint consistent-return: 0, no-else-return: 0*/
    import { polyfill } from 'es6-promise';
    import request from 'axios';
    import md5 from 'spark-md5';
    import * as types from '../types';
    
    polyfill();
    
    export function makeClassificationRequest(method, id, data, api = '/classification') {
      return request[method](api + (id ? ('/' + id) : ''), data);
    }
    
    export function classificationFailure(data) {
      return {
        type: types.CLASSICIATION_FAILURE,
        id: data.id,
        error: data.error
      };
    }
    
    export function changeName(name) {
      return {
        type: types.CLASSIFICATION_NAME_CHANGE,
        name
      };
    }
    
    export function createClassification(name) {
    	return (dispatch, getState) => {
        if (name.trim().length <= 0) return;
    
        const id = md5.hash(name);
        const { classification } = getState();
        const newClassification = {
          id,
          name,
        };
    
        if (classification.classifications.filter(c => c.id === id).length > 0) {
          return dispatch({
            type: types.CREATE_CLASSIFICATION_DUPLICATE
          });
        }
    
        // Dispatch an optimistic update
        dispatch({
          type: types.CREATE_CLASSIFICATION_REQUEST,
          id: newClassification.id,
          name: newClassification.name
        });
    
        return makeClassificationRequest('post', id, newClassification)
          .then(res => {
            if (res.status === 200) {
    					return dispatch({
                type: types.CREATE_CLASSIFICATION_SUCCESS
              });
            }
          })
          .catch(() => {
            return dispatch({
              type: types.CREATE_CLASSICIATION_FAILURE,
              id,
              error: 'Error when attempting to create the training item.'
            });
          });
      };
    }
    
    export function updateClassification(data) {
    	return dispatch => {
    		const id = data.id;
    		// If it has an ID run an update
    		if (id.trim().length > 0) {
    			return makeClassificationRequest('put', id, data)
    				.then(() => dispatch({ type: types.UPDATE_CLASSIFICATION, data }))
    				.catch(() => dispatch(classificationFailure({ id, error: 'Could not update classification.'})));
    		} else {
          // Otherwise create a new one
          return dispatch(createClassification(data));
    		}
      };
    }
    
    export function deleteClassification(id) {
    	return dispatch => {
        return makeClassificationRequest('delete', id)
          .then(() => dispatch({type: types.DELETE_CLASSIFICATION, id}))
          .catch(() => dispatch(classificationFailure({ id, error: 'Could not delete classification.'})));
      };
    }
    
  8. Add the data set and actions to your Container

    Some example of things you might want to add to your container for user are below.

    // Imports section
    import {createClassification, deleteClassification} from '../actions/classifications';
    
    // Renderer binding
    const {classifications, createClassification, deleteClassification} = this.props;
    
    //PropTypes
    Container.propTypes = {
      classifications: PropTypes.array.isRequired,
      createClassifications: PropTypes.func.isRequired,
      deleteClassification: PropTypes.func.isRequired,
    };
    
    // Redux Mapper
    function mapStateToProps(state) {
      return {
        classifications: state.classification.classifications
      };
    }
    
    //Redux connector
    export default connect(mapStateToProps, {
      createClassifications,
      deleteClassification
    })(Container);
    

That’s all she wrote for now happy coding!  Feel free to post fixes, spelling corrections and questions in the comments. (I am one of the worst spellers…)

4 thoughts on “ReactGo – Steps to add a new data set to the store.

  1. Chetan says:

    This is really useful!

    One thing bothers me. Why is fetchData returning object instead of promise like in fetchVoteData??

    1. Joe says:

      If you take a look in my version of the boiler plate you’ll see that I modified the methodology behind the fetch data so I could fetch data from multiple datasets for one route.

  2. Chetan says:

    Another thing is, performance of webpack build is not good or is it my doing?

    1. Joe says:

      Not sure what you mean… when the server has to restart after a server file change it does take a few seconds but mine still complete in less than 1000ms usually. As far as the HMR on the FE goes I’m aware of the issue. The page load delay is a kind of crappy it boils down to an old module that the original boilerplate used in the version I started from. Once I refactor the project to use React Router 4 I’ll be able to replace HMR with something a bit more robust and efficient.

Tell me what you REALLY think...

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>