Pull to refresh

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

Reading time5 min
Views64K

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

Здесь мы видим объявление компонента 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, то давайте её использовать, и не нужно изобретать что-то другое. Это подход проверенный на прочность десятилетиями, и все его плюсы, минусы и подводные камни давно известны. Придумать что-то лучше навряд ли получиться.


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

Tags:
Hubs:
+21
Comments44

Articles

Change theme settings

Information

Website
www.developersoft.ru
Registered
Founded
1998
Employees
201–500 employees
Location
Россия