Когда мы решили вывести на прод Telegram‑мини‑приложение для «капельных» (stream) TON‑платежей, довольно быстро стало ясно: обычный CRUD‑фронт тут не выживет. Сразу накрыла волна специфичных задач — от гранулярного онбординга в Web‑App до борьбы с ограничениями API‑ключей и тонкостей работы с TON SDK во встроенном браузере Telegram. Каждый шаг требовал не только кода, но и аккуратного выбора архитектурных приёмов, иначе продукту грозили дубли запросов, «белые экраны» и несогласованность состояний.

В этой статье я разобрал пятнадцать самых характерных «боевых» сложностей, показал, каким паттерном мы их укрощали, и какой антипаттерн поджидал за поворотом. Это не академический список, а выжимка из коммитов и ночных дебаг‑сессий, которая поможет тем, кто строит похожие интеграции между Telegram, TON и React.


1. Ручное рукопожатие с Telegram Web‑App

Telegram требует вызвать ready() и expand() только после инициализации. Мы завели отдельный useEffect, который выполняется ровно один раз:

useEffect(() => {
  if (window.Telegram?.WebApp) {
    window.Telegram.WebApp.ready();
    window.Telegram.WebApp.expand();
  }
}, []);
  • Паттерн — Lifecycle hook / Template Method. Чётко отделяем «фазу подключения» от остальной логики.

  • Антипаттерн — God Effect. Когда в один useEffect сваливается и подключение к SDK, и загрузка данных, и подписка на DOM‑события.


2. Защита от «двойного старта» при получении пользователя

При каждом ререндере компонент мог повторно стучаться на /api/add-user. Простой useRef‑флаг превратил функцию в Singleton‑guard:

const hasFetched = useRef(false);

const initializeUser = async () => {
  if (hasFetched.current) return;
  hasFetched.current = true;
  /* …дальше идёт fetch… */
};
  • Паттерн — Singleton + Guard Clause. Позволяет выполнить тяжелую операцию ровно один раз.

  • Антипаттерн — Double Initialization, из‑за которого на бэкенд летят дубли, а у пользователя мерцает UI.


3. Deep‑link «/start + contract» прямо в детали контракта

Из чата бот передаёт параметр tgWebAppStartParam. При стартапе мы валидируем роль, ищем ID контракта по адресу и сразу роутим:

const startParam = urlParams.get("tgWebAppStartParam");
if (startParam && res.data.role === "Employee") {
  const { id } = await axios.get("/api/get-contract-by-address", { params:{ contractAddress:startParam }});
  navigate(`/contract/${id}`);
}
  • Паттерн — Front Controller (Router). В едином месте intercept‑им url‑параметры и решаем, куда идти.

  • Антипаттерн — Spaghetti Navigation (ручные window.location в компонентах).


4. Под разные сети TON без условных каскадов

Компонент‑стратегия сам подбирает endpoint и API‑key:

export function useTonClient() {
  return useAsyncInitialize(async () => {
    let endpoint = await getHttpEndpoint({ network: process.env.REACT_APP_NETWORK ?? "testnet" });
    if (process.env.REACT_APP_NETWORK === "testnet") {
      endpoint = "https://testnet.toncenter.com/api/v2/jsonRPC";
    } else {
      endpoint = "https://toncenter.com/api/v2/jsonRPC";
    }
    return new TonClient({ endpoint });
  });
}
  • Паттерн — Strategy. Сеть меняется конфигом, код не трогается.

  • Антипаттерн — Hard‑coded config. Когда URL меняют руками в нескольких файлах перед релизом.


5. Универсальный хук‑фабрика useAsyncInitialize

Позволяет лениво и единожды инициализировать что угодно — SDK, foreign API, контракт:

