Probablemente estés en el punto donde la UI está lista, la pantalla de perfil tiene un botón de «Subir foto» y ahora la parte fácil de repente no lo es. El flujo real de selección de imágenes involucra permisos nativos, interfaces controladas por el SO, formatos de retorno diferentes a los que muchos desarrolladores esperan y varios detalles de compilación que solo aparecen después de lanzar un build real.
Es ahí donde entra Expo Image Picker. Es la biblioteca oficial de Expo para abrir la UI del sistema y elegir imágenes y videos de la biblioteca del dispositivo o tomar una foto con la cámara, como se describe en Repositorio de paquetes de Expo. En la práctica, eso significa que obtienes un puente fiable hacia la entrada de medios nativos, pero no una experiencia de medios personalizada que se comporte de forma idéntica en cada dispositivo.
Esta guía está escrita para la primera implementación, no para la demo. Se centra en las decisiones que importan en producción: configuración del flujo de trabajo gestionado frente a bare, manejo de permisos que no te sorprenderá después, análisis seguro de resultados y un patrón práctico de upload después de que el usuario elija un archivo. Si trabajas en una configuración nativa personalizada, también ayuda entender cómo esto difiere de Opciones de actualización de Expo EAS Capgo compara en su guía alternativa.
Tabla de contenidos
- Primeros pasos con Expo Image Picker
- Instalación y configuración esencial
- Acceso a la cámara y a la galería
- Gestión de resultados y opciones del Picker
- Patrones avanzados y diferencias de plataforma
- Solución de problemas comunes
Primeros pasos con Expo Image Picker
Un product manager pide fotos de perfil. Una semana después, la misma función también necesita subir recibos, capturar con la cámara para informes de incidentes y reintentar cuando el usuario deniega el permiso la primera vez. La entrada de imágenes crece rápido porque involucra permisos nativos, UI propiedad del SO, manejo de archivos temporales y flujos de upload al backend.
expo-image-picker es el módulo de Expo SDK para ese trabajo. Abre el selector de la plataforma o la UI de la cámara y devuelve los medios seleccionados en un formato que tu code de React Native puede manejar. El API de JavaScript es pequeño. El principal desafío está en configurar correctamente el setup nativo, el flujo de permisos y el manejo de resultados tanto en proyectos managed como bare.
El principal trade-off es directo. Dejas que iOS y Android muestren su propia UI de medios en lugar de construir un selector personalizado. Eso suele dar un mejor resultado: los usuarios ya entienden las pantallas del sistema, los prompts de permisos se comportan como el SO espera, y tu equipo evita mantener una implementación de galería en JavaScript.
Trátalo como una función de integración nativa con una interfaz de React.
Esa mentalidad ayuda porque los modos de fallo rara vez están en el botón que llama al picker. Suelen provenir de uno de tres lugares:
- Configuración nativa: setup de plugin faltante, cadenas de permisos incorrectas o un build obsoleto después de cambiar la config
- Comportamiento en runtime: los usuarios pueden denegar el acceso, otorgar acceso limitado a la biblioteca en iOS, o cancelar el flujo sin seleccionar nada
- Parseo de resultados: la versión actual de API devuelve un
assetsarray, por lo que los ejemplos antiguos que leenresult.uridirectamente fallan
La elección del workflow también cambia la ruta de configuración. En una app Expo gestionada, la mayor parte del trabajo nativo está en la configuración de la app y requiere un rebuild cuando esta cambia. En una app bare, sigues teniendo el módulo Expo API, pero debes verificar la configuración subyacente del proyecto de iOS y Android de forma más directa. Si tu equipo usa un cliente personalizado en lugar de Expo Go, esta guía se complementa bien con la de Capgo alternativa a Expo EAS para equipos que necesitan probar módulos nativos más allá de Expo Go.
Esa distinción es clave para el resto de la guía, ya que el camino ideal es solo la mitad de la historia. Una implementación de picker es sólida cuando funciona en ambos flujos de trabajo, gestiona las particularidades de permisos de cada plataforma sin sorprender al usuario y entrega un archivo útil a tu capa de upload en lugar de quedarse en una vista previa local.
Instalación y configuración esencial
La instalación se hace con un solo comando. Configurar bien la parte nativa es lo que determina si el picker funciona en un dispositivo real, en un dev client personalizado y en tu build de producción.
expo-image-picker ofrece un API orientado a React sobre los pickers de la plataforma para fotos, videos y captura de cámara. La llamada de JavaScript es sencilla. La configuración no lo es, porque el acceso a fotos y cámara lo controlan iOS y Android, no React Native.

