При создании форм на 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, порождало перерисовку приложения. Идея хорошая, однако:
Прикладной программист должен наследовать свой класс от Collection, переопределив внутри методы для обращения к backend. Наследование, как порождение лишней абстракции, антипаттерн для frontend, нужно использовать композицию.
Collection предоставлял более 10 методов к переопределению для синхронизации содержимого с backend. Как правило, переопределяли только одну функцию
syncCollection
При передаче доработки фичи от одного программиста к другому, добавлять новые кейсы в функцию для синхронизации коллекции было сложнее с каждым разом. Это связано с тем, что нужно сохранить обратную совместимость с текущим кодом
Что можно сделать?
Можно оставить на 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, не терять контекст исполнения при разбиении взаимосвязанной бизнес-логики на разные файлы
Где посмотреть код хуков?
Чтобы убрать нужду плодить зависимости в вашем проекте, предоставляю ссылки на файлы)