Saltar al contenido principal

Comunicación entre nativo y React Native

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

En las guías Integración con aplicaciones existentes y Componentes de UI nativos aprendemos cómo integrar React Native en componentes nativos y viceversa. Cuando combinamos componentes nativos y de React Native, eventualmente necesitaremos comunicación entre estos dos mundos. Algunas formas de lograrlo ya se mencionaron en otras guías. Este artículo resume las técnicas disponibles.

Introducción

React Native está inspirado en React, por lo que la idea básica del flujo de información es similar. El flujo en React es unidireccional. Mantenemos una jerarquía de componentes donde cada componente depende únicamente de su padre y su propio estado interno. Lo logramos mediante propiedades: los datos pasan de un padre a sus hijos de arriba hacia abajo. Si un componente ancestro depende del estado de su descendiente, debemos pasar un callback para que el descendiente actualice al ancestro.

El mismo concepto aplica a React Native. Mientras construyamos nuestra aplicación exclusivamente dentro del framework, podemos manejar nuestra app con propiedades y callbacks. Pero al mezclar componentes de React Native y nativos, necesitamos mecanismos específicos multiplataforma que permitan pasar información entre ellos.

Propiedades

Las propiedades son la forma más directa de comunicación entre componentes. Por lo tanto, necesitamos una forma de pasar propiedades tanto de nativo a React Native como de React Native a nativo.

Paso de propiedades de nativo a React Native

Para integrar una vista de React Native en un componente nativo, usamos RCTRootView. RCTRootView es una UIView que contiene una aplicación React Native. También proporciona una interfaz entre el lado nativo y la aplicación hospedada.

RCTRootView tiene un inicializador que permite pasar propiedades arbitrarias a la aplicación React Native. El parámetro initialProperties debe ser una instancia de NSDictionary. Internamente, este diccionario se convierte en un objeto JSON al que puede hacer referencia el componente JS de nivel superior.

