Честный MVC на React + Redux

в 1:59, , рубрики: javascript, mvc, react.js, ReactJS, redux, Блог компании DevExpress, Программирование

MVC

Эта статья о том, как построить архитектуру web-приложения в соответствии с принципами MVC на основе React и Redux. Прежде всего, она будет интересна тем разработчикам, кто уже знаком с этими технологиями, или тем, кому предстоит использовать их в новом проекте.

Model-View-Controller

Концепция MVC позволяет разделить данные (модель), представление и обработку действий (производимую контроллером) пользователя на три отдельных компонента:

  1. Модель (англ. Model):

    • Предоставляет знания: данные и методы работы с этими данными;
    • Реагирует на запросы, изменяя своё состояние;
    • Не содержит информации, как эти знания можно визуализировать;

  2. Представление (англ. View) — отвечает за отображение информации (визуализацию).

  3. Контроллер (англ. Controller) — обеспечивает связь между пользователем и системой: контролирует ввод данных пользователем и использует модель и представление для реализации необходимой реакции.

React в роли View

React.js это фреймворк для создания интерфейсов от Facebook. Все аспекты его использования мы рассматривать не будем, речь пойдет про Stateless-компоненты и React исключительно в роли View.

Рассмотрим следующий пример:

class FormAuthView extends React.Component {
   componentWillMount() {
      this.props.tryAutoFill();
   }
   render() {
      return (
         <div>
            <input 
               type = "text" 
               value = {this.props.login}
               onChange = {this.props.loginUpdate}
            />
            <input 
               type = "password" 
               value = {this.props.password}
               onChange = {this.props.passwordUpdate}
            />
            <button onClick = {this.props.submit}>
               Submit
            </button>
         </div>
      );   
   }
}

Здесь мы видим объявление Functional-component-а FormAuthView. Он отображает форму с двумя Input-ами для логина и пароля, а также кнопку Submit.

FormAuthView это Stateless-компонент, т.е. он не имеет внутреннего состояния и все данные для отображения получает исключительно через Props. Также через Props этот компонент получает и Callback-и, которые эти данные меняют. Сам по себе этот компонент ничего не умеет, можно назвать его "Глупым", так как никакой логики обработки данных в нем нет, и сам он не знает, что за функции он использует для обработки пользовательских действий. При создании компонента он пытается использовать Callback из Props для автозаполнения формы. Про реализацию функции автозаполнения формы этому компоненту тоже ничего неизвестно.

Это пример реализации слоя View на React.

Redux в роли Model

Redux является предсказуемым контейнером состояния для JavaScript-приложений. Он позволяет создавать приложения, которые ведут себя одинаково в различных окружениях (клиент, сервер и нативные приложения), а также просто тестируются.

Использование Redux подразумевает существование одного единственного объекта Store, в State которого будет хранится состояние всего вашего приложения, каждого его компонента.

Чтобы создать Store, в Redux есть функция createStore.

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

Её единственный обязательный параметр это Reducer. Reducer это такая функция, которая принимает State и Action, и в соответствии с типом Action определенным образом модифицирует иммутабельный State, возвращая его измененную копию. Это единственное место в нашем приложении, где может меняться State.

Определимся какие Action-ы нужны, для работы нашего примера:

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"
};

Напишем соответствующий 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;
   }
}

И ActionCreator-ы:

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")
      });
   }
}

Таким образом, данные приложения и методы работы с ними описаны с помощью Reducer и ActionCreators. Это пример реализации слоя Model с помощью Redux.

React-redux в роли Controller

Все React-компоненты так или иначе будут получать свой State и Callback-и для его изменения только через Props. При этом ни один React-компонент не будет знать о существовании Redux и Actions вообще, и ни один Reducer или ActionCreator не будет знать о React-компонентах. Данные и логика их обработки полностью отделены от их представления. Я хочу особенно обратить на это внимание. Никаких "Умных" компонентов не будет.

Напишем Controller для нашего приложения:

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

На этом всё: React-компонент FormAuthView получит login, password и Callback-и для их изменения через Props.

Результат работы этого демо-приложения можно посмотреть на Codepen.

Что нам дает такой подход

  • Использование только Stateless-компонентов. Большую часть которых можно написать в виде Functional-component, что является рекомендованным подходом, т.к. они быстрее всего работают и потребляют меньше всего памяти
  • React-компоненты можно переиспользовать с разными контроллерами или без них
  • Легко писать тесты, ведь логика и отображение не связаны между собой
  • Можно реализовать Undo/Redo и использовать Time Travel из Redux-DevTools
  • Не нужно использовать Refs
  • Жесткие правила при разработке делают код React-компонентов однообразным
  • Отсутствуют проблемы с серверным рендерингом

Что будет, если отступить от MVC

Велик соблазн сделать какие-то компоненты поудобнее и написать их код побыстрее, завести внутри компонента State. Мол какие-то его данные временные, и хранить их не нужно. И всё это будет работать до поры до времени, пока, например, вам не придется реализовать логику с переходом на другой URL и возвращением обратно — тут всё сломается, временные данные окажутся не временными, и придется всё переписывать.

При использовании Stateful-компонентов, чтобы достать их State, придется использовать Refs. Такой подход нарушает однонаправленность потока данных в приложении и повышает связность компонентов между собой. И то и другое — плохо.

Что будет, если отступить от MVC

Также некоторые Stateful-компоненты могут иметь проблемы с серверным рендеренгом, ведь их отображение определяется не только с помощью Props.

А еще следует помнить, что в Redux Action-ы обратимы, но изменения State внутри компонентов — нет, и если смешать такое поведение — ничего хорошего не получится.

Заключение

Надеюсь, описание честного MVC подхода при разработке с использованием React и Redux будет полезно разработчикам для создания правильной архитектуры своего web-приложения.

Если есть возможность в полной мере использовать концепцию MVC, то давайте её использовать, и не нужно изобретать что-то другое. Это подход проверенный на прочность десятилетиями, и все его плюсы, минусы и подводные камни давно известны. Придумать что-то лучше навряд ли получиться.

Каждый раз, когда ты смешиваешь Логику и Отображение, Бог убивает котенка

Автор: DevExpress

Источник

Поделиться новостью

* - обязательные к заполнению поля