Loading...
Loading...
Comprehensive guide for creating Telegram Mini Apps with React using @tma.js/sdk-react. Covers SDK initialization, component mounting, signals, theming, back button handling, viewport management, init data, deep linking, and environment mocking for development. Use when building or debugging Telegram Mini Apps with React.
npx skill4agent add nailorsh/agents_utils telegram-mini-apps-react@tma.js/sdk-react# For React projects, install the React-specific package
pnpm i @tma.js/sdk-react
# DO NOT install both @tma.js/sdk and @tma.js/sdk-react - this causes bugs!Important: Thepackage fully re-exports@tma.js/sdk-react, so you don't need to install them separately.@tma.js/sdk
pnpm dlx @tma.js/create-mini-app@latest
# or
npx @tma.js/create-mini-app@latestsrc/
├── main.tsx # Entry point - SDK initialization
├── init.ts # SDK configuration and component mounting
├── mockEnv.ts # Development environment mocking
├── App.tsx # Main React app with routing
├── components/
│ ├── Page.tsx # Page wrapper with back button handling
│ └── EnvUnsupported.tsx # Fallback for non-TG environments
├── hooks/
│ └── useDeeplink.ts # Deep linking handler
└── services/
└── analytics.ts # Analytics with user dataimport {
init as initSDK,
setDebug,
themeParams,
miniApp,
viewport,
backButton,
swipeBehavior,
initData
} from '@tma.js/sdk-react';
export async function init(options: {
debug: boolean;
eruda: boolean;
mockForMacOS: boolean;
}): Promise<void> {
// Enable debug mode for development
setDebug(options.debug);
// Initialize the SDK (REQUIRED before using any features)
initSDK();
// Mount components you'll use in the app
backButton.mount.ifAvailable();
initData.restore();
// Configure swipe behavior
if (swipeBehavior.isSupported()) {
swipeBehavior.mount();
swipeBehavior.disableVertical();
}
// Setup Mini App theming
if (miniApp.mount.isAvailable()) {
themeParams.mount();
miniApp.mount();
themeParams.bindCssVars(); // Binds theme to CSS variables
}
// Configure viewport
if (viewport.mount.isAvailable()) {
viewport.mount().then(() => {
viewport.bindCssVars();
viewport.requestFullscreen();
});
}
}import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { retrieveLaunchParams } from '@tma.js/sdk-react';
import { init } from './init';
import App from "./App";
import { EnvUnsupported } from "./components/EnvUnsupported";
// Mock environment for local development
import './mockEnv';
const root = ReactDOM.createRoot(document.getElementById('root')!);
try {
const launchParams = retrieveLaunchParams();
const { tgWebAppPlatform: platform } = launchParams;
const debug = (launchParams.tgWebAppStartParam || '').includes('debug')
|| import.meta.env.DEV;
await init({
debug,
eruda: debug && ['ios', 'android'].includes(platform),
mockForMacOS: platform === 'macos',
}).then(() => {
root.render(
<StrictMode>
<App/>
</StrictMode>,
);
});
} catch (e) {
// Show fallback UI when not in Telegram
root.render(<EnvUnsupported/>);
}import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { backButton, miniApp } from '@tma.js/sdk-react';
export function Page({ children, back = true }) {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
if (back) {
backButton.show();
// onClick returns a cleanup function
return backButton.onClick(() => {
const isDeeplink = location.state?.fromDeeplink;
const isFirstPage = !window.history.state || window.history.state.idx === 0;
if (isDeeplink || isFirstPage) {
miniApp.close(); // Close the Mini App
} else {
navigate(-1); // Go back in history
}
});
}
backButton.hide();
}, [back, navigate, location]);
return <>{children}</>;
}useSignalimport { useEffect } from 'react';
import { backButton, useSignal } from '@tma.js/sdk-react';
function BackButtonStatus() {
const isVisible = useSignal(backButton.isVisible);
useEffect(() => {
console.log('Back button is', isVisible ? 'visible' : 'hidden');
}, [isVisible]);
return null;
}import { initData } from '@tma.js/sdk-react';
function getUserId(): number | undefined {
try {
const user = initData.user();
return user?.id;
} catch (e) {
return undefined;
}
}
// Get start parameter (for deep linking)
const startParam = initData.startParam();import { retrieveLaunchParams, useLaunchParams } from '@tma.js/sdk-react';
// In component
function Component() {
const launchParams = useLaunchParams();
// launchParams.tgWebAppPlatform - 'ios', 'android', 'macos', 'tdesktop', 'web', 'weba'
// launchParams.tgWebAppVersion - SDK version supported by client
// launchParams.tgWebAppData - init data
// launchParams.tgWebAppThemeParams - theme colors
// launchParams.tgWebAppStartParam - custom start parameter
}
// Outside component
const launchParams = retrieveLaunchParams();import { themeParams, miniApp } from '@tma.js/sdk-react';
// During initialization
if (miniApp.mount.isAvailable()) {
themeParams.mount();
miniApp.mount();
themeParams.bindCssVars(); // Creates CSS variables like --tg-theme-bg-color
}--tg-theme-bg-color--tg-theme-text-color--tg-theme-hint-color--tg-theme-link-color--tg-theme-button-color--tg-theme-button-text-color--tg-theme-secondary-bg-color--tg-theme-header-bg-color--tg-theme-accent-text-color--tg-theme-section-bg-color--tg-theme-section-header-text-color--tg-theme-subtitle-text-color--tg-theme-destructive-text-colorimport { viewport } from '@tma.js/sdk-react';
if (viewport.mount.isAvailable()) {
viewport.mount().then(() => {
viewport.bindCssVars(); // Binds viewport dimensions to CSS
viewport.requestFullscreen(); // Request fullscreen mode
});
}/* Safe area insets */
padding-top: var(--tg-viewport-safe-area-inset-top, 0);
padding-bottom: var(--tg-viewport-safe-area-inset-bottom, 0);
/* Content safe area (for notch, etc.) */
padding-top: var(--tg-viewport-content-safe-area-inset-top, 0);
/* Viewport dimensions */
height: var(--tg-viewport-height);
width: var(--tg-viewport-width);.header {
padding-top: max(2rem, calc(var(--tg-viewport-content-safe-area-inset-top, 0) + var(--tg-viewport-safe-area-inset-top, 0)));
}
.footer {
padding-bottom: calc(1rem + var(--tg-viewport-safe-area-inset-bottom, 0));
}import { emitEvent, isTMA, mockTelegramEnv } from '@tma.js/sdk-react';
if (import.meta.env.DEV) {
if (!await isTMA('complete')) {
const themeParams = {
accent_text_color: '#6ab2f2',
bg_color: '#17212b',
button_color: '#5288c1',
button_text_color: '#ffffff',
destructive_text_color: '#ec3942',
header_bg_color: '#17212b',
hint_color: '#708499',
link_color: '#6ab3f3',
secondary_bg_color: '#232e3c',
section_bg_color: '#17212b',
section_header_text_color: '#6ab3f3',
subtitle_text_color: '#708499',
text_color: '#f5f5f5',
};
mockTelegramEnv({
onEvent(e) {
if (e.name === 'web_app_request_theme') {
return emitEvent('theme_changed', { theme_params: themeParams });
}
if (e.name === 'web_app_request_viewport') {
return emitEvent('viewport_changed', {
height: window.innerHeight,
width: window.innerWidth,
is_expanded: true,
is_state_stable: true,
});
}
if (e.name === 'web_app_request_safe_area') {
return emitEvent('safe_area_changed', { left: 0, top: 0, right: 0, bottom: 0 });
}
},
launchParams: new URLSearchParams([
['tgWebAppThemeParams', JSON.stringify(themeParams)],
['tgWebAppData', new URLSearchParams([
['auth_date', (Date.now() / 1000 | 0).toString()],
['hash', 'mock-hash'],
['signature', 'mock-signature'],
['user', JSON.stringify({ id: 1, first_name: 'Developer' })],
]).toString()],
['tgWebAppVersion', '8.4'],
['tgWebAppPlatform', 'tdesktop'],
]),
});
console.info('⚠️ Running in mocked Telegram environment');
}
}import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { initData } from "@tma.js/sdk-react";
export function useDeeplink() {
const navigate = useNavigate();
const processedRef = useRef(false);
useEffect(() => {
if (processedRef.current) return;
const startParam = initData.startParam();
if (!startParam) return;
processedRef.current = true;
try {
// startParam is base64url encoded
const base64 = startParam.replace(/-/g, '+').replace(/_/g, '/');
const decoded = atob(base64);
const params = new URLSearchParams(decoded);
const route = params.get('route');
if (route) {
navigate(route, { replace: true, state: { fromDeeplink: true } });
}
} catch (e) {
console.error("Failed to parse startParam:", e);
}
}, [navigate]);
}import { backButton } from '@tma.js/sdk-react';
// Option 1: Check before calling
if (backButton.show.isAvailable()) {
backButton.show();
}
// Option 2: Call only if available (safer, no-op if unavailable)
backButton.show.ifAvailable();
// Option 3: Mount only if available
backButton.mount.ifAvailable();// ❌ Wrong - will throw error
backButton.show();
// ✅ Correct
backButton.mount();
backButton.show();if (platform === 'macos') {
mockTelegramEnv({
onEvent(event, next) {
if (event.name === 'web_app_request_theme') {
const tp = themeParams.state() || retrieveLaunchParams().tgWebAppThemeParams;
return emitEvent('theme_changed', { theme_params: tp });
}
if (event.name === 'web_app_request_safe_area') {
return emitEvent('safe_area_changed', { left: 0, top: 0, right: 0, bottom: 0 });
}
next();
},
});
}@tma.js/sdk@tma.js/sdk-react// ❌ Wrong - causes bugs
{
"dependencies": {
"@tma.js/sdk": "^3.0.0",
"@tma.js/sdk-react": "^3.0.8"
}
}
// ✅ Correct - only the React package
{
"dependencies": {
"@tma.js/sdk-react": "^3.0.8"
}
}if (swipeBehavior.isSupported()) {
swipeBehavior.mount();
swipeBehavior.disableVertical(); // Prevents swipe-to-close
}import { retrieveRawInitData } from '@tma.js/sdk-react';
const initDataRaw = retrieveRawInitData();
fetch('https://api.example.com/auth', {
method: 'POST',
headers: {
Authorization: `tma ${initDataRaw}`,
},
});@tma.js/init-data-nodeinit()backButton.mount();
backButton.show();if (backButton.show.isAvailable()) {
backButton.show();
}