Skip to content

@tma.js/sdk-react

React JS bindings for client SDK. Includes hooks, components and utilities for comfortable usage of React JS on the Telegram Mini Apps platform.

Installation

Before anything else, it is assumed that you have already installed the react package, as it is a peer dependency of this package.

bash
pnpm i @tma.js/sdk-react
pnpm i @tma.js/sdk-react
bash
npm i @tma.js/sdk-react
npm i @tma.js/sdk-react
bash
yarn add @tma.js/sdk-react
yarn add @tma.js/sdk-react

Initialization

SDKProvider is a component responsible for providing the SDK functionality. Internally, it utilizes the init function from @tma.js/sdk. It accepts an optional list of parameters through the options property.

jsx
import { SDKProvider, type InitOptions } from '@tma.js/sdk-react';

/**
 * Root component for the whole project.
 */
export function Root() {
  const options: InitOptions = {
    acceptCustomStyles: true,
    cssVars: true,
  };

  return (
    <SDKProvider options={options}>
      <div>My application!</div>
    </SDKProvider>
  );
}
import { SDKProvider, type InitOptions } from '@tma.js/sdk-react';

/**
 * Root component for the whole project.
 */
export function Root() {
  const options: InitOptions = {
    acceptCustomStyles: true,
    cssVars: true,
  };

  return (
    <SDKProvider options={options}>
      <div>My application!</div>
    </SDKProvider>
  );
}

Asynchronous

SDK initialization is considered asynchronous in case the async init option equals true. In this case, SDKProvider will start initialization using the React useEffect hook. This mode is useful in case the developer is using Server-Side Rendering or when the valid state of all components is required.

Nevertheless, as long as this process is asynchronous, developers should be careful when calling package-specific component hooks, such as useBackButton, useMainButton, etc. When the SDK is not yet initialized, these hooks will throw an error as long as they will be unable to retrieve the component from the SDK.

jsx
import {
  SDKProvider,
  useMainButton,
  type SDKInitOptions,
} from '@tma.js/sdk-react';

function App() {
  const mainButton = useMainButton();
  // We will get an error here as long as SDK is not yet initialized.
  // The solution will be provided further.
  return <div>My application!</div>;
}

/**
 * Root component for the whole project.
 */
export function Root() {
  return (
    <SDKProvider options={{ async: true }}>
      <App/>
    </SDKProvider>
  );
}
import {
  SDKProvider,
  useMainButton,
  type SDKInitOptions,
} from '@tma.js/sdk-react';

function App() {
  const mainButton = useMainButton();
  // We will get an error here as long as SDK is not yet initialized.
  // The solution will be provided further.
  return <div>My application!</div>;
}

/**
 * Root component for the whole project.
 */
export function Root() {
  return (
    <SDKProvider options={{ async: true }}>
      <App/>
    </SDKProvider>
  );
}

Synchronous

SDK initialization is considered synchronous in case the async init option is omitted or equals false. In this case, SDKProvider will call the init function on the first render (before the mount). This simplifies the application structure and allows avoiding the definition of intermediate components.

jsx
import { SDKProvider, useMainButton } from '@tma.js/sdk-react';

function App() {
  const mainButton = useMainButton();
  // There will be no error here as long as initialization is synchronous
  // and at this moment SDK is already initialized.
  return <div>My application!</div>;
}

/**
 * Root component for the whole project.
 */
export function Root() {
  return (
    <SDKProvider>
      <App/>
    </SDKProvider>
  );
}
import { SDKProvider, useMainButton } from '@tma.js/sdk-react';

function App() {
  const mainButton = useMainButton();
  // There will be no error here as long as initialization is synchronous
  // and at this moment SDK is already initialized.
  return <div>My application!</div>;
}

/**
 * Root component for the whole project.
 */
export function Root() {
  return (
    <SDKProvider>
      <App/>
    </SDKProvider>
  );
}

INFO

The default initialization accepted by the package is synchronous.

Getting SDK context

Retrieving the SDK context is important in asynchronous mode to prevent the application from crashing. To get the current SDK context, it is required to use the useSDKContext hook or the withSDKContext higher-order component.

jsx
import {
  SDKProvider,
  useSDKContext,
  withSDKContext,
  type SDKContextType,
} from '@tma.js/sdk-react';

function App() {
  const sdk = useSDKContext();
  // Here, we can use SDK information.
  return <div>My application!</div>;
}

// or
const AppWrapped = withSDK(({ sdk }) => {
  // Here, we can use SDK information.
  return <div>My application!</div>;
});

