or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

app-utilities.mderror-handling.mdevent-communication.mdindex.mdnative-modules.mdnative-views.mdpermissions.mdplatform-utilities.mdshared-memory.mduuid-generation.md
tile.json

shared-memory.mddocs/

Shared Memory Management

Efficient memory sharing between JavaScript and native code with manual memory management capabilities for performance-critical scenarios.

Capabilities

SharedObject Class

Base class for objects that share memory between JavaScript and native code with manual lifecycle management.

/**
 * Base class for all shared objects that extends EventEmitter
 * Implementation is written in C++ and installed through JSI
 * Provides manual memory management for performance-critical scenarios
 */
class SharedObject<TEventsMap extends EventsMap = Record<never, never>>
  extends EventEmitter<TEventsMap>
  implements EventEmitter<TEventsMap> {
  
  /**
   * Detaches JS and native objects to allow native deallocation
   * before JS garbage collection. Subsequent native function calls will throw.
   * Use only when manual memory management is needed for performance.
   * React hooks usually handle this automatically (e.g., useVideoPlayer, useImage).
   */
  release(): void;
}

Usage Examples:

import { SharedObject } from "expo-modules-core";

// Example: Image processing shared object
type ImageEvents = {
  processingComplete: (result: ProcessingResult) => void;
  error: (error: Error) => void;
};

class ImageProcessor extends SharedObject<ImageEvents> {
  // Native methods would be bound here
  processImage?: (imageData: ArrayBuffer) => Promise<ProcessingResult>;
}

// Manual memory management
const processor = new ImageProcessor();

// Process image
processor.addListener("processingComplete", (result) => {
  console.log("Processing complete:", result);
  
  // Important: Release when done to free native memory immediately
  processor.release();
});

try {
  await processor.processImage?.(imageData);
} catch (error) {
  console.error("Processing failed:", error);
  processor.release(); // Always release on error too
}

SharedRef Class

Holds references to native objects, enabling efficient passing of native instances between different modules.

/**
 * A SharedObject that holds a reference to any native object
 * Allows passing references to native instances among independent libraries
 * without additional file system reads/writes
 */
class SharedRef<
  TNativeRefType extends string = 'unknown',
  TEventsMap extends EventsMap = Record<never, never>
> extends SharedObject<TEventsMap>
  implements SharedObject<TEventsMap> {
  
  /**
   * The type of the native reference (e.g., 'Drawable', 'UIImage')
   * Used for type checking when passing between modules
   */
  nativeRefType: string;
}

Usage Examples:

import { SharedRef } from "expo-modules-core";

// Example: Image reference that can be shared between modules
type ImageRef = SharedRef<'UIImage' | 'Drawable', {
  loaded: () => void;
  error: (error: Error) => void;
}>;

// Create image reference (typically done by expo-image)
declare function createImageRef(uri: string): ImageRef;

// Use in image manipulation (typically done by expo-image-manipulator)
declare function manipulateImage(imageRef: ImageRef, operations: Operation[]): Promise<ImageRef>;

// Use in image component (typically done by expo-image)
declare function displayImage(imageRef: ImageRef): void;

// Example usage
const originalImage = createImageRef("https://example.com/image.jpg");

// Listen for load event
originalImage.addListener("loaded", () => {
  console.log("Image loaded");
});

// Pass reference between modules efficiently (no file I/O)
const manipulatedImage = await manipulateImage(originalImage, [
  { resize: { width: 300, height: 200 } },
  { flip: { vertical: true } }
]);

// Display the manipulated image
displayImage(manipulatedImage);

// Check reference type
console.log("Native type:", manipulatedImage.nativeRefType); // 'UIImage' on iOS, 'Drawable' on Android

// Clean up when done
originalImage.release();
manipulatedImage.release();

useReleasingSharedObject Hook

React hook that automatically manages SharedObject lifecycle with component unmounting and dependency changes.

/**
 * React hook that returns a shared object with automatic cleanup
 * Object is automatically released when component unmounts or dependencies change
 * @param factory - Function that creates the shared object
 * @param dependencies - Dependency array like useEffect
 * @returns The shared object instance
 */
function useReleasingSharedObject<TSharedObject extends SharedObject>(
  factory: () => TSharedObject,
  dependencies: DependencyList
): TSharedObject;

Usage Examples:

import { useReleasingSharedObject } from "expo-modules-core";
import { useEffect, useState } from "react";

// Example: Audio player component
function AudioPlayer({ audioUri }: { audioUri: string }) {
  // SharedObject is automatically managed
  const audioPlayer = useReleasingSharedObject(
    () => createAudioPlayer(audioUri), // Factory function
    [audioUri] // Dependencies - player recreated when URI changes
  );
  
  const [isPlaying, setIsPlaying] = useState(false);
  
  useEffect(() => {
    // Listen for playback events
    const subscription = audioPlayer.addListener("playbackStatusChanged", (status) => {
      setIsPlaying(status.isPlaying);
    });
    
    return () => subscription.remove();
  }, [audioPlayer]);
  
  const togglePlayback = async () => {
    if (isPlaying) {
      await audioPlayer.pauseAsync();
    } else {
      await audioPlayer.playAsync();
    }
  };
  
  // No need to manually call release() - hook handles it automatically
  return (
    <button onClick={togglePlayback}>
      {isPlaying ? "Pause" : "Play"}
    </button>
  );
}

// Example: Image processor with dependency changes
function ImageProcessor({ imageUri, filters }: { imageUri: string; filters: Filter[] }) {
  const processor = useReleasingSharedObject(
    () => createImageProcessor(imageUri, filters),
    [imageUri, filters] // Processor recreated when image or filters change
  );
  
  const [processedImage, setProcessedImage] = useState<string | null>(null);
  
  useEffect(() => {
    processor.processAsync().then(setProcessedImage);
  }, [processor]);
  
  return processedImage ? <img src={processedImage} /> : <div>Processing...</div>;
}

Memory Management Best Practices

Guidelines for efficient memory management with shared objects:

import { SharedObject, useReleasingSharedObject } from "expo-modules-core";

// ✅ Good: Use React hook for automatic management
function GoodComponent({ uri }: { uri: string }) {
  const sharedObject = useReleasingSharedObject(
    () => createSharedObject(uri),
    [uri]
  );
  // Automatically released when component unmounts or uri changes
  return <div>Using shared object</div>;
}

// ⚠️ Manual management - only when necessary
function ManualComponent({ uri }: { uri: string }) {
  const [sharedObject, setSharedObject] = useState<SharedObject | null>(null);
  
  useEffect(() => {
    const obj = createSharedObject(uri);
    setSharedObject(obj);
    
    return () => {
      // ✅ Always release in cleanup
      obj.release();
    };
  }, [uri]);
  
  return <div>Using shared object</div>;
}

// ❌ Bad: Forgetting to release
function BadComponent({ uri }: { uri: string }) {
  const sharedObject = createSharedObject(uri);
  // Memory leak - object never released
  return <div>Using shared object</div>;
}

// ✅ Good: Error handling with release
async function processWithSharedObject(data: ArrayBuffer) {
  const processor = createSharedProcessor();
  
  try {
    const result = await processor.process(data);
    return result;
  } catch (error) {
    console.error("Processing failed:", error);
    throw error;
  } finally {
    // ✅ Always release in finally block
    processor.release();
  }
}

Types

/**
 * React dependency list type for useReleasingSharedObject
 */
type DependencyList = ReadonlyArray<any>;

/**
 * Base type for events map used by SharedObject
 */
type EventsMap = Record<string, (...args: any[]) => void>;