export function useAsyncInitialize<T>(fn: () => Promise<T>, deps:any[]=[]){
  const [state,setState] = useState<T>()
  useEffect(()=>{ (async()=>setState(await fn()))() }, deps)
  return state;
}
  • Паттерн — Lazy Factory. Экономим код и память, создаём объект только когда нужен.

  • Антипаттерн — Async Call in Render, вызывающий «Cannot update a component while rendering…».


6. Адаптер к Ton Connect: одно лицо вместо трёх SDK

В UI нам нужен просто метод send(), а не вся тоновская экосистема:

export function useTonConnect(): { sender:Sender } {
  const [tonConnectUI] = useTonConnectUI();
  return {
    sender: {
      send: async (args) => {
        tonConnectUI.sendTransaction({ messages:[{/* ... */}], validUntil: Date.now()+5*60*1000 });
      },
    },
  };
}
  • Паттерн — Adapter. UI остаётся неизменным, даже если поменяем SDK.

  • Антипаттерн — Leaky Abstraction. Когда глубоко вниз протаскивают «сырые» объекты SDK.


7. Отсечка времени на подпись транзакции

Пользователь может уйти; pending TX тогда «висит» вечно. Мы добавили validUntil:

validUntil: Date.now() + 5 * 60 * 1000 // 5 минут
  • Паттерн — Timeout / Expiry. Делает UX предсказуемым и упрощает повторную отправку.

  • Антипаттерн — Infinite Pending Promise. Когда транзакция никогда не закрывается и UI не знает, что делать.


8. Шим Buffer в браузере

Библиотеки crypto из Node требуют global.Buffer. Один shim во всём приложении:

declare global { interface Window { Buffer: typeof Buffer } }
window.Buffer = Buffer;
  • Паттерн — Polyfill / Shim. Централизованное решение совместимости.

  • Антипаттерн — Monkey‑patch Chaos, когда каждый модуль пытается импортировать/переопределять Buffer.


9. Единая тема вместо разноцветного хаоса

Создали ThemeProvider и конфиг:

const theme = createTheme({
  palette:{ primary:{ main:"#1976d2"}, mode:"light"},
  typography:{ fontFamily:"Roboto, Arial, sans-serif"},
});
  • Паттерн — Abstract Factory (Theme Object). Меняем фирменный цвет — меняется всё.

  • Антипаттерн — Hard‑coded colors, когда дизайнер меняет палитру, а фронт переписывает десятки файлов.


10. «Раковина»‑shell и чистые бизнес‑страницы

Навигация держится в одном месте, каждый экран знает только о своих данных:

<Routes>
  <Route path="/" element={!roleSelected ? <WelcomePage/> : …}/>
  <Route path="/contracts" element={<AllContractsPage user={user}/>}/>
  <Route path="/contract/:id" element={<ContractDetailPage/>}/>
</Routes>
  • Паттерн — Page Controller (MVVM разделение). Упрощает on‑boarding новых страниц.

  • Антипаттерн — God Component на 1000 строк JSX.


11. Конечный автомат состояний: роль → кошелёк → главная

Три булевых флага превращаются в два «чистых» состояния:

!roleSelected       // ещё не выбрана роль
!user.walletAddress // кошелёк не привязан
/* иначе — главная */
  • Паттерн — State Machine. Нет «полутонов» (кошелёк есть, но роль не выбрана).

  • Антипаттерн — Boolean State Explosion. Когда появляется четвёртая комбинация, о которой никто не подумал.


12. Грациозный провал вместо белого экрана

Ошибка сети на старте не роняет всё приложение:

catch (error) {
  console.error("Error initializing user:", error);
}
  • Паттерн — Graceful Degradation / Fail‑safe. Пользователь остаётся в Welcome‑экран, а не видит «Nothing was returned».

  • Антипаттерн — Fail‑Fast Crash, особенно болезненный на мобильном webview.


13. Gateway к смарт‑контракту вместо прямых вызовов из React