function Root() {
  return (
    <SDKProvider>
      <App/>
      <AppWrapped/>
    </SDKProvider>
  );
}
import {
  SDKProvider,
  useSDKContext,
  withSDKContext,
  type SDKContextType,
} from '@tma.js/sdk-react';

function App() {
  const sdk = useSDKContext();
  // Here, we can use SDK information.
  return <div>My application!</div>;
}

// or
const AppWrapped = withSDK(({ sdk }) => {
  // Here, we can use SDK information.
  return <div>My application!</div>;
});

function Root() {
  return (
    <SDKProvider>
      <App/>
      <AppWrapped/>
    </SDKProvider>
  );
}

Let's enhance the previous example and introduce crucial logic associated with the SDK lifecycle:

jsx
import { useEffect, type PropsWithChildren } from 'react';
import {
  SDKProvider,
  useSDKContext,
  useBackButton,
  useMiniApp,
} from '@tma.js/sdk-react';

/**
 * Part of the application which doesn't know anything about SDK
 * initialization and which should be rendered only in case, SDK is already
 * initialized and could provide init result.
 */
function App() {
  const backButton = useBackButton();
  const miniApp = useMiniApp();

  // When App is attached to DOM, lets show back button and
  // add "click" event handler, which should close current application.
  // When component will unmount, listener will be removed.
  useEffect(() => {
    backButton.show();
    return backButton.on('click', () => miniApp.close());
  }, []);

  return <div>My application!</div>;
}

/**
 * This component is the layer controlling the application display.
 * It displays application in case, the SDK is initialized, displays an error
 * if something went wrong, and a loader if the SDK is warming up.
 */
function Loader({ children }: PropsWithChildren) {
  const { loading, initResult, error } = useSDKContext();

  // Each value is falsy. It means, init function was not called.
  if (!loading && !error && !initResult) {
    return <div>SDK init function is not yet called.</div>;
  }

  // Error occurred during SDK init.
  if (error) {
    return <div>Something went wrong.</div>;
  }

  // SDK is currently initializing.
  if (loading) {
    return <div>Warming up SDK.</div>;
  }

  // Safely render application.
  return <>{children}</>;
}

/**
 * Root component of the whole project.
 */
export function Root() {
  return (
    <SDKProvider options={{ async: true }}>
      <Loader>
        <App/>
      </Loader>
    </SDKProvider>
  );
}
import { useEffect, type PropsWithChildren } from 'react';
import {
  SDKProvider,
  useSDKContext,
  useBackButton,
  useMiniApp,
} from '@tma.js/sdk-react';

/**
 * Part of the application which doesn't know anything about SDK
 * initialization and which should be rendered only in case, SDK is already
 * initialized and could provide init result.
 */
function App() {
  const backButton = useBackButton();
  const miniApp = useMiniApp();

  // When App is attached to DOM, lets show back button and
  // add "click" event handler, which should close current application.
  // When component will unmount, listener will be removed.
  useEffect(() => {
    backButton.show();
    return backButton.on('click', () => miniApp.close());
  }, []);

  return <div>My application!</div>;
}

/**
 * This component is the layer controlling the application display.
 * It displays application in case, the SDK is initialized, displays an error
 * if something went wrong, and a loader if the SDK is warming up.
 */
function Loader({ children }: PropsWithChildren) {
  const { loading, initResult, error } = useSDKContext();

  // Each value is falsy. It means, init function was not called.
  if (!loading && !error && !initResult) {
    return <div>SDK init function is not yet called.</div>;
  }

  // Error occurred during SDK init.
  if (error) {
    return <div>Something went wrong.</div>;
  }

  // SDK is currently initializing.
  if (loading) {
    return <div>Warming up SDK.</div>;
  }

  // Safely render application.
  return <>{children}</>;
}

/**
 * Root component of the whole project.
 */
export function Root() {
  return (
    <SDKProvider options={{ async: true }}>
      <Loader>
        <App/>
      </Loader>
    </SDKProvider>
  );
}

You might wonder why we need a component like Loader. The reason is that the SDK initialization process can be asynchronous. Some of its components need to send requests to the Telegram application to fetch their current state. Due to this, we cannot determine the required properties for these components until the initialization is completed.

As a result, all hooks that return component instances will throw an error because they cannot retrieve the necessary component from the initResult property. Therefore, these hooks should not be called until the SDK is fully initialized.

When init is done

Once the initialization is successfully completed, developers should call the miniApp.ready() method. It notifies the Telegram application that the current Mini App is ready to be displayed.