Empieza con el instalador de Expo compatible con versiones:
npx expo install expo-image-picker
Usa expo install en lugar de npm install o yarn add. Expo vincula la versión del paquete con tu SDK, lo que evita un tipo muy común de problemas de compatibilidad nativa. Si estás comparando cómo encajan los módulos de Expo en tu proceso de release, esto Resumen de las herramientas de Expo es una referencia útil.
Configuración del managed workflow
En el managed workflow, declara el plugin en la app config para que Expo pueda aplicar los cambios nativos al hacer el build.
Ejemplo con app.json:
{
"expo": {
"plugins": ["expo-image-picker"]
}
}
Esa es la configuración mínima. En la práctica, los equipos suelen añadir también el texto de permisos, especialmente en iOS, donde el prompt del sistema debe explicar por qué la app necesita acceso. Usa un lenguaje específico para la acción del usuario. “Subir una foto de perfil” es mejor que “Necesita acceso a archivos multimedia”.
Un detalle operativo hace perder mucho tiempo. Cambiar plugins, los strings de permisos u otra config nativa requiere un rebuild. Recargar JavaScript no aplica esos cambios. En Expo Go, también estás limitado por lo que el cliente ya incluye. En un development build o production build, el proyecto nativo refleja tu config solo tras un nuevo build.
Detalles de configuración en Bare React Native
En una bare app, el paquete API es el mismo, pero debes verificar más cosas del proyecto nativo tú mismo. Lo primero es revisar las descripciones de uso de iOS. Si tu flujo puede abrir la galería, la cámara o grabar video con audio, tu app necesita los strings de permisos correspondientes en Info.plist antes de hacer el rebuild.
Una checklist práctica para proyectos bare es la siguiente:
- Instalar
expo-image-pickerconnpx expo install expo-image-picker. - Añade la configuración del plugin si tu proyecto usa Expo config plugins.
- Confirma que las descripciones de uso en iOS coincidan con las funciones que expones.
- Vuelve a compilar las apps de iOS y Android tras cualquier cambio en la config nativa.
La falta de texto de permisos suele parecer un bug de runtime porque la UI code está bien y el handler del botón se ejecuta. El fallo está más abajo en el stack. Normalmente reviso Info.plistla app config y si el build actual incluye los últimos cambios nativos antes de tocar el componente code.
A few habits make setup more predictable:
- Escribe el texto de permisos para la acción real: los usuarios deben entender por qué ven el prompt.
- Configura la cámara y la galería por separado: una puede funcionar mientras la otra sigue fallando.
- Recompila tras cambios nativos: El hot reload y el fast refresh no actualizan los permisos nativos.
- Prueba en un dispositivo: El comportamiento del simulador puede ocultar problemas de permisos y de cámara.
Si el picker funciona durante el desarrollo pero falla en TestFlight o en el build de la Play Store, trátalo primero como un problema de configuración. Casi siempre lo es.
Acceso a la cámara y a la galería
Un usuario toca “Subir foto”, espera que se abra la cámara o la galería, y tu app tiene una sola tarea en ese momento: abrir la UI del sistema adecuada, manejar el rechazo o la cancelación sin romper la pantalla y devolver una referencia de archivo local utilizable para la vista previa o para subirla.
Parece sencillo hasta que pruebas tanto los builds managed como los bare en iOS y Android. El API de JavaScript se mantiene compacto, pero el comportamiento en runtime sigue dependiendo de los prompts del OS, el hardware del dispositivo y de cómo configuraste antes los permisos nativos.

