Чистая архитектура во фронтенде возможна?

Олег Кусов30.12.2021
JavaScript
ReactJS
Архитектура
Фронтенд

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

clean architecture, onion, mvc, hexagonal - стандартные архитектуры монолитных приложений, легко применимы к бэкенд приложениям, однако с фронтенд приложениями, где куча стейта, дела куда сложнее.

Саша Беспоясов не так давно выступил с докладом о чистой архитектуре, его вклад в популяризацию Clean Architecture кажется немалым, за что хочется сказать спасибо. В докладе применяется классическая чистая архитектура на примере магазина печенек на React. Архитектура подразумевает следующие слои:

Зависимости направлены внутрь, то есть доменный слой не зависит ни от чего - он главный. Слой ниже - use cases, здесь описываются конкретные кейсы, функциональность приложения. Разделяют слой юзкейсов от слоёв ниже - порты. Ниже слой адаптеров, который адаптирует API внешних сервисов, делая их совместимыми с портами юзкейсов. Это короткое, но понятное объяснение.

Эта статья - рассуждение на тему доклада. Поэтому, чтобы лучше понять о чем идет речь, советую сначала прочитать статью Саши.

Прочитав статью Саши, непонятно как работать с эффектами и зависимыми сторами. То есть, как получить реактивную логику, которая будет автоматически реагировать на изменения другого стейта в приложении и при этом не будет зависеть от UI-библиотеки? В приложении из статьи используются сервисы OrdersStorageService для хранения стейта, NotificationService для уведомлений, PaymentService для отправки запроса на бэкенд. Допустим, от нашего хранилища orders зависит стейт, который считает количество оплаченных заказов. Давайте заведем новый стейт в нашем хранилище:

const StoreContext = React.createContext({});
export const useStore = () => useContext(StoreContext);

export const Provider: React.FC = ({ children }) => {
  const [ordersCount, setOrdersCount] = useState([]); //новый стейт
  const [orders, setOrders] = useState([]);

  const value = {
    // ...
    orders,
    ordersCount,
    updateOrders: setOrders,
    updateOrdersCount: setOrdersCount,
  };

  return {children};
};

Чтобы ordersCount автоматически менялся при изменении orders можно обновлять ordersCount внутри useEffect или использовать хук useMemo:

const ordersCount = useMemo(() => {
  return orders.length;
}, [orders]);

ordersCount будет обновляться только при изменении orders. Но useEffect, useMemo это хуки React и используя их мы будем зависеть от UI. Кроме того, как мне кажется, использование хуков React в виде адаптеров изначально подразумевает то, что мы используем API UI:

export function useOrderProducts() {
  const notifier = useNotifier();
  const payment = usePayment();
  const orderStorage = useOrdersStorage();

  async function orderProducts(user: User, cookies: Cookie[]) {
    // …
  }

  return { orderProducts };
}

Нужна реактивность

Можно сделать собственный адаптер для React, собственный Observer, но это уже пахнет оверинженерингом, а ведь это потом еще поддерживать, поэтому нам нужен стейт-менеджер.

Для использования стейт-менеджера нужен будет хук-адаптер. В случае с Effector это useStore, который под капотом использует React.useRef в качестве обертки "стора" и useIsomorphicLayoutEffect для обновления компонента в случае изменения значения в "сторе". Effector позволяет изолировать бизнес логику от UI, не теряя при этом в реактивности, давая использовать эффекты и зависимое поведение вне контекста React. Но минус в том, что теперь мы зависим от API эффектора, потому что это не хранилище данных, а библиотека, с помощью которой строятся связи между юнитами (сторы, ивенты, эффекты).

С Эффектором в связке с feature-sliced юзкейс можно представить в виде отдельной папки features/order-products/. А императивная логика внутри orderProducts легко описывается с помощью API эффектора. Что касается адаптеров для localStorage и API браузера, то здесь большой вопрос - нужны ли они? Как мне кажется, нет, потому что это не похоже на зависимость - браузерное API это часть приложения, на мой взгляд. Хочу заметить, что Эффектор это лишь пример, его можно заменить чем угодно.

Из всего я сделал один главный вывод: Стейт-менеджер не должен быть зависимостью, иначе, как мне кажется, мы не сможем быстро строить юзкейсы.

По материалам bespoyasov