jsx
import { useEffect } from 'react';
import { useMiniApp } from '@tma.js/sdk-react';

function App() {
  const miniApp = useMiniApp();

  useEffect(() => {
    miniApp.ready();
  }, []);

  return <div>Here is my App</div>;
}
import { useEffect } from 'react';
import { useMiniApp } from '@tma.js/sdk-react';

function App() {
  const miniApp = useMiniApp();

  useEffect(() => {
    miniApp.ready();
  }, []);

  return <div>Here is my App</div>;
}

Hooks and HOCs

Launch parameters

There may be cases where a developer needs to retrieve launch parameters without initializing the entire SDK. For example, they might want to access current theme parameters stored in window.location. In such cases, SDK initialization may not be necessary.

To retrieve Mini App launch parameters, the useLaunchParams hook (or the withLaunchParams higher-order component) can be used.

jsx
import {
  useLaunchParams,
  withLaunchParams,
  type LaunchParams,
} from '@tma.js/sdk-react';

function DisplayLaunchParams() {
  const launchParams = useLaunchParams();
  return <pre><code>{JSON.stringify(launchParams, null, ' ')}</code></pre>;
}

// or

const DisplayLaunchParamsWrapped = withLaunchParams(({ launchParams }) => {
  return <pre><code>{JSON.stringify(launchParams, null, ' ')}</code></pre>;
});
import {
  useLaunchParams,
  withLaunchParams,
  type LaunchParams,
} from '@tma.js/sdk-react';

function DisplayLaunchParams() {
  const launchParams = useLaunchParams();
  return <pre><code>{JSON.stringify(launchParams, null, ' ')}</code></pre>;
}

// or

const DisplayLaunchParamsWrapped = withLaunchParams(({ launchParams }) => {
  return <pre><code>{JSON.stringify(launchParams, null, ' ')}</code></pre>;
});

The library provides a collection of simple hooks and higher-order components (HOCs) for each init result value. Here is the list of following hooks and corresponding higher-order components:

  • useBackButton, withBackButton. Returns the BackButton component
  • useClosingBehavior, withClosingBehavior. Returns the ClosingBehavior component
  • useCloudStorage, withCloudStorage. Returns the CloudStorage component
  • useHapticFeedback, withHapticFeedback. Returns the HapticFeedback component
  • useInitData, withInitData. Returns the InitData component
  • useInitDataRaw, withInitDataraw. Returns init data raw representation
  • useInvoice, withInvoice. Returns the Invoice component
  • useMainButton, withMainButton. Returns the MainButton component
  • useMiniApp, withMiniApp. Returns the MiniApp component
  • usePopup, withPopup. Returns the Popup component
  • usePostEvent, withPostEvent. Returns the postEvent function to call Telegram Mini Apps methods
  • useQRScanner, withQRScanner. Returns the QRScanner component
  • useThemeParams, withThemeParams. Returns the ThemeParams component
  • useUtils, withUtils. Returns the Utils component
  • useViewport, withViewport. Returns the Viewport component

TIP

Each of these hooks and higher-order components returns SDK components that are automatically updated if some of their properties change. As a result, the hook or HOC will return a new instance of the component.

DANGER

Using these hooks and higher-order components with an uninitialized SDK will result in throwing a corresponding error.

Declarative vs Imperative

This package does not offer declarative components that can be used during development. This section of the documentation provides information on why this solution was chosen.

The main difference between declarative and imperative approaches is that the declarative approach hides the implementation behind a simple entity. In contrast, the imperative approach requires describing the process step-by-step.

This makes the imperative approach more flexible and intuitive, as long as the developer can see each step hidden behind the process.

Declarative

Let's start from the example of how we could use the declarative approach in this package:

jsx
import { MainButton } from '@tma.js/sdk-react';

function App() {
  return (
    <div>
      <p>Just an example</p>
      <MainButton text='Submit' textColor='#aabb01'/>
    </div>
  );
}
import { MainButton } from '@tma.js/sdk-react';

function App() {
  return (
    <div>
      <p>Just an example</p>
      <MainButton text='Submit' textColor='#aabb01'/>
    </div>
  );
}

This code will have to display the Main Button, set text Submit and the color of text equal to #aabb01. This usage of the component seems comfortable.

Imperative

Now, let's see the imperative alternative of the same code:

jsx
import { useEffect } from 'react';
import { useMainButton } from '@tma.js/sdk-react';