Un componente mínimo y seguro
El flujo principal es consistente tanto en proyectos de Expo con workflow managed como bare. Solicita el permiso correspondiente, lanza el picker, verifica si el usuario canceló y luego lee el primer asset de result.assets.
Un componente base se ve así:
import { useState } from 'react';
import { View, Button, Image, Alert } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
export default function PhotoInput() {
const [imageUri, setImageUri] = useState<string | null>(null);
const pickFromLibrary = async () => {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) {
Alert.alert('Permission required', 'Please allow photo library access.');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
if (result.canceled) return;
const asset = result.assets?.[0];
if (!asset?.uri) return;
setImageUri(asset.uri);
};
const takePhoto = async () => {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (!permission.granted) {
Alert.alert('Permission required', 'Please allow camera access.');
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
quality: 1,
});
if (result.canceled) return;
const asset = result.assets?.[0];
if (!asset?.uri) return;
setImageUri(asset.uri);
};
return (
<View>
<Button title="Choose from library" onPress={pickFromLibrary} />
<Button title="Take photo" onPress={takePhoto} />
{imageUri ? (
<Image
source={{ uri: imageUri }}
style={{ width: 200, height: 200 }}
/>
) : null}
</View>
);
}
Aquí importan tres detalles.
- Solicita los permisos de la librería y de la cámara por separado. Fallan de forma independiente.
- Trata la cancelación como una acción normal del usuario, no como un estado de error.
- Leer desde
assets[0], porque el selector devuelve un array de assets en lugar de un nivel superioruri.
Flujos de galería y cámara
Empieza con el flujo de la biblioteca si quieres la vía más rápida a una funcionalidad que funcione. Es más fácil de probar, funciona en más configuraciones de simulador y evita casos límite del hardware de la cámara. Añade soporte para la cámara una vez que el manejo de resultados sea estable.
La ruta de la cámara tiene más formas de fallar en desarrollo. La compatibilidad con iOS Simulator es limitada. Puede que los emuladores de Android no ofrezcan el mismo comportamiento de cámara que un dispositivo real. En proyectos bare, esas diferencias pueden hacerte revisar el componente code aunque el problema real sea la configuración nativa o el entorno de pruebas.
Un patrón de UI limpio es pedirle al usuario la fuente antes de llamar al selector API:
const showPickerOptions = () => {
Alert.alert('Upload image', 'Choose a source', [
{ text: 'Camera', onPress: takePhoto },
{ text: 'Photo Library', onPress: pickFromLibrary },
{ text: 'Cancel', style: 'cancel' },
]);
};
Esa separación mantiene cada función enfocada. También facilita añadir analytics, feature flags o reglas específicas del backend más adelante. Por ejemplo, algunos equipos permiten subidas desde la galería para imágenes de perfil, pero exigen capturas nuevas de la cámara para la verificación de identidad.
Si tu app en general también soporta patrones de acceso a archivos fuera de Expo o estás comparando convenciones entre stacks nativos, esta referencia de la biblioteca de fotos Capacitor es un contexto útil.
Una demo breve ayuda cuando muestras este flujo a tus compañeros o a QA:
Qué esperar de la UI del sistema
expo-image-picker abre el selector de la plataforma o la interfaz de la cámara. Tu app no controla cada pantalla de ese flujo. Esa distinción es clave porque “funciona en mi dispositivo” suele significar “el SO permitió la ruta que probé”.
En iOS, los usuarios pueden conceder acceso limitado a la biblioteca en lugar de acceso completo. En Android, el comportamiento del picker puede variar según la versión del SO y la capa de personalización del fabricante. En proyectos con managed workflow, Expo gestiona gran parte de la configuración nativa por ti. En proyectos con bare workflow, debes confirmar que tu app compilada incluya los cambios de permisos nativos que hiciste. El punto de llamada de JavaScript puede ser idéntico en ambos casos, aunque el resultado en runtime sea distinto.
Suelo probar estos casos antes de dar la funcionalidad por terminada:
- primera solicitud de permiso
- permiso denegado
- cancelación del usuario
- selección exitosa de la biblioteca
- captura de cámara exitosa en un dispositivo físico
- vista previa inmediata del URI local devuelto
Esos casos reflejan directamente el comportamiento real en producción. Además, preparan el siguiente paso de forma limpia si necesitas enviar el archivo a un servidor, un pipeline de moderación o un endpoint de publicación como el Publicación de contenido en Instagram API.
Gestión de resultados y opciones del Picker
El resultado del picker es la parte que suele requerir lógica de producción real. La UI del sistema devuelve un objeto estructurado, no solo una ruta de archivo, y pequeños errores aquí provocan previews rotas, uploads vacíos o crashes después de que el usuario cancele.
Cómo leer correctamente el objeto de resultado
La estructura del resultado que importa en las apps actuales de Expo es result.assets[0].uri, no un nivel superior result.uri. Ese detalle afecta tanto a proyectos managed como bare, ya que el API de JavaScript es el mismo aunque la configuración nativa varíe.
Usa un patrón guard-first:
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
if (result.canceled) {
return;
}
const asset = result.assets?.[0];
if (!asset) {
return;
}
const { uri } = asset;
setImageUri(uri);
Esto cubre los dos casos de fallo que veo más a menudo. Un picker cancelado no te da un asset que leer, y code que asuma que result.assets[0] siempre existe fallará en runtime.
Una vez que tengas la URI, renderizar un preview es sencillo:
<Image source={{ uri: imageUri }} style={{ width: 240, height: 240 }} />
Si planeas hacer el upload más tarde, guarda todo el asset objeto completo, no solo la URI. En la práctica, fileName, mimeType, width, height, y fileSize suelen ser útiles para validación, logging o para construir una solicitud multipart más limpia.
Opciones que afectan el comportamiento posterior
Algunas opciones del picker afectan más que la pantalla de selección. Determinan el tamaño de archivo, el comportamiento de edición y lo que tu backend debe aceptar.
| Opción | Tipo | What it changes | Uso típico |
|---|---|---|---|
mediaTypes |
array | Restringe lo que el usuario puede elegir | Limita la selección a imágenes si tu API solo acepta imágenes |
allowsEditing |
booleano | Permite que el SO ofrezca recorte o edición donde sea compatible | Avatares, portadas cuadradas, captura de recibos |
quality |
número | Comprime las salidas de imagen compatibles | Reduce el tamaño de la subida en redes móviles |
base64 |
booleano | Añade los datos codificados de la imagen al resultado | Solo para integraciones que requieran explícitamente datos de imagen inline |
Hay algunos trade-offs fáciles de pasar por alto:
allowsEditinges útil cuando el espacio de la imagen tiene una forma o tamaño fijo. Es menos útil si tu servidor tiene su propio pipeline de recorte y quieres el archivo original.qualityafecta al tiempo de subida, la presión de memoria y el almacenamiento del servidor.quality: 1no es automáticamente la opción correcta.mediaTypesdebe coincidir con las reglas del backend. Si el servidor rechaza videos, no dejes que el picker los devuelva.base64aumenta el tamaño del payload en memoria. Evítalo a menos que el servicio receptor lo requiera.
Ese último punto es importante en dispositivos con poca memoria. Un URI de archivo local suele ser la mejor opción para la vista previa y la subida multipart. Base64 tiene usos válidos, pero es costoso en comparación con pasar una referencia de archivo.
URI vs. base64
Para la mayoría de las apps, la regla es simple:
- Usa URI para las vistas previas.
- Usa URI para las subidas de archivos.
- Usa base64 solo cuando el sistema receptor solicita explícitamente contenido codificado.
Ese patrón mantiene el code del picker pequeño y más fácil de testear. También se alinea con cómo se construyen muchos flujos de medios de backend, incluidos servicios que acaban publicando en plataformas externas como Publicación de contenido en Instagram API.
Si tu equipo lanza actualizaciones OTA frecuentes o mueve assets con muchas imágenes por la entrega de la app, las decisiones de tamaño de archivo que tomes aquí repercuten en el resto del pipeline. Esta guía sobre optimizar imágenes para actualizaciones de la app es un complemento útil para la configuración del picker.
Un patrón de resultado más seguro para apps reales
Para el code de la demo, guardar solo imageUri está bien. En producción, guarda un objeto normalizado para que el siguiente paso (preview, validación, upload o reintento) no tenga que reinterpretar la respuesta raw del picker cada vez.
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 0.8,
});
if (result.canceled || !result.assets?.length) {
return;
}
const asset = result.assets[0];
setSelectedImage({
uri: asset.uri,
fileName: asset.fileName ?? 'upload.jpg',
mimeType: asset.mimeType ?? 'image/jpeg',
width: asset.width,
height: asset.height,
fileSize: asset.fileSize ?? null,
});
Esto te da una estructura predecible dentro de la app. También facilita mantener alineados los proyectos managed y bare, ya que el code de la app se mantiene estable mientras resuelves diferencias nativas en otros puntos.
Una última comprobación ayuda. No actives campos de resultado extra por si acaso. Solicita solo los datos que sabes que necesitas y mantén el picker centrado en la selección, en lugar de convertirlo en una etapa general de procesamiento de archivos.
Patrones avanzados y diferencias de plataforma
Una función de picker suele dejar de ser simple en el momento en que la primera imagen seleccionada debe sobrevivir a reintentos, cabeceras de autenticación, diferencias en permisos nativos y un endpoint de upload real. expo-image-picker gestiona bien la selección. El resto de la función depende de tu app.

