Как стать автором
Обновить

ReCA: React Clean Architecture state manager

Время на прочтение8 мин
Количество просмотров3.1K

Что будет если объединить Функциональное Программирование и Объектно-Ориентированное Программирование в одном веб приложении? Получится мощный инструмент для написания веб приложений объединяющий всю простоту написания верстки в функциональном стиле и мощь ООП для написания бизнес логики сложного приложения! А произвести такое объединение позволяет библиотека ReCA. Которая позволяет использовать в одном приложении оба подхода при это разделяя зоны ответственности и не создавая конфликтов стилей, а также решающая множество повседневных задач.

Данный подход является логическим продолжением подхода Чистой Архитектуры описанной в прошлой статье. С тех пор в Реакте появились функциональные компоненты и хуки, которые сильно упрощают верстку приложений, поэтому тот подход надо было подтянуть до современных реалий. Так и родилась библиотека Reca.

Кроме того, если та статья была ориентирована на продвинутых разработчиков и чтение толстой книжки, то эта статья ориентирована на самых начинающих разработчиков. Ведь ReCA сильно упрощает применение Чистой Архитектуры избавляя от чтения сложных книг и позволяет писать приложение сразу после прочтения README.md. В связи с этим, я надеюсь, мне простят некоторые допущение сделанные для большей доступности статьи.

Преимущества

При создании ReCA в него были заложены следующие идеи:

Микросторы - вместо создание одного большого сложного стора, вы создаете множество маленьких независимых сторов. Такие сторы гораздо проще разрабатывать и поддерживать. А с ростом их количества приложение не начинает тормозить, как это происходит с моносторами.

Dependency Injection - из коробки поставляется механизм внедрения зависимостей, который позволяет переопределить часть функционала для определенного теста, заказчика, платформы. Например вы можете использовать один и тот же функционал стора на веб странице и в мобильном приложении на реакт нейтив, при этом использовать в них разные сервисы уведомления пользователей.

Микрофронтенды - ReCA создавалась для работы во встраиваемых микрофронтендах. Микросторы обеспечивают равномерное распределение сторов по чанкам. Для работы ReCA не используются провайдеры, по этому на одной странице может быть множество React приложений.

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

Нет бойлерплейта - совсем нет. Просто пишите логику на обычном typescript.

Простой флоу данных - Не надо разбираться в сложных цепочках экшенов. Вся логика размещена в 4 очевидных слоях с быстрыми переходами по горячей клавише в ИДЕ.

Очень маленький размер - менее 1 кб в минифицированном коде. Кроме того он еще и легко расширяется.

В целом библиотека ReCA очень похожа на библиотеку MobX, но имеет ряд улучшений позволяющие использовать подход ООП гораздо более лучше.

Простой пример

Пользоваться ReCA очень просто, достаточно взглянуть на пример:

// todo.store.ts
import {AutoStore} from "reca";
import type {FormEvent} from "react";

export class ToDoStore extends AutoStore {

    public currentTodo: string = "";

    public todos: string[] = [];

    public handleAddTodo (): void {
        this.todos.push(this.currentTodo);
    }

    public handleDeleteTodo (index: number): void {
        this.todos.splice(index, 1);
    }

    public handleCurrentEdit (event: FormEvent<HTMLInputElement>): void {
        this.currentTodo = event.currentTarget.value;
    }

}


// todo.component.ts
import {useStore} from "reca";
import {ToDoStore} from "../stores/todo.store";

export const ToDoComponent = (): JSX.Element => {
    const store = useStore(ToDoStore);

    return (
        <div className="todos">
            <div className="todos-list">
                {
                    store.todos.map((todo, index) => (
                        <div className="todo">
                            {todo}

                            <button
                                className="todo-delete"
                                onClick={() => store.handleDeleteTodo(index)}
                                type="button"
                            >
                                X
                            </button>
                        </div>
                    ))
                }
            </div>

            <div className="todos-input">
                <input
                    onInput={store.handleCurrentEdit}
                    value={store.currentTodo}
                />

                <button
                    onClick={store.handleAddTodo}
                    type="button"
                >
                    add
                </button>
            </div>
        </div>
    );
};

