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.
From zero to a live chat, on one screen.
See the full example app →1. Install
npm install react-native-tdlib
cd ios && pod installRN ≥ 0.60 · iOS ≥ 11 · Android minSdk ≥ 21
2. Boot and listen
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.
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.
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 unmountChats & messages
Load, send, react — one-liners on both platforms.
// 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.
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.
// 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.
// 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.
// 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.