or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

albums.mdassets.mdevents.mdindex.mdpermissions.md
tile.json

events.mddocs/

Event Listeners

Real-time media library change notifications with incremental update support for efficient UI updates, allowing applications to respond to external changes to the media library.

Capabilities

Add Event Listener

Subscribes to media library change events to receive real-time notifications when assets are added, modified, or deleted.

/**
 * Subscribes to media library changes for real-time notifications
 * @param listener - Callback function to handle media library change events
 * @returns EventSubscription object with remove() method for cleanup
 */
function addListener(
  listener: (event: MediaLibraryAssetsChangeEvent) => void
): EventSubscription;

interface MediaLibraryAssetsChangeEvent {
  /** Whether the changes are incremental and detailed */
  hasIncrementalChanges: boolean;
  /** Assets that were inserted (if hasIncrementalChanges is true) */
  insertedAssets?: Asset[];
  /** Assets that were deleted (if hasIncrementalChanges is true) */
  deletedAssets?: Asset[];
  /** Assets that were updated (if hasIncrementalChanges is true) */
  updatedAssets?: Asset[];
}

interface EventSubscription {
  /** Remove this specific event subscription */
  remove(): void;
}

Usage Examples:

import * as MediaLibrary from "expo-media-library";

// Basic event listener
const subscription = MediaLibrary.addListener((event) => {
  if (event.hasIncrementalChanges) {
    console.log(`Inserted: ${event.insertedAssets?.length || 0}`);
    console.log(`Deleted: ${event.deletedAssets?.length || 0}`);
    console.log(`Updated: ${event.updatedAssets?.length || 0}`);
  } else {
    console.log('Media library changed, full refresh needed');
  }
});

// React component with event listener
import React, { useState, useEffect } from 'react';

function MediaGallery() {
  const [assets, setAssets] = useState([]);

  useEffect(() => {
    // Initial load
    const loadAssets = async () => {
      const result = await MediaLibrary.getAssetsAsync({ first: 20 });
      setAssets(result.assets);
    };
    loadAssets();

    // Listen for changes
    const subscription = MediaLibrary.addListener((event) => {
      if (event.hasIncrementalChanges) {
        setAssets(currentAssets => {
          let updatedAssets = [...currentAssets];
          
          // Remove deleted assets
          if (event.deletedAssets) {
            const deletedIds = event.deletedAssets.map(asset => asset.id);
            updatedAssets = updatedAssets.filter(asset => 
              !deletedIds.includes(asset.id)
            );
          }
          
          // Add new assets
          if (event.insertedAssets) {
            updatedAssets = [...event.insertedAssets, ...updatedAssets];
          }
          
          // Update modified assets
          if (event.updatedAssets) {
            event.updatedAssets.forEach(updatedAsset => {
              const index = updatedAssets.findIndex(asset => 
                asset.id === updatedAsset.id
              );
              if (index !== -1) {
                updatedAssets[index] = updatedAsset;
              }
            });
          }
          
          return updatedAssets;
        });
      } else {
        // Full refresh needed
        loadAssets();
      }
    });

    return () => subscription.remove();
  }, []);

  return (
    <div>
      {assets.map(asset => (
        <img key={asset.id} src={asset.uri} alt={asset.filename} />
      ))}
    </div>
  );
}

// Advanced listener with error handling
const advancedSubscription = MediaLibrary.addListener((event) => {
  try {
    if (event.hasIncrementalChanges) {
      // Handle incremental changes efficiently
      handleIncrementalChanges(event);
    } else {
      // Fallback to full refresh
      handleFullRefresh();
    }
  } catch (error) {
    console.error('Error handling media library change:', error);
  }
});

function handleIncrementalChanges(event) {
  // Process insertions
  if (event.insertedAssets && event.insertedAssets.length > 0) {
    console.log('New assets added:', event.insertedAssets.map(a => a.filename));
    // Update UI to show new assets
  }
  
  // Process deletions
  if (event.deletedAssets && event.deletedAssets.length > 0) {
    console.log('Assets deleted:', event.deletedAssets.map(a => a.filename));
    // Remove assets from UI
  }
  
  // Process updates
  if (event.updatedAssets && event.updatedAssets.length > 0) {
    console.log('Assets updated:', event.updatedAssets.map(a => a.filename));
    // Update asset metadata in UI
  }
}

function handleFullRefresh() {
  console.log('Full media library refresh needed');
  // Reload all assets from scratch
}

Remove Specific Subscription

Removes a specific event subscription while leaving other listeners active.

/**
 * Removes specific event subscription
 * @param subscription - EventSubscription object to remove
 * @returns void
 */
function removeSubscription(subscription: EventSubscription): void;

Usage Examples:

import * as MediaLibrary from "expo-media-library";

// Create and later remove specific subscription
const subscription = MediaLibrary.addListener((event) => {
  console.log('Media library changed');
});

// Remove using the subscription object
subscription.remove(); // Preferred method

// Or remove using the removeSubscription function
MediaLibrary.removeSubscription(subscription);

// Multiple subscriptions management
const subscriptions = [];

// Add multiple listeners
subscriptions.push(MediaLibrary.addListener(handleAssetChanges));
subscriptions.push(MediaLibrary.addListener(handleAlbumChanges));
subscriptions.push(MediaLibrary.addListener(logChanges));