Достаточно создать ваш класс отнаследовав его от объекта AutoStore библиотеки ReCA и описать там всю логику. В функциональном компоненте достаточно использовать хук useStore из той же библиотеки. И все. Никакого бойлерплейта. Только логика.

Обратите внимание что методы объекта можно передавать в события элементов как через стрелочную функцию, так и напрямую по ссылке. ReCA сама позаботиться о сохранении контента.

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

Тут есть важное отличие от сторов MobX и контекстов React. В данном случае у каждого компонента будет свой экземпляр стора. Один компонент ничего не знает об изменениях в другом компоненте использующий тот же стор. Изменения стора компонента затрагивает перерисовку только того компонента, в котором он используется. ReCA не предоставляет инструментов глобального стора т.к. ориентирован на микрофронтенды, но в тоже время и не ограничивает вас в выборе глобального стора. Это может быть EventBus, Observer, React Context или любой другой. Так же ниже будет пример переиспользуемых состояний с использованием Сервисов.

Как происходит обновление стейта?

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

Но это еще не все. Процесс перерисовки запускается не сразу, а с задержкой на requestAnimationFrame, для синхронизации с циклом обновления монитора. В результате такой оптимизации даже если будет вызвано десятки методов и заменено десятки свойств стора, перерисовка реакта произойдет только одна, которую уже и увидит пользователь. Что очень хорошо влияет на производительность даже на самых слабых устройствах.

Пример для энтерпрайза

А теперь давайте посмотрим пример созданный по заветам Чистой Архитектуры:

// SpaceXCompanyInfo.ts
export class SpaceXCompanyInfo {

    public name: string = "";

    public founder: string = "";

    public employees: number = 0;

    public applyData (json: object): this {
        Object.assign(this, json);
        return this;
    }

}


// SpaceXService.ts
import {reflection} from "first-di";
import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo";

@reflection
export class SpaceXService {

    public async getCompanyInfo (): Promise<SpaceXCompanyInfo> {
        const response = await fetch("https://api.spacexdata.com/v3/info");
        const json: unknown = await response.json();

        // ... and manies manies lines of logics

        if (typeof json === "object" && json !== null) {
            return new SpaceXCompanyInfo().applyData(json);
        }
        throw new Error("SpaceXService.getCompanyInfo: response object is not json");
    }

}


// SpaceXStore.ts
import {reflection} from "first-di";
import {AutoStore} from "reca";
import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo.js";
import {SpaceXService} from "../services/SpaceXService.js";

@reflection
export class SpaceXStore extends AutoStore {

    public companyInfo: SpaceXCompanyInfo = new SpaceXCompanyInfo();

    public constructor (
        private readonly spaceXService: SpaceXService,
        // private readonly logger: Logger
    ) {
        super();
    }

    public activate (): void {
        this.fetchCompanyInfo();
    }

    private async fetchCompanyInfo (): Promise<void> {
        try {
            this.companyInfo = await this.spaceXService.getCompanyInfo();
        } catch (error) {
            // Process exceptions, ex: this.logger.error(error.message);
        }
    }

}


// SpaceXComponent.tsx
import {useStore} from "reca";
import {SpaceXStore} from "../stores/SpaceXStore.js";

export const TestStoreComponent = (): JSX.Element => {
    const store = useStore(SpaceXStore);

    return (
        <div>
            <p>
                Company:
                {" "}

                {store.companyInfo.name}
            </p>

            <p>
                Founder:
                {" "}

                {store.companyInfo.founder}
            </p>
        </div>
    );
};

В данном примере мы создали дополнительный класс SpaceXService куда вынесли логику пере используемую в нескольких сторах. В данном случае это получение данных из API компании SpaceX. И приведение этих данных к классу.

Спойлер для тех кто хочет правильнее

На самом деле так json к классу лучше не приводить, а для таких операций использовать библиотеку TS-Serializable которой я писал ранее. Данный пример упрощен для наглядности.

Кроме того данные в сервисах лучше не запрашивать, а сам запрос вынести в слой Repository согласно заветам Чистой Архитектуры. А в слое Service оставить только логику.

Далее сервис необходимо сделать доступным в сторе. Для этого просто определяем его как свойство в конструкторе стора, а на сам стор вешаем декоратор @reflection. Декоратор заставит typescript сгенерировать метаинформацию благодаря которой механизм Dependency Injection поймет какого типа зависимости используется в конструкторе и встроит их туда.

