Honest MVC on React + Redux

  • Tutorial

MVC


This article is about how to build a web application architecture in accordance with the principles of MVC based on React and Redux . First of all, it will be of interest to those developers who are already familiar with these technologies, or those who will use them in a new project.



Model-View-Controller


The MVC concept allows us to divide the data (model), presentation and processing of actions (performed by the controller) of the user into three separate components:


  1. Model (Eng. Model):


    • Provides knowledge: data and methods of working with this data;
    • Reacts to requests, changing its state;
    • It does not contain information on how this knowledge can be visualized;

  2. View (Eng. View) - is responsible for the display of information (visualization).


  3. Controller (Eng. Controller) - provides communication between the user and the system: it controls the user input and uses the model and view to implement the necessary reaction.

React as a View


React.js is a framework for creating interfaces from Facebook. We will not consider all aspects of its use, we will talk about Stateless-components and React exclusively in the role of View.


Consider the following example:


class FormAuthView extends React.Component {
   componentWillMount() {
      this.props.tryAutoFill();
   }
   render() {
      return (
         
); } }

Here we see the declaration of the FormAuthView component. It displays a form with two Inputs for login and password, as well as a Submit button.


FormAuthView is a Stateless component, i.e. it has no internal state and receives all data for display exclusively through Props. Also, through Props, this component receives Callbacks, which change this data. By itself, this component does not know anything, you can call it "Stupid", since there is no data processing logic in it, and he himself does not know what functions he uses to process user actions. When creating a component, it tries to use Callback from Props to autocomplete the form. Nothing is known about this component's implementation of the autocomplete function.


This is an example implementation of a View layer on React.


Redux as a Model


Redux is a predictable state container for JavaScript applications. It allows you to create applications that behave identically in different environments (client, server and native applications), and are also just tested.


Using Redux implies the existence of a single Store object, in State of which the state of your entire application, each of its components will be stored.


To create a Store, Redux has a createStore function.


createStore(reducer, [preloadedState], [enhancer])

Its only required parameter is Reducer. Reducer is a function that accepts State and Action, and in accordance with the type of Action modifies the immutable State in a certain way, returning its modified copy. This is the only place in our application where State can change.


We will determine what actions are needed for our example to work:


const EAction = {
   FORM_AUTH_LOGIN_UPDATE    : "FORM_AUTH_LOGIN_UPDATE",
   FORM_AUTH_PASSWORD_UPDATE : "FORM_AUTH_PASSWORD_UPDATE",
   FORM_AUTH_RESET           : "FORM_AUTH_RESET",
   FORM_AUTH_AUTOFILL        : "FORM_AUTH_AUTOFILL"
};

Let's write the corresponding Reducer:


function reducer(state = {
    login : "",
    password : ""
}, action) {
   switch(action.type) {
      case EAction.FORM_AUTH_LOGIN_UPDATE:
         return {
            ...state,
            login : action.login
         };
      case EAction.FORM_AUTH_PASSWORD_UPDATE:
         return {
            ...state,
            password : action.password
         };
      case EAction.FORM_AUTH_RESET:
         return {
            ...state,
            login : "",
            password : ""
         };
      case EAction.FORM_AUTH_AUTOFILL:
         return {
            ...state,
            login : action.login,
            password : action.password
         };
      default:
         return state;
   }
}

And ActionCreators:


function loginUpdate(event) {
   return {
      type : EAction.FORM_AUTH_LOGIN_UPDATE,
      login : event.target.value
   };
}
function passwordUpdate(event) {
   return {
      type : EAction.FORM_AUTH_PASSWORD_UPDATE,
      password : event.target.value
   };
}
function reset() {
   return {
      type : EAction.FORM_AUTH_RESET
   };
}
function tryAutoFill() {
   if(cookies && (cookies.login !== undefined) && (cookies.password !== undefined)) {
      return {
         type : EAction.FORM_AUTH_AUTOFILL,
         login : cookies.login,
         password : cookies.password
      };
   } else {
       return {};
   }
}
function submit() {
   return function(dispatch, getState) {
      const state = getState();
      dispatch(reset());  
      request('/auth/', {send: {
          login : state.login,
          password : state.password
      }}).then(function() {
          router.push('/');
      }).catch(function() {
          window.alert("Auth failed")
      });
   }
}

Thus, these applications and methods of working with them are described using Reducer and ActionCreators. This is an example implementation of the Model layer using Redux.


React-redux as Controller


All React-components in one way or another will receive their State and Callback-and for its change only through Props. However, no React component will know about the existence of Redux and Actions in general, and no Reducer or ActionCreator will know about the React components. The data and the logic of their processing are completely separate from their presentation . I want to especially pay attention to this. There will be no "smart" components.


We will write Controller for our application:


const FormAuthController = connect(
    state => ({
        login : state.login,
        password : state.password
    }),
    dispatch => bindActionCreators({
        loginUpdate,
        passwordUpdate,
        reset,
        tryAutoFill,
        submit
    }, dispatch)
)(FormAuthView)

That's all: the React component of FormAuthView will receive login, password and Callback to change them through Props.


The result of this demo application can be viewed on Codepen .


What gives us such an approach


  • Use only stateless components. Most of which can be written as Functional-component, which is the recommended approach , because they work the fastest and consume the least memory
  • React components can be reused with or without different controllers.
  • It’s easy to write tests, because logic and mapping are not related
  • You can implement Undo / Redo and use Time Travel from Redux-DevTools
  • No need to use Refs
  • Strict development rules make React component code uniform
  • There are no problems with server rendering

What happens if you step back from MVC


The temptation is great to make some components more comfortable and write their code faster, to get them inside the State component. Like, some of his data is temporary, and they do not need to be stored. And all this will work for the time being, until, for example, you have to implement logic with switching to another URL and returning back - everything will break here, temporary data will not be temporary, and you will have to rewrite everything.


When using Stateful components, you will have to use Refs to get their State. This approach violates the unidirectional flow of data in the application and increases the connectivity of the components among themselves. Both are bad.


What happens if you step back from MVC


Also, some Stateful components may have problems with server rendering, because their display is determined not only using Props.


Also, remember that in Redux Actions are reversible, but there are no changes to State inside the components, and if you mix this behavior, nothing good will come of it.


Conclusion


I hope that a description of an honest MVC approach when developing using React and Redux will be useful for developers to create the right architecture for their web application.


If it is possible to fully use the MVC concept, then let's use it, and there is no need to invent something else. This approach has been tested for strength for decades, and all its pros, cons and pitfalls have long been known. Come up with something better is unlikely to succeed.


Every time you mix Logic and Imaging, God kills a kitten


Also popular now: