0Pricing
React Native Academy · درس

ردود الاستدعاء والوعود والأحداث غير المتزامنة من الشيفرة الأصلية

أعيدوا البيانات من الشيفرة الأصلية باستخدام Promises وRCTPromiseResolveBlock، وأرسلوا الأحداث إلى JavaScript باستخدام RCTEventEmitter، وعالجوها في JS باستخدام NativeEventEmitter.

ردود الاستدعاء والوعود والأحداث غير المتزامنة من الشيفرة الأصلية درس مجاني في React Native Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في React Native Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة React Native Academy 4 دروس في المجموع.

بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.

Why Native Methods Must Be Async

React Native native module methods run on a native background thread, not the JavaScript thread. Because the two runtimes run independently, native code cannot return a value directly. Instead it must communicate results back to JavaScript through one of three patterns: Callbacks, Promises, or Events. Each pattern suits different use cases based on how many responses you need and when they arrive.

Callbacks: The Original Pattern

Callbacks are JavaScript functions passed as arguments to a native method. The native code stores them and invokes them later. The convention is to pass two callbacks — one for success and one for failure — similar to Node.js error-first callbacks. A callback can only be invoked once; invoking it twice throws a runtime error on the JS side.

// Kotlin
@ReactMethod
fun readFile(
    path: String,
    successCallback: Callback,
    errorCallback: Callback
) {
    try {
        val content = java.io.File(path).readText()
        successCallback.invoke(content)
    } catch (e: Exception) {
        errorCallback.invoke(e.message)
    }
}

// JavaScript
NativeModules.FileModule.readFile(
  '/data/test.txt',
  (content) => console.log(content),
  (error) => console.error(error)
);

Promises: The Modern Approach

Promises are now the preferred pattern for native methods that return a single result. Add Promise as the final parameter in Kotlin or RCTPromiseResolveBlock / RCTPromiseRejectBlock in Swift. React Native automatically wraps the call in a JS Promise, so you can await it or chain .then(). Promises are self-documenting and integrate naturally with async/await in modern JavaScript.

// Kotlin
@ReactMethod
fun fetchUserData(userId: String, promise: Promise) {
    Thread {
        try {
            val data = apiClient.getUser(userId)
            val map = Arguments.createMap()
            map.putString('name', data.name)
            map.putString('email', data.email)
            promise.resolve(map)
        } catch (e: Exception) {
            promise.reject('FETCH_ERROR', e.message, e)
        }
    }.start()
}

// JavaScript
try {
  const user = await NativeModules.UserModule.fetchUserData('123');
  console.log(user.name);
} catch (err) {
  console.error('Failed:', err.message);
}

Promise Rejection Codes and Messages

When rejecting a Promise, provide three pieces of information: an error code string (like 'PERMISSION_DENIED'), a human-readable message, and optionally the native exception object. On the JavaScript side these become properties of the caught Error: err.code, err.message, and err.nativeStackAndroid or err.nativeStackIOS for debugging.

// Kotlin — structured rejection
@ReactMethod
fun openCamera(promise: Promise) {
    val permission = ContextCompat.checkSelfPermission(
        reactApplicationContext,
        Manifest.permission.CAMERA
    )
    if (permission != PackageManager.PERMISSION_GRANTED) {
        promise.reject(
            'PERMISSION_DENIED',
            'Camera permission is not granted. Please enable it in settings.',
            null
        )
        return
    }
    // proceed...
    promise.resolve(true)
}

// JavaScript
try {
  await NativeModules.CameraModule.openCamera();
} catch (err) {
  if (err.code === 'PERMISSION_DENIED') showSettingsPrompt();
}

Events: Push Data from Native to JS

Events are the right tool when native needs to push data to JavaScript multiple times — such as location updates, sensor readings, download progress, or Bluetooth device discovery. Events flow one-way: native emits, JS listens. On Android you use RCTDeviceEventEmitter; on iOS you use RCTEventEmitter methods.

// Kotlin — emit an event
private fun sendEvent(name: String, data: WritableMap) {
    reactApplicationContext
        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
        .emit(name, data)
}

// Call this from a background task:
val map = Arguments.createMap()
map.putDouble('progress', 0.75)
map.putString('fileName', 'video.mp4')
sendEvent('downloadProgress', map)

Listening to Events in JavaScript

On the JS side, subscribe to native events using NativeEventEmitter. Pass it the native module object that emits events, then call addListener with the event name and a handler. Always store the subscription reference and call subscription.remove() in a cleanup function (e.g., inside useEffect's return) to prevent memory leaks from stale listeners.

import React, { useEffect, useState } from 'react';
import { NativeModules, NativeEventEmitter } from 'react-native';

const emitter = new NativeEventEmitter(NativeModules.DownloadModule);

export function DownloadScreen() {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const sub = emitter.addListener('downloadProgress', (event) => {
      setProgress(event.progress);
    });
    return () => sub.remove(); // cleanup
  }, []);

  return <ProgressBar value={progress} />;
}

addListener and removeListeners on iOS

On iOS, any class that extends RCTEventEmitter must implement two boilerplate methods: addListener(_:) and removeListeners(_:). These let the native module know when JS is actively listening so it can avoid emitting events to an empty audience. Failing to implement them causes a warning in development and may throw in newer RN versions.

// Swift
@objc(DownloadModule)
class DownloadModule: RCTEventEmitter {

  override func supportedEvents() -> [String]! {
    return ['downloadProgress', 'downloadComplete', 'downloadError']
  }

  // Required boilerplate
  override func addListener(_ eventName: String!) { }
  override func removeListeners(_ count: Double) { }

  func reportProgress(_ pct: Double, file: String) {
    sendEvent(
      withName: 'downloadProgress',
      body: ['progress': pct, 'fileName': file]
    )
  }
}

WritableArray for List Results

When you need to return an array from native to JavaScript, use WritableArray and Arguments.createArray(). You push items into it with typed methods like pushString, pushInt, and pushMap. Nested structures (maps inside arrays, arrays inside maps) work seamlessly — React Native serializes the entire tree.

// Kotlin
@ReactMethod
fun listBluetoothDevices(promise: Promise) {
    val array = Arguments.createArray()
    val devices = bluetoothAdapter?.bondedDevices ?: emptySet()
    for (device in devices) {
        val map = Arguments.createMap()
        map.putString('name', device.name)
        map.putString('address', device.address)
        array.pushMap(map)
    }
    promise.resolve(array)
}

// JavaScript
const devices = await NativeModules.BleModule.listBluetoothDevices();
devices.forEach(d => console.log(d.name, d.address));

Choosing Between Callbacks, Promises, and Events

Use this decision guide: Callbacks — legacy codebases or when you need exactly two outcomes (success/error) in one shot. Promises — any single async operation that resolves once; pairs perfectly with async/await. Events — when native must push multiple updates over time (streams, sensors, ongoing background tasks). Most new code should prefer Promises for one-time results and Events for streams.

// Decision chart as comments
// One result, awaitable? → Promise
const photo = await NativeModules.Camera.takePhoto();

// Multiple results over time? → Event emitter
const sub = emitter.addListener('locationUpdate', handleLocation);
NativeModules.LocationModule.startWatching();

// Legacy API you must support? → Callback
NativeModules.OldModule.doThing(onSuccess, onError);

Thread Safety for Event Emission

A common bug is emitting events from a background thread without a listener registered yet, causing a crash or silent drop. On Android, guard with a listener count check. On iOS, RCTEventEmitter handles this internally — sending to zero listeners is silently ignored. Always start background work (GPS polling, BLE scanning) only after the JS side has called the start method, not in the module's initializer.

// Kotlin — safe event emit with listener guard
private var listenerCount = 0

@ReactMethod
fun addListener(eventName: String) {
    listenerCount++
}

@ReactMethod
fun removeListeners(count: Int) {
    listenerCount -= count
}

private fun safeSendEvent(name: String, data: WritableMap) {
    if (listenerCount > 0) {
        reactApplicationContext
            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
            .emit(name, data)
    }
}

Testing Native Module Callbacks in Jest

When unit-testing components that use native modules, mock the module in Jest setup. Replace the native module with a Jest mock object that returns resolved Promises or calls callbacks with test data. This lets you test the JS side in isolation without a real device. Place module mocks in __mocks__/react-native.js or a setup file declared in jest.config.js.

// __mocks__/NativeModules.js
jest.mock('react-native', () => ({
  ...jest.requireActual('react-native'),
  NativeModules: {
    CameraModule: {
      takePhoto: jest.fn(() => Promise.resolve('/path/to/photo.jpg')),
      openCamera: jest.fn(() => Promise.resolve(true)),
    },
    DownloadModule: {},
  },
}));

// In your test
it('captures a photo and sets image URI', async () => {
  const { getByTestId } = render(<CameraScreen />);
  fireEvent.press(getByTestId('shutter-btn'));
  await waitFor(() =>
    expect(getByTestId('preview').props.source).toEqual(
      { uri: '/path/to/photo.jpg' }
    )
  );
});

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: how to return data from native using Callbacks, Promises, and Events, how to use WritableMap and WritableArray to pass complex data structures, and how to listen to native events safely in React components with useEffect cleanup. You also saw how to mock native modules in Jest for unit testing. Next up we explore Expo Config Plugins.

الأسئلة الشائعة

هل درس «ردود الاستدعاء والوعود والأحداث غير المتزامنة من الشيفرة الأصلية» مجاني؟

نعم — نص درس «ردود الاستدعاء والوعود والأحداث غير المتزامنة من الشيفرة الأصلية» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة React Native Academy، انتقل إلى CoddyKit PRO. تتضمن دورة React Native Academy 4 دروس في المجموع.

ماذا ستتعلم في «ردود الاستدعاء والوعود والأحداث غير المتزامنة من الشيفرة الأصلية»؟

أعيدوا البيانات من الشيفرة الأصلية باستخدام Promises وRCTPromiseResolveBlock، وأرسلوا الأحداث إلى JavaScript باستخدام RCTEventEmitter، وعالجوها في JS باستخدام NativeEventEmitter. تتمرن على React Native Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ React Native Academy؟

لا تُشترط خبرة سابقة. React Native Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «ردود الاستدعاء والوعود والأحداث غير المتزامنة من الشيفرة الأصلية»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس React Native Academy هذا؟

نعم. كل درس في React Native Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. كتابة وحدة أصلية قديمة باستخدام Kotlin
  2. كتابة وحدة أصلية قديمة باستخدام Swift
  3. وحدات Turbo الأصلية باستخدام JSI
  4. ردود الاستدعاء والوعود والأحداث غير المتزامنة من الشيفرة الأصلية
← العودة إلى React Native Academy