// Remove specific subscription
MediaLibrary.removeSubscription(subscriptions[1]);

// Clean up all subscriptions
subscriptions.forEach(sub => sub.remove());

Remove All Listeners

Removes all active media library event listeners at once.

/**
 * Removes all media library event listeners
 * @returns void
 */
function removeAllListeners(): void;

Usage Examples:

import * as MediaLibrary from "expo-media-library";

// Add several listeners
const listener1 = MediaLibrary.addListener(handleChange1);
const listener2 = MediaLibrary.addListener(handleChange2);
const listener3 = MediaLibrary.addListener(handleChange3);

// Remove all at once
MediaLibrary.removeAllListeners();

// Useful in app cleanup or navigation
class MediaScreen extends Component {
  componentDidMount() {
    this.subscription1 = MediaLibrary.addListener(this.handleChanges);
    this.subscription2 = MediaLibrary.addListener(this.handleUploads);
  }
  
  componentWillUnmount() {
    // Clean up all listeners when leaving screen
    MediaLibrary.removeAllListeners();
  }
}

// React hook for automatic cleanup
function useMediaLibraryListener(callback) {
  useEffect(() => {
    const subscription = MediaLibrary.addListener(callback);
    
    return () => {
      // Clean up this specific listener
      subscription.remove();
      // Or clean up all listeners if this is the main component
      // MediaLibrary.removeAllListeners();
    };
  }, [callback]);
}

Event Processing Patterns

Debounced Event Handling

Handle rapid changes efficiently by debouncing events.

import * as MediaLibrary from "expo-media-library";

let debounceTimeout;

const debouncedListener = MediaLibrary.addListener((event) => {
  clearTimeout(debounceTimeout);
  
  debounceTimeout = setTimeout(() => {
    processMediaLibraryChange(event);
  }, 300); // Wait 300ms before processing
});

function processMediaLibraryChange(event) {
  if (event.hasIncrementalChanges) {
    // Process incremental changes
    updateUIIncrementally(event);  
  } else {
    // Full refresh
    refreshAllAssets();
  }
}

Batched UI Updates

Batch multiple changes for efficient UI updates.

import * as MediaLibrary from "expo-media-library";

let pendingChanges = {
  inserted: [],
  deleted: [],
  updated: []
};

let batchTimeout;

const batchedListener = MediaLibrary.addListener((event) => {
  if (event.hasIncrementalChanges) {
    // Accumulate changes
    if (event.insertedAssets) {
      pendingChanges.inserted.push(...event.insertedAssets);
    }
    if (event.deletedAssets) {
      pendingChanges.deleted.push(...event.deletedAssets);
    }
    if (event.updatedAssets) {
      pendingChanges.updated.push(...event.updatedAssets);
    }
    
    // Batch process after brief delay
    clearTimeout(batchTimeout);
    batchTimeout = setTimeout(processBatchedChanges, 100);
  } else {
    // Immediate full refresh for non-incremental changes
    refreshAllAssets();
  }
});

function processBatchedChanges() {
  const changes = { ...pendingChanges };
  
  // Reset pending changes
  pendingChanges = { inserted: [], deleted: [], updated: [] };
  
  // Apply all batched changes at once
  applyUIChanges(changes);
}

Types

interface MediaLibraryAssetsChangeEvent {
  /** 
   * Whether changes provide detailed incremental information.
   * If false, a full refresh of the media library is recommended.
   */
  hasIncrementalChanges: boolean;
  
  /** 
   * Assets that were added to the media library.
   * Only available when hasIncrementalChanges is true.
   */
  insertedAssets?: Asset[];
  
  /** 
   * Assets that were removed from the media library.
   * Only available when hasIncrementalChanges is true.
   */
  deletedAssets?: Asset[];
  
  /** 
   * Assets that were modified in the media library.
   * Only available when hasIncrementalChanges is true.
   */
  updatedAssets?: Asset[];
}

interface EventSubscription {
  /** Remove this specific event listener subscription */
  remove(): void;
}

interface Asset {
  id: string;
  filename: string;
  uri: string;
  mediaType: MediaTypeValue;
  mediaSubtypes?: MediaSubtype[];
  width: number;
  height: number;
  creationTime: number;
  modificationTime: number;
  duration: number;
  albumId?: string;
}

Platform-Specific Behavior

iOS

  • Incremental Changes: Full support for detailed incremental change events
  • Real-time Updates: Immediate notifications for photo library changes
  • Live Photos: Updates include changes to paired video assets
  • Smart Albums: Changes to smart albums (like Favorites) are tracked

Android

  • Scoped Storage: Events respect scoped storage permissions and access
  • Media Scanner: Changes detected through Android's MediaStore
  • Granular Permissions: Events limited to media types with granted permissions
  • Background Updates: May have delays when app is backgrounded

Web

  • Limited Support: Basic file system change detection where available
  • Browser Limitations: May not detect all external media library changes
  • Polling Alternative: Consider periodic refresh for reliable updates

Performance Considerations

  • Incremental Processing: Always check hasIncrementalChanges before processing
  • Debouncing: Use debouncing for rapid change sequences
  • Batching: Batch UI updates for better performance
  • Memory Management: Always clean up listeners to prevent memory leaks
  • Background Handling: Consider pausing listeners when app is backgrounded