Тут важно понимать что на всех экзмеплярах всех сторов будет встроен один и тот же экземпляр сервиса, т.е. как Singleton. В качестве механизма встраивания используется библиотека First DI о которой я недавно писал. Так же в ReCA заложен функционал по опциональной смене DI библиотеки на аналогичную по функциональности.

Как правильно написать Сервис

Сервисы организуются по методологии SOLID

Обратите внимание что при использовании асинхронных запросов в Сторах и Сервисах нету необходимости в сторонних библиотеках вроде Thunk, Saga и подобных. Используется простой, красивый и стандартный синтаксис async / await.

Так же Синглтон Сервисы можно использовать для организации состояний которые шарятся между разными сторами и использовать их вместо глобальных сторов в т.ч. для микрофронтендов. Для этого достаточно разместить в Сервисе EventBus или Observer.

Методы жизненного цикла

Методы жизненного цикла очень похожи на те что есть в классовых компонентах React.

  • constructor - вызывается при создании объекта. В нем рекомендуется инициализировать свойства стора, но не делать никаких асинхронных запросов, т.к. асинхронные запросы плохо влияют на скорость первой отрисовки в React. В классовых компонентах имеет аналог constructor, в функциональных имеет аналог хук useState.

  • activate - вызывается после первой отрисовки вьюхи. В этом методе рекомендуется запускать асинхронные запросы, в т.ч. для данных с сервера, а так же навешивать логику на ref элементы если это необходимо. В классовых компонентах имеет аналог componentDidMount, в функциональных имеет аналог хук useEffect(() => store.activate(), []).

  • update - вызывается при перерисовке компонента. В этом методе можно обновить состояние компонента если в этом есть необходимость. В классовых компонентах имеет аналог shouldCompoonentUpdate, в функциональных не имеет прямого аналога.

  • afterUpdate - вызывается после перерисовке компонента. От activate отличается тем что activate вызывается только один первый раз отрисовки компонента, а afterUpdate второй и последующие разы. В классовых компонентах прямого аналога нет, в функциональных имеет аналог useEffect(() => store.afterUpdate())

  • dispose - вызывается при удалении компонента. Туда рекомендуется добавлять код который зачищает логику от своего присутствия. Например отписку от html элементов, удаление внешних скриптов завязанных на компонент.

Так же хук useStore принимает второй параметр, который называется props. Этот параметр передается во все методы жизненного цикла в качестве параметра.

Особенности АвтоСтора

Иногда есть необходимость не перерисовывать компонент при изменении некоторых свойств или вызовов некоторых методов. Для этого на это свойство или метод необходимо накинуть декоратор @notRedraw:

import {AutoStore, notRedraw, reflection} from "reca";

@reflection
export class SemiAuto extends AutoStore {

    public refElement: HTMLDivElement | null = null;

    public activate(): void {
        window.addEventListener("mousemove", this.onMouseMove);
    }

    public dispose(): void {
        window.removeEventListener("mousemove", this.onMouseMove);
    }

    @notRedraw()
    private onMouseMove(event: MouseEvent): void {
        // логика обработки события мыши
		    if (this.refElement) {
			      this.refElement.style.top = ...
		    }
    }
}

Таким образом не будет происходить лишних React перерисовок на каждое шевеление мышки.

Итог

Получился инструмент который использует лучшие стороны Функциональной и ООП парадигмы разработки. Сторонники обоих парадигм могут одновременно работать на одном проекте. Функциональные разработчике в области верстки и простой логики, а ООП в области сложной логики.

Данный подход не залочен на библиотеку React и может быть распространён на любой другой. Так же библиотека легко расширяется для интеграции с вашими особенностями логики.

Если вы еще пишите на Angular, вы без проблем сможете перенести свою логику на связку React + Reca. Сохранив большую часть вашей логики, фактически меняется только слой view.

Описанные здесь фичи далеко не предел возможностей ReCA, очень много функционала осталась за пределом этой статьи. А еще больше идей по упрощению бытовых операций будет реализовано в будущих версиях по мере необходимости.

Понравился подход? Помогите распространить материал. Звезды на гитхабе также помогут сделать его более заметным.

Спасибо за внимание! =)

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 4: ↑2 и ↓20
Комментарии25

Публикации