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
-
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.htmlimport mongoose from 'mongoose'; const ClassificationSchema = new mongoose.Schema({ id: String, name: String, date: { type: Date, default: Date.now } }); export default mongoose.model('Classification', ClassificationSchema);
-
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 };
-
Update the indexes…
server > db > mongo > models > index.js
+ require(‘./classification’);
server > db > mongo > controllers >index.jsimport 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 };
-
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.
-
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;
-
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. -
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';
-
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.
-
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.
-
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. -
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.'}))); }; }
-
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…)
This is really useful!
One thing bothers me. Why is fetchData returning object instead of promise like in fetchVoteData??
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.
Another thing is, performance of webpack build is not good or is it my doing?
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.