objectivec
NSArray *imageList = @[@"https://dummyimage.com/600x400/ffffff/000000.png",
@"https://dummyimage.com/600x400/000000/ffffff.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
tsx
import React from 'react';
import {View, Image} from 'react-native';

export default class ImageBrowserApp extends React.Component {
renderImage(imgURI) {
return <Image source={{uri: imgURI}} />;
}
render() {
return <View>{this.props.images.map(this.renderImage)}</View>;
}
}

RCTRootView también proporciona una propiedad de lectura/escritura llamada appProperties. Después de establecer appProperties, la aplicación React Native se vuelve a renderizar con las nuevas propiedades. La actualización solo se realiza cuando las nuevas propiedades difieren de las anteriores.

objectivec
NSArray *imageList = @[@"https://dummyimage.com/600x400/ff0000/000000.png",
@"https://dummyimage.com/600x400/ffffff/ff0000.png"];

rootView.appProperties = @{@"images" : imageList};

Es posible actualizar las propiedades en cualquier momento. Sin embargo, las actualizaciones deben realizarse en el hilo principal. El acceso mediante el getter puede hacerse en cualquier hilo.

nota

Actualmente existe un problema conocido: al establecer appProperties durante el inicio del bridge, los cambios pueden perderse. Consulta https://github.com/facebook/react-native/issues/20115 para más información.

No existe forma de actualizar solo algunas propiedades. Sugerimos implementar esta funcionalidad en tu propio wrapper.

Paso de propiedades de React Native a nativo

El problema de exponer propiedades de componentes nativos se cubre en detalle en este artículo. En resumen: exporta propiedades con la macro RCT_CUSTOM_VIEW_PROPERTY en tu componente nativo personalizado, luego úsalas en React Native como si fuera un componente ordinario.

Limitaciones de las propiedades

La principal desventaja de las propiedades multiplataforma es que no admiten callbacks, que nos permitirían manejar enlaces de datos ascendentes. Imagina que tienes una pequeña vista RN que deseas eliminar de la vista nativa principal como resultado de una acción JS. No hay forma de hacerlo con props, ya que la información necesitaría viajar de abajo hacia arriba.

Aunque tenemos una variante de callbacks multiplataforma (descritos aquí), no siempre son lo que necesitamos. El principal problema es que no están diseñados para pasarse como propiedades. Más bien, este mecanismo nos permite activar una acción nativa desde JS y manejar su resultado en JS.

Otras formas de interacción multiplataforma (eventos y módulos nativos)

Como se mencionó en el capítulo anterior, el uso de propiedades tiene algunas limitaciones. A veces las propiedades no son suficientes para manejar la lógica de nuestra aplicación y necesitamos una solución que ofrezca más flexibilidad. Este capítulo cubre otras técnicas de comunicación disponibles en React Native. Pueden usarse tanto para comunicación interna (entre las capas JS y nativas en RN) como para comunicación externa (entre RN y la parte 'nativa pura' de tu aplicación).

React Native te permite realizar llamadas de funciones entre lenguajes. Puedes ejecutar código nativo personalizado desde JS y viceversa. Desafortunadamente, dependiendo del lado en el que estemos trabajando, logramos el mismo objetivo de maneras diferentes. En el lado nativo, utilizamos el mecanismo de eventos para programar la ejecución de una función de manejo en JS, mientras que para React Native llamamos directamente a los métodos exportados por módulos nativos.

Llamar a funciones de React Native desde el nativo (eventos)

Los eventos se describen en detalle en este artículo. Ten en cuenta que usar eventos no nos da garantías sobre el tiempo de ejecución, ya que el evento se maneja en un hilo separado.

Los eventos son poderosos porque nos permiten cambiar componentes de React Native sin necesidad de una referencia a ellos. Sin embargo, existen algunas trampas en las que puedes caer al usarlos:

  • Dado que los eventos pueden enviarse desde cualquier lugar, pueden introducir dependencias estilo espagueti en tu proyecto.

  • Los eventos comparten un espacio de nombres, lo que significa que puedes encontrarte con colisiones de nombres. Las colisiones no se detectarán estáticamente, lo que las hace difíciles de depurar.

  • Si usas varias instancias del mismo componente de React Native y quieres distinguirlas desde la perspectiva de tu evento, es probable que necesites introducir identificadores y pasarlos junto con los eventos (puedes usar el reactTag de la vista nativa como identificador).

El patrón común que usamos al integrar componentes nativos en React Native es hacer que el RCTViewManager del componente nativo sea un delegado para las vistas, enviando eventos de vuelta a JavaScript a través del bridge. Esto mantiene las llamadas a eventos relacionadas en un solo lugar.

Llamar a funciones nativas desde React Native (módulos nativos)

Los módulos nativos son clases Objective-C disponibles en JS. Normalmente se crea una instancia de cada módulo por bridge de JS. Pueden exportar funciones y constantes arbitrarias a React Native. Se han cubierto en detalle en este artículo.

El hecho de que los módulos nativos sean singulares limita el mecanismo en el contexto de integración. Supongamos que tenemos un componente de React Native integrado en una vista nativa y queremos actualizar la vista nativa principal. Usando el mecanismo de módulos nativos, exportaríamos una función que no solo toma los argumentos esperados, sino también un identificador de la vista nativa principal. Este identificador se usaría para obtener una referencia a la vista principal y actualizarla. Dicho esto, necesitaríamos mantener un mapeo de identificadores a vistas nativas en el módulo.

Aunque esta solución es compleja, se usa en RCTUIManager, que es una clase interna de React Native que gestiona todas las vistas de React Native.

Los módulos nativos también pueden usarse para exponer bibliotecas nativas existentes a JS. La biblioteca de Geolocalización es un ejemplo vivo de esta idea.

precaución

Todos los módulos nativos comparten el mismo espacio de nombres. Ten cuidado con las colisiones de nombres al crear nuevos.

Flujo de cálculo de diseño

Al integrar componentes nativos y de React Native, también necesitamos una forma de consolidar dos sistemas de diseño diferentes. Esta sección cubre problemas comunes de diseño y proporciona una breve descripción de mecanismos para abordarlos.

Diseño de un componente nativo integrado en React Native

Este caso se cubre en este artículo. En resumen, dado que todas nuestras vistas nativas de React son subclases de UIView, la mayoría de los atributos de estilo y tamaño funcionarán como cabría esperar.

Diseño de un componente de React Native integrado en nativo

Contenido de React Native con tamaño fijo

El escenario general es cuando tenemos una aplicación React Native con un tamaño fijo, conocido por el lado nativo. En particular, una vista React Native a pantalla completa entra en este caso. Si queremos una vista raíz más pequeña, podemos establecer explícitamente el frame de RCTRootView.

Por ejemplo, para crear una app RN de 200 píxeles lógicos de alto y con el ancho de la vista contenedora, podríamos hacer:

SomeViewController.m
- (void)viewDidLoad
{
[...]
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:appName
initialProperties:props];
rootView.frame = CGRectMake(0, 0, self.view.width, 200);
[self.view addSubview:rootView];
}

