react-native-tdlib
Telegram · TDLib · iOS + Android

Build a real Telegram client
in React Native.

Official TDLib under the hood. Prebuilt binaries for both platforms, a single typed API for all 51 methods, and every Telegram update streaming live through NativeEventEmitter.

$ npm i react-native-tdlibMITRN ≥ 0.60
example/ — iOS Simulator

From zero to a live chat, on one screen.

See the full example app →

1. Install

npm install react-native-tdlib
cd ios && pod install

RN ≥ 0.60 · iOS ≥ 11 · Android minSdk ≥ 21

2. Boot and listen

app.ts
import TdLib from 'react-native-tdlib';
import { NativeEventEmitter, NativeModules } from 'react-native';

await TdLib.startTdLib({ api_id: 12345, api_hash: 'your_hash' });

const emitter = new NativeEventEmitter(NativeModules.TdLibModule);

emitter.addListener('tdlib-update', e => {
  if (e.type === 'updateAuthorizationState') {
    // drive phone → code → password from here
  }
  if (e.type === 'updateNewMessage') {
    console.log('📨', JSON.parse(e.raw).message);
  }
});
Wrapped methods
51
Android ABIs prebuilt
3
Lines of cmake on your end
0
Released Apr 21, 2026
v2.2.1

The problem

Building a Telegram client shouldn’t start with compiling 200 MB of C++.

TDLib hands you the whole Telegram protocol — auth, chats, messages, reactions, files, real-time updates. But only if you’re ready to wrestle with CMake on both platforms, ship your own xcframework, and write platform-specific bridges that disagree on JSON shapes.

react-native-tdlib ships all of that. You write the app.

Is this for you?

Two-minute fit check.

Reach for this if

  • You're shipping a Telegram experience on real user accounts, not bots.
  • You need iOS and Android on the same JSON shape, same types, same TDLib version.
  • You'd rather write app code than maintain a CMake toolchain on both platforms.

Pick something else if

  • Your bot lives server-side — node-telegram-bot-api is one npm install away.
  • You're on Expo Go — native modules don't run there. EAS Build / bare RN works.
  • You only need chat UI, not the protocol — reach for a chat-kit instead.

What you get

TDLib, wrapped where it hurts.

Prebuilt binaries

No cmake, no brew install. npm install, pod install, and the whole of TDLib ships with your app — iOS xcframework (device + simulator), Android arm64-v8a, armeabi-v7a, x86_64.

Cross-platform parity

iOS and Android emit the exact same TDLib JSON — snake_case keys, @type markers. One handler, one set of types, both stores.

TypeScript definitions

Every wrapped method in `index.d.ts` — parameters, return shapes, input helpers. Update events arrive as `{ type, raw }` so you parse the TDLib JSON yourself, with the shape you expect.

The surface

The TDLib surface, wrapped.

Authentication

Reactive auth, not a fixed sequence.

TDLib doesn’t follow a rigid login flow. It may skip to registration, request email confirmation, or go straight to Ready for returning users. Drive your auth UI from updateAuthorizationState and your app adapts to whatever Telegram sends.
emitter.addListener('tdlib-update', e => {
  if (!e.type.startsWith('updateAuthorizationState')) return;
  const state = JSON.parse(e.raw).authorization_state['@type'];

  switch (state) {
    case 'authorizationStateWaitPhoneNumber': /* show phone input */ break;
    case 'authorizationStateWaitCode':        /* show SMS code input */ break;
    case 'authorizationStateWaitPassword':    /* show 2FA input */ break;
    case 'authorizationStateReady':           /* ✅ logged in */ break;
  }
});

Real-time updates

Event-driven by design. No polling.

Every change — new messages, typing, read receipts, download progress, reactions, online status — flows through a single tdlib-update event. Subscribe once, fan out to the reducers that care.
const emitter = new NativeEventEmitter(NativeModules.TdLibModule);

const sub = emitter.addListener('tdlib-update', event => {
  const { type, raw } = event;
  const data = JSON.parse(raw);

  if (type === 'updateNewMessage')     console.log('message', data.message);
  if (type === 'updateChatAction')     console.log('typing',  data.action);
  if (type === 'updateFile')           console.log('file',    data.file);
  if (type === 'updateUserStatus')     console.log('status',  data.status);
});

sub.remove(); // on unmount

Chats & messages

Load, send, react — one-liners on both platforms.

All 51 methods — chats, messages, reactions, replies, files, options, users — are wrapped, typed, and tested on iOS and Android. Same signatures, same error codes, same JSON shapes.
// Load and list chats
await TdLib.loadChats(25);
const chats = JSON.parse(await TdLib.getChats(25));

// Send a message or reply
await TdLib.sendMessage(chatId, 'Hello');
await TdLib.sendMessage(chatId, 'Replying', messageId);

// Reactions
await TdLib.addMessageReaction(chatId, messageId, '❤️');
await TdLib.removeMessageReaction(chatId, messageId, '❤️');

Files

Stream downloads, render thumbnails while you wait.

Kick off downloads fire-and-forget via td_json_client_send so the bridge stays free. Watch updateFile for progress and render the TDLib-provided mini-thumbnail until the full image arrives.
TdLib.td_json_client_send({
  '@type': 'downloadFile',
  file_id: fileId,
  priority: 1,
  synchronous: false,
});

emitter.addListener('tdlib-update', e => {
  if (e.type !== 'updateFile') return;
  const { file } = JSON.parse(e.raw);
  if (file.id === fileId && file.local?.is_downloading_completed) {
    setLocalPath(`file://${file.local.path}`);
  }
});

Typing indicators

Broadcast and display typing in real time.

Send chatActionTyping while the user is in the input field. Read updateChatAction to show the familiar “someone is typing…” bubble.
// Tell the server you're typing
TdLib.td_json_client_send({
  '@type': 'sendChatAction',
  chat_id: chatId,
  action: { '@type': 'chatActionTyping' },
});

// Render "someone is typing…" for incoming actions
emitter.addListener('tdlib-update', e => {
  if (e.type !== 'updateChatAction') return;
  const { chat_id, action } = JSON.parse(e.raw);
  setTyping(chat_id, action['@type'] !== 'chatActionCancel');
});

Cross-platform parity

Write one handler. Ship both stores.

A Gson adapter on Android ensures every Java object serializes with snake_case keys and @type markers — identical to the canonical JSON iOS emits directly. No platform branches, no shape-by-shape parsing.
// Same event payload on iOS and Android:
{
  type: 'updateNewMessage',
  raw: '{"@type":"updateNewMessage","message":{'
     + '"chat_id":1234567890,"id":100,"date":1700000000,'
     + '"content":{"@type":"messageText","text":{"@type":"formattedText","text":"hi"}}'
     + '}}',
}

Escape hatch

The whole TDLib surface is one call away.

51 methods are wrapped with typed signatures. Everything else — setChatTitle, searchMessages, setProfilePhoto, whatever TDLib adds next — is reachable via td_json_client_send. Fire-and-forget; the response lands on the same update stream.
// Anything not yet wrapped — call it directly.
// The response (or update) arrives on the same stream.
TdLib.td_json_client_send({
  '@type': 'setChatTitle',
  chat_id: chatId,
  title: 'New title',
});

GitHub

Star it if it works for you.

Stars are the discovery signal for React Native wrappers on GitHub.