function App() {
  const mainButton = useMainButton();

  useEffect(() => {
    mainButton.setParams({
      text: 'Submit',
      textColor: '#aabb01'
    });
  }, []);

  return (
    <div>
      <p>Just an example</p>
    </div>
  );
}
import { useEffect } from 'react';
import { useMainButton } from '@tma.js/sdk-react';

function App() {
  const mainButton = useMainButton();

  useEffect(() => {
    mainButton.setParams({
      text: 'Submit',
      textColor: '#aabb01'
    });
  }, []);

  return (
    <div>
      <p>Just an example</p>
    </div>
  );
}

As you can see, the imperative approach requires writing more code. Nevertheless, this implication has more advantages than disadvantages.

Comparison

The declarative approach seems more comfortable, requiring less code to write but has at least one disadvantage, which becomes more visible in larger projects. This drawback manifests when there are several application components using the MainButton component. All these parent components could be placed anywhere in the application.

You can imagine such a case with a simplified structure like this:

jsx
import { MainButton } from '@tma.js/sdk-react';

function ComponentA() {
  return (
    <MainButton
      text='Submit'
      textColor='#aabb01'
      onClick={...}
    />
  );
}

function ComponentB() {
  return (
    <MainButton
      text='Cancel'
      textColor='#010101'
      onClick={...}
      backgroundColor='#000000'
    />
  );
}

function ParentOfA() {
  return (
    <div>
      ...
      <ComponentA/>
    </div>
  );
}

function Root() {
  return (
    <div>
      <ParentOfA/>
      ...
      <div>
        ...
        <div>
          ...
          <ComponentB/>
        </div>
      </div>
    </div>
  );
}
import { MainButton } from '@tma.js/sdk-react';

function ComponentA() {
  return (
    <MainButton
      text='Submit'
      textColor='#aabb01'
      onClick={...}
    />
  );
}

function ComponentB() {
  return (
    <MainButton
      text='Cancel'
      textColor='#010101'
      onClick={...}
      backgroundColor='#000000'
    />
  );
}

function ParentOfA() {
  return (
    <div>
      ...
      <ComponentA/>
    </div>
  );
}

function Root() {
  return (
    <div>
      <ParentOfA/>
      ...
      <div>
        ...
        <div>
          ...
          <ComponentB/>
        </div>
      </div>
    </div>
  );
}

In this case, you don't really know what would happen if ComponentA or ComponentB were re-rendered. Will MainButton update all properties, or only the changed ones? Will they ever update anything in the Main Button? Which properties of the MainButton component would be applied? Well, there is no intuitive answer to these questions.

That's why we prefer using the imperative approach which directly notifies the developer of what action will be performed. This also allows developer to select the strategy he really needs - update all properties or maybe just some of them:

jsx
import { useEffect } from 'react';
import { useMainButton } from '@tma.js/sdk-react';

function ComponentA() {
  const mb = useMainButton();

  useEffect(() => {
    mb.setParams({
      text: 'Submit',
      textColor: '#aabb01',
    });

    return mb.on('click', ...);
  }, []);

  return null;
}

function ComponentB() {
  const mb = useMainButton();

  useEffect(() => {
    mb.setParams({
      text: 'Cancel',
      textColor: '#010101',
      backgroundColor: '#000000',
    });

    return mb.on('click', ...);
  }, []);

  return null;
}

function ParentOfA() {
  return (
    <div>
      ...
      <ComponentA/>
    </div>
  );
}

function Root() {
  return (
    <div>
      <ParentOfA/>
      ...
      <div>
        ...
        <div>
          ...
          <ComponentB/>
        </div>
      </div>
    </div>
  );
}
import { useEffect } from 'react';
import { useMainButton } from '@tma.js/sdk-react';

function ComponentA() {
  const mb = useMainButton();

  useEffect(() => {
    mb.setParams({
      text: 'Submit',
      textColor: '#aabb01',
    });

    return mb.on('click', ...);
  }, []);

  return null;
}

function ComponentB() {
  const mb = useMainButton();

  useEffect(() => {
    mb.setParams({
      text: 'Cancel',
      textColor: '#010101',
      backgroundColor: '#000000',
    });

    return mb.on('click', ...);
  }, []);

  return null;
}

function ParentOfA() {
  return (
    <div>
      ...
      <ComponentA/>
    </div>
  );
}

function Root() {
  return (
    <div>
      <ParentOfA/>
      ...
      <div>
        ...
        <div>
          ...
          <ComponentB/>
        </div>
      </div>
    </div>
  );
}

In this case, we know that when ComponentA and ComponentB are mounted, they will try to set specified parameters. No other re-renders will have an effect on the Main Button.

Released under the MIT License.