Cuando tenemos una vista raíz de tamaño fijo, debemos respetar sus límites en el lado JS. Es decir, necesitamos asegurarnos de que el contenido React Native pueda contenerse dentro de la vista raíz de tamaño fijo. La forma más sencilla es usar el diseño Flexbox. Si usas posicionamiento absoluto y los componentes React son visibles fuera de los límites de la vista raíz, se solaparán con las vistas nativas, causando comportamientos inesperados. Por ejemplo, 'TouchableHighlight' no resaltará los toques fuera de los límites de la vista raíz.

Es perfectamente válido actualizar dinámicamente el tamaño de la vista raíz reestableciendo su propiedad frame. React Native se encargará del diseño del contenido.

Contenido React Native con tamaño flexible

En algunos casos queremos renderizar contenido de tamaño inicialmente desconocido. Supongamos que el tamaño se definirá dinámicamente en JS. Tenemos dos soluciones para este problema.

  1. Puedes envolver tu vista React Native en un componente ScrollView. Esto garantiza que tu contenido siempre estará disponible y no se solapará con vistas nativas.

  2. React Native te permite determinar en JS el tamaño de la app RN y proporcionárselo al propietario del RCTRootView contenedor. El propietario es responsable de reorganizar las subvistas y mantener la UI consistente. Logramos esto con los modos de flexibilidad de RCTRootView.

RCTRootView admite 4 modos de flexibilidad de tamaño diferentes:

RCTRootView.h
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};

RCTRootViewSizeFlexibilityNone es el valor predeterminado, que hace que el tamaño de la vista raíz sea fijo (aunque aún puede actualizarse con setFrame:). Los otros tres modos nos permiten rastrear actualizaciones de tamaño del contenido React Native. Por ejemplo, establecer el modo en RCTRootViewSizeFlexibilityHeight hará que React Native mida la altura del contenido y pase esa información al delegado de RCTRootView. Se puede realizar cualquier acción en el delegado, incluyendo establecer el frame de la vista raíz para que el contenido encaje. El delegado se llama solo cuando cambia el tamaño del contenido.

precaución

Hacer una dimensión flexible tanto en JS como en nativo conduce a comportamiento indefinido. Por ejemplo: no hagas flexible el ancho de un componente React de nivel superior (con flexbox) mientras usas RCTRootViewSizeFlexibilityWidth en el RCTRootView contenedor.

Veamos un ejemplo.

FlexibleSizeExampleView.m
- (instancetype)initWithFrame:(CGRect)frame
{
[...]

_rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"FlexibilityExampleApp"
initialProperties:@{}];

_rootView.delegate = self;
_rootView.sizeFlexibility = RCTRootViewSizeFlexibilityHeight;
_rootView.frame = CGRectMake(0, 0, self.frame.size.width, 0);
}

#pragma mark - RCTRootViewDelegate
- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView
{
CGRect newFrame = rootView.frame;
newFrame.size = rootView.intrinsicContentSize;

rootView.frame = newFrame;
}

En el ejemplo tenemos una vista FlexibleSizeExampleView que contiene una vista raíz. Creamos la vista raíz, la inicializamos y establecemos el delegado. El delegado manejará las actualizaciones de tamaño. Luego, establecemos la flexibilidad de tamaño de la vista raíz en RCTRootViewSizeFlexibilityHeight, lo que significa que se llamará al método rootViewDidChangeIntrinsicSize: cada vez que el contenido React Native cambie su altura. Finalmente, establecemos el ancho y posición de la vista raíz. Nota que también establecemos la altura, pero no tiene efecto ya que hicimos la altura dependiente de RN.

Puedes ver el código fuente completo del ejemplo aquí.

Es válido cambiar dinámicamente el modo de flexibilidad de tamaño de la vista raíz. Cambiar el modo de flexibilidad programará un recálculo del diseño y se llamará al método delegado rootViewDidChangeIntrinsicSize: una vez que se conozca el tamaño del contenido.

nota

El cálculo del diseño de React Native se realiza en un hilo separado, mientras que las actualizaciones de vistas UI nativas se hacen en el hilo principal. Esto puede causar inconsistencias temporales en la UI entre lo nativo y React Native. Este es un problema conocido y nuestro equipo está trabajando en sincronizar actualizaciones de UI de diferentes fuentes.

nota

React Native no realiza cálculos de diseño hasta que la vista raíz se convierte en subvista de otras vistas. Si deseas ocultar la vista de React Native hasta conocer sus dimensiones, agrega la vista raíz como subvista configurándola inicialmente como oculta (usa la propiedad hidden de UIView). Luego modifica su visibilidad en el método delegado.