export function useContract(addr:string){
  const client = useTonClient();
  const finance = useAsyncInitialize(async()=>{
    if(!client) return;
    return client.open(new Finance(Address.parse(addr)));
  },[client]);

  return { getConfig: () => finance?.getConfig() };
}
  • Паттерн — Repository / Gateway. Меняем ABI — правим только этот файл.

  • Антипаттерн — Anemic Model. Когда методы контракта размазаны по разным компонентам.


14. «Поднять» state, а не раздавать Context направо‑налево

user и role хранятся в App, а дочерние страницы получают их только если нужно:

<AllContractsPage user={user} role={role}/>
  • Паттерн — Lifting State Up. Простой и прозрачный способ избежать prop‑drilling глубже трёх уровней.

  • Антипаттерн — Global Mutable Singleton (window.user или чрезмерно общий React Context).


15. Тайная жизнь контрактов: асинхронный «бинарник» Finance

Сам контракт открывается лениво, а компоненты получают только чистую функцию getConfig() — никакой сериализации / десериализации в UI:

Использованный код уже приведён в пункте 13.

  • Паттерн — Facade. Декодирование, проверка подписи и другие детали спрятаны за «одной ручкой».

  • Антипаттерн — Leaking Encapsulation, когда в UI начинают парсить BOC‑байты.


Небольшое подведение итогов:

  • Чёткая архитектура = позволяет быстрее менять бизнес‑логику.
     — Переключение сети (testnet ↔ mainnet) заняло минуты, а не дни, потому что доступ к TON вынесен в Strategy‑слой.

  • Каждая проблема нашла «имя» (паттерн) — упрощает ревью и онбординг.
     — Новому разработчику легче понять, зачем useAsyncInitialize, когда он видит ссылку на Lazy Factory, а не доморощенный «костыль».

  • Антипаттерны — отличный чек‑лист для code‑review.
     — Мы буквально проходились по списку: «Не дублируем ли запрос?», «Не утекла ли абстракция?», «А что будет, если сеть упадёт?».

  • Результат — фронт держит нагрузку, быстро подменяет контракты и переживает «падения» внешних сервисов без белого экрана. Всё это — с минимумом кода‑клонов и максимумом предсказуемости.

Для понимания общего смысла проекта:

Driptonbot — это Telegram‑бот и смарт‑контракт в сети TON, который превращает обычную почасовую оплату в поток минивыплат в реальном времени. Работодатель депонирует сумму единовременно, а деньги «капают» сотруднику согласно таймеру. От каждого перевода % уходит на адрес рекомендателя.