Un patrón práctico de upload
Para APIs que esperan un file upload, FormData sigue siendo la opción predeterminada más segura. Funciona con backends comunes de Rails, Node, Laravel, Django y Go, y mantiene el selector separado de las preocupaciones de transporte.
async function uploadImage(imageUri: string) {
const formData = new FormData();
formData.append('file', {
uri: imageUri,
name: 'upload.jpg',
type: 'image/jpeg',
} as any);
const response = await fetch('https://your-api.example.com/uploads', {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error('Upload failed');
}
return response.json();
}
Ese code es suficiente para demostrar que la ruta funciona, pero las apps de producción suelen necesitar una capa más. Deriva name y type del activo seleccionado cuando sea posible, adjunta la autenticación fuera de la función del selector y mantén el estado de la subida separado del estado del selector para que una solicitud fallida no obligue al usuario a reabrir la biblioteca.
Algunas comprobaciones previenen los fallos comunes que veo en las revisiones:
- Confirma que el local
uriexiste antes de construir la solicitud - Render a preview before upload so users catch the wrong file early
- Evita toques repetidos mientras la solicitud está en curso
- Maneja los fallos de red por separado de la cancelación del selector o los errores de permisos
- Espera que la validación del backend rechace archivos grandes, tipos MIME no admitidos o falta de autenticación
Si tu backend requiere base64 en lugar de multipart, suele ser una restricción del servidor, no un requisito del selector. Multipart es más eficiente en memoria y más fácil de razonar en móvil.
Dónde importan realmente las diferencias de plataforma
La UI del selector es nativa, así que hereda el comportamiento nativo. Eso afecta tanto lo que ven los usuarios como lo que tu code debería asumir.
En iOS, los flujos de edición y los prompts de permisos siguen las convenciones de Apple. El acceso limitado a Fotos puede devolver un conjunto más reducido de assets del que vio tu cuenta de prueba en un dispositivo con permisos completos. En Android, el comportamiento del selector varía más según la versión del SO y la capa de personalización del fabricante, sobre todo con álbumes, nombres de archivo y cómo se devuelven las capturas de cámara. Las apps bare de React Native notan estas diferencias más directamente porque tú controlas más la configuración nativa, pero las apps gestionadas de Expo siguen necesitando code que trate al selector como algo moldeado por la plataforma, no como perfectamente uniforme.
La regla práctica es simple. Depende de los campos que puedas validar, no de que la UI o los metadatos sean idénticos entre dispositivos.
Algunos ejemplos que importan en apps reales:
- Edición y recorte: La interfaz y el comportamiento de recorte no son idénticos entre iOS y Android
- Metadatos devueltos:
fileName,mimeType, yfileSizepueden faltar o ser inconsistentes, así que añade fallbacks - Permisos: El acceso a fotos en iOS puede limitarse a elementos seleccionados, mientras que en Android el comportamiento depende más de la versión del SO y del soporte del selector del sistema
- Salida de la cámara: Las imágenes capturadas pueden tener nombres, orientación o características de compresión diferentes a los de los activos de la biblioteca
Si tu equipo también trabaja fuera de Expo, esta guía de desarrollo de apps de DesignStack proporciona un contexto útil de Android para las decisiones de gestión de medios que trascienden una sola biblioteca.
Diferencias entre managed y bare workflow
En este punto, las decisiones de configuración empiezan a importar a nivel operativo.
En el managed workflow, los strings de permisos y la config de los plugins suelen vivir en el app config, y los cambios nativos se aplican al crear un nuevo build. Eso mantiene limpia la superficie de JavaScript, pero también significa que un fix de config no será visible hasta el próximo native build. Las actualizaciones OTA no parchean permisos nativos faltantes.
En el bare workflow, la misma funcionalidad tiene más piezas en juego. Debes verificar por tu cuenta las descripciones de uso en iOS, el comportamiento del manifest de Android, la instalación de paquetes y los tiempos de rebuild. La ventaja es el control. El costo es que un problema del picker puede venir de la configuración nativa y no del call site de JavaScript.
Los equipos que cambian entre Expo y Capacitor suelen subestimar lo diferentes que son estas capas de abstracción. Capgo tiene una explicación útil sobre cómo Capacitor gestiona las diferencias entre plataformas, y es un buen punto de comparación si estás decidiendo cuánto setup nativo quiere asumir tu equipo.
Mi preferencia es la misma en ambos workflows. Mantén acotado el code del picker, normaliza el resultado una sola vez, haz el upload mediante una capa API dedicada, y trata el comportamiento específico de cada plataforma como algo que hay que configurar y testear explícitamente, en vez de taparlo con suposiciones.
Solución de problemas comunes
La mayoría de los bugs de Expo Image Picker caen en un pequeño conjunto de categorías. El fix más rápido suele ser identificar qué capa está fallando: config, permisos, manejo de resultados o renderizado.

Comprobaciones rápidas para fallos comunes
Si el selector no se abre o fallan los permisos, revisa primero la configuración nativa. En bare apps, la falta de descripciones de uso en iOS es una causa raíz común.
Si la app falla tras cerrar el picker, revisa cómo manejas el resultado. Muchas implementaciones aún asumen una URI directa y omiten el canceled verificación.
Algunos mapeos rápidos ayudan:
- Errores de permiso denegado: Verifica la configuración de tu app y los permisos nativos, luego vuelve a compilar.
undefinedURI de imagen: Leer desderesult.assets?.[0]?.uri, noresult.uri.- No pasa nada al cancelar: Puede ser correcto. Maneja el cancelamiento como un estado sin efecto (no-op).
- La imagen no se renderiza: Confirma que la URI se guardó en el estado y se pasó a
<Image source={{ uri }} />. - Camera acts strangely in simulator: Prueba en un dispositivo físico antes de perseguir un bug de la librería.
Una breve checklist de producción
Usa esto como una revisión final antes de publicar:
- Instala con las herramientas de Expo: Usa
npx expo install expo-image-picker. - Configura las piezas nativas: Añade el plugin y las descripciones de permisos requeridos.
- Solicita permisos de forma intencional: Separa los flujos de cámara y biblioteca multimedia.
- Protege cada resultado: Verificar
result.canceledy lee de forma seguraassets[0]. - Preferencia por uploads basados en URI: Reserva base64 solo para casos especiales.
- Prueba en dispositivos reales: Especialmente para captura de cámara y prompts de permisos.
Si tu equipo publica Capacitor o apps de Electron junto a proyectos de React Native, Capgo es una opción para entregar actualizaciones de JavaScript, CSS, configuración y assets sin esperar a la revisión de la tienda en cada cambio. Es relevante cuando las correcciones relacionadas con imágenes están en tu capa web, como la UI de subida, las reglas de validación, el texto o el manejo de assets alrededor del flujo del selector.