Ionic y React: integración para una experiencia móvil

Ionic y React: integración para una experiencia móvil

¿Cuál es la razón de combinar Ionic y React?

Ahora vamos a aprender a utilizar React dentro de un proyecto Ionic, trabajar con componentes Ionic para este propósito y lograr así desplegar nuevos elementos dentro de nuestra aplicación.

¿Cómo se instala del Ionic?

El Cli de Ionic nos permite crear proyectos nuevos de ionic de forma sencilla, así como realizar varias tareas de forma automática.

Para instalar el Cli de Ionic es necesario primer tener instalado nodejs.

Con nodejs instalado el siguiente paso es instalar el Cli de Ionic.

$ npm i -g @ionic/cli

¿Cómo crear un proyecto en Ionic?

$ ionic start

El asistente nos pedirá una serie de información.

  • Framework: React
  • Project name: miapp
  • Starter template: blank

El sistema empezará a realizar la instalación de todos los paquetes necesarios incluyendo capacitor.

Accedemos al directorio del proyecto.

$ cd miapp

Ejecutamos la aplicación.

$ ionic serve

Se abrirá nuestra aplicación base dentro del navegador sobre http://localhost:8081.

¿Cuáles son los directorios que conforman un proyecto en Ionic?

  • node_modules : el directorio en donde se instalan los paquetes de npm.
  • public : contiene archivos de acceso público, entre ellos el index.html que es el punto de entrada a la aplicación.
  • src : contiene el código general del proyecto que iremos agregando.
  • capacitor.config.json : define la configuración a utilizar por capacitor.
  • ionic.config.json : define la configuración de ionic.
  • package.json : es la lista de dependencias y scripts que utilizará el proyecto.
  • tsconfig.json : configuración utilizada por TypeScript.

Cuando trabajas con React en Ionic generalmente utilizamos TypeScript. Es posible utilizar solo JavaScript, sin embargo Ionic por default implementa (sugiere) el uso de TypeScript. TypeScript es un superset de JavaScript que permite agregar mas características como el uso de tipado.

Limpiar el proyecto base

Antes de iniciar vamos a limpiar el proyecto para quedar con la información base de este.

Eliminamos el serviceworker de App.tsx.

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

Eliminamos también el archivo setupTests.ts, App.test.tsx y serviceWorker.ts.

Dentro de App.tsx vamos a dejar solo

import React from "react";
import { IonApp } from "@ionic/react";

/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";

/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";

/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";

/* Theme variables */
import "./theme/variables.css";

const App: React.FC = () => (
    <IonApp>
        <div>Hola Mundo!</div>
    </IonApp>
);

export default App;

Los directorios src/pages y components contienen ejemplos de componentes por lo que podemos eliminar completamente este directorio.

Ahora podemos ver que nuestra aplicación esta en blanco, solo contiene un “Hola Mundo!”.

¿En que consiste el tipado en TypeScript?

Como comentamos TypeScript es un superset que agrega características adicionales a JavaScript como el tipado. El poder agregar tipado nos permite tener un control mas fino de la asignación de valores y prevenir algunos errores durante el proceso de compilación.

En el siguiente ejemplo estamos definiendo App como una constante de tipo React.FC (React Functional Component).

const App: React.FC = () => (
    <IonApp>
        <div>Hola Mundo!</div>
    </IonApp>
);

Para poder agregar estos tipos, dentro del package.json existe una dependencia llamada @types/react.

Si por ejemplo cambio el código anterior por lo siguiente obtendré un error, ya que el tipo de constante esta definido como un React.FC y no como un string.

const App: React.FC => "Hola Mundo!";

En algunos editores como Visual Studio Code, el interprete nos mostrará el error de sintaxis sin requerir siquiera ejecutar el compilador.

Componentes web de Ionic vs Componentes en React

Si regresamos al primer ejemplo nos podemos dar cuenta que la sintaxis es diferente a la que utilizamos en el ejemplo con React. Mientras en el primer ejemplo usamos el CDN y las etiquetas lucen…

<ion-app></ion-app>

en react las etiquetas lucen así…

<IonApp></IonApp>

La razón es que en el segundo caso no estamos usando los componentes web de Ionic, sino los componentes de Ionic React, de forma que puedan ser utilizados dentro del ambiente de React. Ionic React se encarga de envolver los componentes web originales y proveer todas las características de componentes React.

Los componentes en React solo pueden utilizarse dentro de React y son creados mediante la librería React utilizando jsx ya sea para crear componentes funcionales o basados en clases.

Los componentes web Ionic son creados con vanilla JavaScript (la versión pura del lenguaje), utilizan tecnologías web nativas y pueden ser utilizados en cualquier proyecto en donde se utilice HTML y JavaScript.

Utilizar los componentes React Ionic

Ahora vamos a actualizar nuestro ejemplo con el ejemplo de la calculadora, para ello reemplazamos todas las referencias a los web components.

import React, { useRef, useState } from "react";
import {
    IonApp,
    IonHeader,
    IonContent,
    IonToolbar,
    IonTitle,
    IonGrid,
    IonRow,
    IonCol,
    IonItem,
    IonLabel,
    IonInput,
    IonButton,
    IonCard,
    IonCardContent,
} from "@ionic/react";

// referencias a los archivos css