Комментарии (5)


  1. fransua
    12.05.2025 20:41

    Многие "паттерны" притянуты за уши, при чем тут MVVM, Anemic Model, Repository, Abstract Factory, Strategy? Просто наброс умных слов без содержания. В целом весь приведенный код - это один большой антипаттерн "пихаем все в реакт". У Вас на обход сложности реакта ушло больше костылей, чем было сложности в изначальной задаче.
    Но, это хотя бы попытка сделать "архитектуру", удачи в пути.


    1. alex896079 Автор
      12.05.2025 20:41

      Спасибо за прямоту! Коротко - зачем здесь "умные" слова и куда ушли "костыли".
      MVVM / Page split В одном web view разделили React страницы (View Model) и хуки шлюзы кTON/REST (Model). Так проще тестировать и менять UI, не трогая домен.
      Strategy useTonClient() скрывает выбор mainnet/testnet: одна переменная окружения вместо трёх правок в коде.
      Abstract/Factory Темы MUI - готовая фабрика стилей; поменяли цвет в одном месте - перекрасилось всё.
      Repository /Gateway useContract() инкапсулирует ABI и TonClient. Переезд на gRPC - правим один файл.
      Anemic Model (как антипаттерн) Специально напомнили, чего избегаем: разбросанные finance.getConfig() по компонентам - и есть "анемия".

      Почему кажется, что "всё в React":
      Telegram mini app = SPA внутри webview. "Костыли" закрывают ограничения SDK (одноразовый init, deep links, TTL подписи) - каждый обёрнут в хук вместо дублирования кода.
      Отправка транзакций вынесена отдельно. Сеть переключаем стратегией. Цвета и шрифты управляются единой темой.
      На практике это снизило боль: обновление TON SDK заняло минуту, без глобального поиска замены. Критику учту - всегда есть, что улучшить, удачи!


      1. fransua
        12.05.2025 20:41

        Использованные паттерны хорошие, я не спорю, они помогают, только они называются по-другому, или же не имеют названия. Если некая конструкция имеет одну цель - инкапсуляция некоторой ответственности внутри одной сущности - то это SRP, очень хороший паттерн, не надо его называть иначе.

        1. MVVM - там должно быть 3 составляющих Model, View и ViewModel. React не реализует ViewModel, а хуки-шлюзы сложно назвать моделью даже с натяжкой. Пример касается вынесения навигации в 1 view - хороший SRP, но проблема с использованием данных роутера вне View. Например, в Gateway/Repository вам понадобится текущая страница для какого-нибудь редиреста с returnUrl. Ее придется прокидывать через параметры из View.

        2. Abstract Factory - используется когда нужно создавать много объектов и существует несколько реализаций. Если бы у вас было несколько разных createTheme: createMobileTheme, createDesktopTheme и они использовались с разными параметрами createTheme('dark'), createTheme('light') то это была бы фабрика. Сейчас скорей всего Provider. Он ничем не хуже/лучше фабрики, просто уместнее в данном случае.

        3. Repository/Gateway - это разные паттерны. Здесь скорее Adapter для внешнего сервиса. и так далее.

        Но все эти паттерны не должны лежать в области View - в реакте. Если поменять представление на API или CLI или Mobile или кофеварку - надо будет переписывать все. Стоило реализовать логику как отдельную коробочку на JS/TS с нужными адаптерами. И в реакте уже использовать RichModel, если не нравится анемичная, или сервис с методами { async init(); async getFinance(); getUser(); getTransaction() }.


  1. kubk
    12.05.2025 20:41

    То есть простой try / catch теперь называется "Graceful Degradation / Fail‑safe вместо Fail‑Fast Crash"? Можно было просто показать как делать и как не делать. Сейчас объяснения примеров сделаны через AI и надуманы, статью было тяжело дочитать.


  1. Vitaly_js
    12.05.2025 20:41

    1. Паттерн — Lifecycle hook / Template Method.

    Если это действительно шаблонный метод, то проверять доступность объекта он не должен. А ваше желание использовать глобальную переменную сыграло с вами злую шутку. И вы назвали шаблонным методом то, что по сути выполняет две задачи: непосредственно операции и проверку доступности.

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

    Если предположить, что будут разные реализации объектов WebApp то все они будут использовать одну глобальную переменную.

    Таким образом это решение навеяно шаблонным методом, но им не является.

    2. Паттерн — Singleton + Guard Clause.

    Строго говоря, никакого сингтона обнаружить не удалось. Тут вообще нет никаких абстракций. Есть набор каких-то реализаций, которые можно куда угодно в таком виде вставлять.

    3. Паттерн — Front Controller (Router)

    Т.е. не писать простыню всего чего только можно в одном месте - это фронт контроллер?

    4. Паттерн — Strategy. Сеть меняется конфигом, код не трогается.

    А в чем стратегия то? У вас прямая зависимость от TonClient. Простыня из настроек объекта этого класса. Да и само название useTonClient, т.е. явно ориентированное ровно на TonClient. Где стратегия?

    5. Паттерн — Lazy Factory.

    Честно говоря, не понял что тут вообще происходит. Вы написали хук useAsyncInitialize. По семантике хуков, они выполняются всегда и сразу. А где тогда lazy? Кто собственно выполняет отложенное создание компонента с этим хуком? И если он это выполняет, то причем тут тогда данный хук?

    6. Паттерн — Adapter

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

    Всю статью уже не могу осилить...