Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Data models and business logic in isomorphic (React/Redux/Express/Mongo) app

I've recently built a few isomporphic/univeral projects using the React-Redux-Express-Mongoose stack.

In my mongoose models is contained a lot of business-logic. As a very basic example (excuse my ES6):

import mongoose, {Schema} from 'mongoose';

const UserSchema = new Schema({
  name: String,
  password: String,
  role: String
});

UserSchema.methods.canDoSomeBusinessLogic = function(){
  return this.name === 'Jeff';
};

UserSchema.methods.isAdmin = function(){
  return this.role === 'admin';
};

This is all great on the server, however when these models are hydrated in the browser as plain JSON objects, I then have to re-implement this same business logic in some React component or Redux reducer, which doesn't feel very clean to me. I'm wondering how best to approach this.

From reading around Mongoose, there seems to be limited browser support, mostly just for document validation. I suppose my main options are:

  • Move all the business logic into some "normal" JS classes, and instantiate those all over the place. For example:

    # JS Class definition - classes/user.js
    export default class User {
        constructor(data = {}){
          Object.assign(this,data);
        }
    
        canDoSomeBusinessLogic(){
          return this.name === 'Jeff';
        };
    
        isAdmin(){
          return this.role === 'admin';
        }
    }
    
    # Server - api/controllers/user.js
    import UserClass from 
    User.findById(1,function(err,user){
        let user = new UserClass(user.toJSON();
    });
    
    # Client - reducers/User.js
    export default function authReducer(state = null, action) {
      switch (action.type) {
        case GET_USER:
          return new UserClass(action.response.data);
      }
    }
    
    # Client - containers/Page.jsx
    import {connect} from 'react-redux';
    
    @connect(state => ({user: state.user}))
    export default class Page extends React.Component {
        render(){
          if(this.props.user.isAdmin()){ 
            // Some admin 
          } 
        }
    }
    
  • Move all the business logic into a some static helper functions. I won't write out the whole example again, but essentially:

    # helpers/user.js
    export function isAdmin(user){
        return user.role === 'admin';
    }
    

I suppose the difference between the above 2 is just personal preference. But does anyone have any other thoughts about isomorphic apps and data modelling? Or have seen any open-source example of people solving this problem.

As an extension to the above, what about an isomorphic save() function e.g. User.save(). So if called on the client it could do a POST to the relevant API endpoint, and if run on the server it would call the Mongoose save() function.

like image 393
Joe Woodhouse Avatar asked Apr 15 '16 08:04

Joe Woodhouse


1 Answers

Spoiler: Expect an opinionated reply. There is no 'right' way to do it.

First of all, I want to make the difference between isomorphic and universal clear, so that you know exactly what we are talking about:

Isomorphism is the functional aspect of seamlessly switching between client- and server-side rendering without losing state. Universal is a term used to emphasize the fact that a particular piece of JavaScript code is able to run in multiple environments.

Is it worth it that much abstraction into an universal app?

Generally what you want an universal app for is to have the client and the server that pre-renders the app both loading the same code. Although you can run the API from the same server that pre-renders the app, I would rather proxy it and run it in a different process.

Let me show you two different React repositories:

  • React + API erikras/react-redux-universal-hot-example
  • React wellyshen/react-cool-starter

Erikras well-known boilerplate uses his universal app to share dependencies globally, and code between the server that pre-renders the page and the client. Although he could, he does not share validation. Survey API validation Survey client validation

Wellyshen does not have an API, but he also shares his dependencies and code, though only between the server and the client. The server loads the routes, the store and everything that is being run by the client app. That is to provide isomorphism.

Having said that, it is up to you whether to move all validation in one place. I probably would just consider it for complicated validation cases, like an email validation which you could actually have a helper for that. (that was just an example, for email validation you already have validator). In certain occasions, it might be more convenient to rely on the API validation, albeit not being the best practice.

Simple validations, like the ones in your examples, can be done effortless with redux-form anyway, which that I know there is no direct way to translate it on the API. Instead you should probably be looking for express-validator on it.

One more thing, despite the fact that a few very popular React boilerplates will have the API and client together, I tend to work with two different repositories: the React + server-side rendering and the API. In the long term run it will result in a cleaner code that will be totally independent one from the other. organizing-large-react-applications

like image 117
zurfyx Avatar answered Oct 15 '22 15:10

zurfyx