const App: React.FC = () => {
    const [calculoDeIMC, setCalculoDeIMC] = useState<number>();

    const pesoRef = useRef<HTMLIonInputElement>(null);
    const alturaRef = useRef<HTMLIonInputElement>(null);

    const calcularIMC = () => {
        const peso = pesoRef.current!.value;
        const altura = alturaRef.current!.value;
        if (!peso || !altura) {
            return;
        }
        const imc = +peso / (+altura * +altura);
        setCalculoDeIMC(imc);
    };

    return (
        <IonApp>
            <IonApp>
                <IonHeader>
                    <IonToolbar>
                        <IonTitle>Calculadora</IonTitle>
                    </IonToolbar>
                </IonHeader>
                <IonContent>
                    <IonGrid>
                        <IonRow>
                            <IonCol>
                                <IonItem>
                                    <IonLabel>Tu peso:</IonLabel>
                                    <IonInput ref={pesoRef}></IonInput>
                                </IonItem>
                            </IonCol>
                        </IonRow>
                        <IonRow>
                            <IonCol>
                                <IonItem>
                                    <IonLabel>Tu altura:</IonLabel>
                                    <IonInput ref={alturaRef}></IonInput>
                                </IonItem>
                            </IonCol>
                        </IonRow>
                        <IonRow>
                            <IonCol>
                                <IonButton onClick={calcularIMC}>
                                    Calcular
                                </IonButton>
                            </IonCol>
                        </IonRow>
                        {calculoDeIMC && (
                            <IonRow>
                                <IonCol>
                                    <IonCard>
                                        <IonCardContent>
                                            <h2>{calculoDeIMC}</h2>
                                        </IonCardContent>
                                    </IonCard>
                                </IonCol>
                            </IonRow>
                        )}
                    </IonGrid>
                </IonContent>
            </IonApp>
        </IonApp>
    );
};

export default App;

Para tener un acceso directo a los campos utilizamos el hook useRef, este nos permite crear una referencia a estos.

const pesoRef = useRef<HTMLIonInputElement>(null);
const alturaRef = useRef<HTMLIonInputElement>(null);

A diferencia del uso de web components, en React podemos manejar los estados echando mando de los states (estados).

const calcularIMC = () => {
    const peso = pesoRef.current!.value;
    const altura = alturaRef.current!.value;
    if (!peso || !altura) {
        return;
    }
    const imc = +peso / (+altura * +altura);
    setCalculoDeIMC(imc);
};

Este valor puede ser luego mostrado mediante un condicional dentro de una IonCard.

{
    calculoDeIMC && (
        <IonRow>
            <IonCol>
                <IonCard>
                    <IonCardContent>
                        <h2>{calculoDeIMC}</h2>
                    </IonCardContent>
                </IonCard>
            </IonCol>
        </IonRow>
    );
}

Uso de múltiples componentes en React

React permite descomponer un componente como este en componentes mas pequeños que luego pueden ser importados y utilizados incluso dentro de otros componentes.

Supongamos que deseamos mover el botón de envío de nuestro componente principal a un subcomponente, para ello creamos el archivo components/ControlesIMC.tsx.

Ahora movemos el código relacionado con este.

import React, { useRef } from "react";
import {
    IonRow,
    IonCol,
    IonItem,
    IonLabel,
    IonInput,
    IonButton,
} from "@ionic/react";

const ControlesIMC: React.FC<{ onProcess: () => void }> = (props) => {
    return (
        <React.Fragment>
            <IonRow>
                <IonCol>
                    <IonButton onClick={props.onProcess}>Calcular</IonButton>
                </IonCol>
            </IonRow>
        </React.Fragment>
    );
};

export default ControlesIMC;

Dentro de App.tsx podemos importar nuestro componente y utilizarlo en donde antes se encontraba localizado el código.

// componentes
import ControlesIMC from "./components/ControlesIMC";
<ControlesIMC onProcess={calcularIMC} />

Crear subcomponentes nos permite reutilizar estos dentro de otros, de esta forma evitamos repetir los mismos bloques de código continuamente.

Agregar un alerta de error

El componente IonAlert nos permite crear una alerta con un mensaje y una serie de botones.

Encerramos nuestra IonApp dentro de un React.Fragment y en la parte mas superior de este agregamos un IonAlert.

return (
    <React.Fragment>
        <IonAlert
            isOpen={error !== ""}
            message={error}
            buttons={[
                {
                    text: "Ok",
                    handler: () => {
                        setError("");
                    },
                },
            ]}
        ></IonAlert>
        <IonApp>...</IonApp>
    </React.Fragment>
);

También necesitamos agregar un nuevo estado para el manejo de un posible error.

const [error, setError] = useState<string>("");

Y actualizar las validaciones de los cálculos para que el error se muestre cuando este no se ingrese un valor válido.

const calcularIMC = () => {
    const peso = pesoRef.current!.value;
    const altura = alturaRef.current!.value;
    if (!peso || !altura || +peso <= 0 || +altura <= 0) {
        setError("El peso y/o la altura son inválidos");
        return;
    }
    const imc = +peso / (+altura * +altura);
    setCalculoDeIMC(imc);
};

Cuando el peso o la altura no estén definidos, o cuando alguno de estos sea menor o igual a cero, setError definirá el valor del estado error.