Применяем паттерн MVVM на React

от автора

При создании форм на React встает вопрос управления состоянием приложения. Казалось бы, богатый выбор, однако Redux поражает своим «fizzbuzz enterprise», а Mobx сайд-эффектами в ООП коде от начинающих разработчиков. Исходя из моего опыта, оба инструмента плохо подходят для онбординга начинающих программистов. А бизнесу их трудоустроить очень выгодно: они дешевые, пугливые и наивные

Решением проблемы будет выкидывание лишних абстракций из кодовой базы Frontend. Бизнес-логика, размазанная на несколько файлов, вгоняет начинающего разработчика в ступор. Я хотел бы поделиться одной хорошо зарекомендовавшей себя практикой.

Давным-давно, в далёкой-далёкой галактике…

На момент 2010-ого года, когда управление состоянием веб-приложения переходило от ненаправленного потока данных (jQuery) к контейнерам состояния, первые фреймворки широко использовали разновидности паттерна MVVM (AngularJS, MarionetteJS, BackboneJS). Дело в том, что на тот момент Microsoft пытались захватить рынок и широко продвигали Silverlight, разновидность WPF для интернета. И бой был на смерть, к примеру, язык разметки XAML первым ввел нечто похожее на FlexBox и CSS Grid (см. StackPanel и Grid). Веб копировал наработки мелкомягких по максимуму.

Ошибка, которая привела к смерти Backbone

Подход Backbone к управлению состоянием приложения подразумевал использование сущности Collection, которая автоматически синхронизирует содержимое массива с CRUD на стороне backend.

Изменение объекта, который лежит внутри Collection, порождало перерисовку приложения. Идея хорошая, однако:

  1. Прикладной программист должен наследовать свой класс от Collection, переопределив внутри методы для обращения к backend. Наследование, как порождение лишней абстракции, антипаттерн для frontend, нужно использовать композицию.

  2. Collection предоставлял более 10 методов к переопределению для синхронизации содержимого с backend. Как правило, переопределяли только одну функцию syncCollection

  3. При передаче доработки фичи от одного программиста к другому, добавлять новые кейсы в функцию для синхронизации коллекции было сложнее с каждым разом. Это связано с тем, что нужно сохранить обратную совместимость с текущим кодом

Что можно сделать?

Можно оставить на Collection только задачу перерисовки UI, вынеся запросы к backend на чтение и запись в обработчики действий пользователя до мутации данных

import { useCollection } from "react-declarative";  const ListItem = ({ entity }) => {    const handleIncrement = () => {     /*     await fetch(`/api/v1/counters/${entity.id}`, {       method: "PATCH",       headers: {         'Content-Type': 'application/json',       },       body: JSON.stringify({         counter: entity.data.counter + 1       })     });     */     entity.setData({       id: entity.id,       counter: entity.data.counter + 1     });   };    return (     <div key={entity.id}>       {entity.data.counter}       <button onClick={handleIncrement}>Increment counter</button>     </div>   ); };  export const App = () => {   const collection = useCollection({     onChange: (collection, target) =>       console.log({         collection,         target       }),     initialValue: [] // await fetch() or props...   });    const handleAdd = () => {     /*     const { id, ...data } = await fetch("/api/v1/counters", {       method: "POST",     }).then((data) => data.json());     */     collection.push({       id: Math.max(...collection.ids, 0) + 1,       counter: 0       // ...data     });   };    return (     <>       {collection.map((entity) => (         <ListItem key={entity.id} entity={entity} />       ))}       <button onClick={handleAdd}>Add item</button>     </>   ); };  export default App; 

Код, представленный выше, выводит список счетчиков (демо на codesandbox). Внутри кода закомментированы места, где можно обратиться к серверу. Также возможно обработать исключение в запросе так, что блок catch будет выполнен в контексте формы. Например, это можно использовать, чтобы вывести snackbar из хука notistack

import { useSnackbar } from 'notistack';  ...  const { enqueueSnackbar } = useSnackbar();  const handleNetworkRequest = () => {   fetchSomeData()     .then(() => enqueueSnackbar('Successfully fetched the data.'))     .catch(() => enqueueSnackbar('Failed fetching data.'));

Массив, содержимое которого требуется синхронизировать с backend, нужно положить в аргумент initialValue хука useCollection (строка 38). Хук вернет объект Collection, реализующий метод map, позволяющий бесшовно с массивом вывести список элементов. Каждый элемент массива будет обернут в контейнер Entity, предоставляющий доступ к оригинальному значению через свойство data и метод setData. Вызов метода setData (строка 17) синхронно изменит data и, через debounce, попросит хук useCollection перерисовать форму

Как синхронизировать один объект?

По аналогии с useCollection, экспортируется хук useEntity. Он вернет переданный в аргументы объект, обернутый в Entity, вызов setData у последнего также перерисует форму.

Используя два вышеупомянутых хука можно сэкономить на передаче обратных вызовов через props, убрать лишний boilerplate, не терять контекст исполнения при разбиении взаимосвязанной бизнес-логики на разные файлы

Где посмотреть код хуков?

Чтобы убрать нужду плодить зависимости в вашем проекте, предоставляю ссылки на файлы)

  1. Collection

  2. Entity

  3. useCollection

  4. useEntity


ссылка на оригинал статьи https://habr.com/ru/post/687568/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *