🥇 Uso de dispositivos nativos en Ionic

Ahora que hemos logrado entender como utilizar los componentes de Ionic en React y como controlar los estados de una aplicación en React, requerimos aprender como utilizar los dispositivos nativos de un equipo como pueden ser la camara o el sistema de archivos.

Para ello Ionic utiliza la librería Capacitor que permite acceso a los elementos nativos de cada dispositivo. En este tutorial vamos a aprender como utilizar alguno de ellos para construir aplicaciones de Ionic.

🍿 Configuración inicial de un proyecto en Ionic

Creamos un proyecto en Ionic React y eliminamos los elementos que no se suelen utilizar como lo hemos venido haciendo.

🍿 Creación de los componentes básicos

La aplicación consiste en un gestor de memorias, momentos o recuerdos, para ello iniciamos con los siguientes componentes.

Empezamos creando BuenosRecuerdos.

import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import React from 'react';

export const BuenosRecuerdos: React.FC = () => {
    return (
        <IonPage>
            <IonHeader>
                <IonToolbar>
                    <IonTitle>Buenos Recuerdos</IonTitle>
                    <IonButtons slot="end">
                        <IonButton routerLink="/recuerdos/agregar">
                            <IonIcon slot="icon-only" icon={add} />
                        </IonButton>
                    </IonButtons>
                </IonToolbar>
            </IonHeader>
            <IonContent>
                <h2>Buenos Recuerdos</h2>
            </IonContent>
        </IonPage>
    )
}

Ahora creamos MalosRecuerdos.

import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import React from 'react';

export const MalosRecuerdos: React.FC = () => {
    return (
        <IonPage>
            <IonHeader>
                <IonToolbar>
                    <IonTitle>Malos Recuerdos</IonTitle>
                    <IonButtons slot="end">
                        <IonButton routerLink="/recuerdos/agregar">
                            <IonIcon slot="icon-only" icon={add} />
                        </IonButton>
                    </IonButtons>
                </IonToolbar>
            </IonHeader>
            <IonContent>
                <h2>Malos Recuerdos</h2>
            </IonContent>
        </IonPage>
    )
}

También es necesario un componente para agregar recuerdos AgregarRecuerdo.

import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import React from 'react';

export const AgregarRecuerdo: React.FC = () => {
    return (
        <IonPage>
            <IonHeader>
                <IonToolbar>
                    <IonButtons slot="start">
                        <IonBackButton defaultHref="/recuerdos/buenos" />
                    </IonButtons>
                    <IonTitle>Agregar Nuevo Recuerdo</IonTitle>
                </IonToolbar>
            </IonHeader>
            <IonContent>
                <h2>Nuevo Recuerdo</h2>
            </IonContent>
        </IonPage>
    )
}

Además se necesita un componente para mostrar rutas invalidas NotFound.

import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from "@ionic/react";
import React from 'react';

export const NotFound: React.FC = () => {
    return (
        <IonPage>
            <IonHeader>
                <IonToolbar>
                    <IonTitle>Pagina no encontrada</IonTitle>
                </IonToolbar>
            </IonHeader>
            <IonContent>
                <h2>Pagina no encontrada</h2>
            </IonContent>
        </IonPage>
    )
}

Implementamos las rutas dentro de nuestro router que se encuentra dentro de App.

export const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonTabs>
        <IonRouterOutlet>
          <Route exact path='/recuerdos/buenos' component={BuenosRecuerdos} />
          <Route exact path='/recuerdos/malos' component={MalosRecuerdos} />
          <Route exact path='/recuerdos/agregar' component={AgregarRecuerdo} />
          <Redirect exact path='/' to='/recuerdos/agregar' />
          <Route component={NotFound} />
        </IonRouterOutlet>
        <IonTabBar slot="bottom">
          <IonTabButton href="/recuerdos/buenos" tab="buenos-recuerdos">
            <IonIcon icon={happy} />
            <IonLabel>Buenos Recuerdos</IonLabel>
          </IonTabButton>
          <IonTabButton href="/recuerdos/malos" tab="malos-recuerdos">
            <IonIcon icon={sad} />
            <IonLabel>Malos Recuerdos</IonLabel>
          </IonTabButton>
        </IonTabBar>
      </IonTabs>
    </IonReactRouter>
  </IonApp>
);

🍿 Desplegar botones en base a la plataforma en uso

Vamos a complementar los componentes BuenosRecuerdos y MalosRecuerdos de forma que usemos un botón IconFab cuando la plataforma en uso sea Android. Y simultáneamente el botón + cuando estemos usando otro sistema.

Para ello usamos la función isPlatform de @ionic/react.

export const BuenosRecuerdos: React.FC = () => {
    return (
        <IonPage>
            <IonHeader>
                <IonToolbar>
                    <IonTitle>Buenos Recuerdos</IonTitle>
                    {!isPlatform('android') && (
                        <IonButtons slot="end">
                            <IonButton routerLink="/recuerdos/agregar">
                                <IonIcon slot="icon-only" icon={add} />
                            </IonButton>
                        </IonButtons>
                    )}
                </IonToolbar>
            </IonHeader>
            <IonContent>
                <h2>Buenos Recuerdos</h2>
                {!isPlatform('android') && (
                    <IonFab vertical="bottom" horizontal="end">
                        <IonFabButton routerLink="/recuerdos/agregar">
                            <IonIcon icon={add} />
                        </IonFabButton>
                    </IonFab>
                )}

            </IonContent>
        </IonPage>
    )
}
export const MalosRecuerdos: React.FC = () => {
    return (
        <IonPage>
            <IonHeader>
                <IonToolbar>
                    <IonTitle>Malos Recuerdos</IonTitle>
                    {!isPlatform('android') && (
                        <IonButtons slot="end">
                            <IonButton routerLink="/recuerdos/agregar">
                                <IonIcon slot="icon-only" icon={add} />
                            </IonButton>
                        </IonButtons>
                    )}
                </IonToolbar>
            </IonHeader>
            <IonContent>
                <h2>Malos Recuerdos</h2>
                {!isPlatform('android') && (
                    <IonFab vertical="bottom" horizontal="end">
                        <IonFabButton routerLink="/recuerdos/agregar">
                            <IonIcon icon={add} />
                        </IonFabButton>
                    </IonFab>
                )}
            </IonContent>
        </IonPage>
    )
}

🍿 Como utilizar de Capacitor

Para tomar una fotografía podemos echar mano de capacitor. En el Sitio Web Oficial de Capacitor se puede encontrar toda la información acerca de las funcionalidades que capacitor integra, como pueden ser el uso de la camara mediante capacitor.

Para agregar capacitor a un proyecto existente, utiliza.

ionic integrations enable capacitor

Cuando se complete a instalación se creará un archivo capacitor.config.json.

{
  "appId": "io.apuntes.miapp",
  "appName": "miapp",
  "bundledWebRuntime": false,
  "npmClient": "npm",
  "webDir": "build",
  "plugins": {
    "SplashScreen": {
      "launchShowDuration": 0
    }
  },
  "cordova": {}
}

Ahora compilamos nuestra aplicación.

yarn build

Al terminar de compilar el proyecto tendremos un directorio build listo para producción, este directorio con los compilados es necesario para ejecutar nuestra aplicación.

Para agregar soporte de Android en capacitor utilizamos.

ionic capacitor add android

🍿 ¿Cómo compilar una aplicación Android en Ionic?

Después de ejecutar el build con Ionic procedemos a realizar la compilación en Android.

ionic capacitor build android

🍿 ¿Cómo abrir una aplicación Android con Ionic?

Ahora para abrir la aplicación en Android utilizando Ionic ejecutamos el siguiente comando.

ionic capacitor open android

Después de ejecutar el comando anterior Android Studio abrirá y podremos ejecutar la aplicación que acabamos de compilar, siempre que tengamos las librerías de Android necesarias así como la versión de Android requerida.

🍿 ¿Cómo asignar permisos de uso de la cámara en Ionic?

Para utilizar ciertos dispositivos las aplicaciones solicitan dichos permisos al momento de intentar acceder a ellos.

Vamos al archivo app/src/main/AndroidManifest.xml y eliminamos todos los permisos que no requerimos.

<!-- Permissions -->

<!-- Camera, Photos, input file -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Network API -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Navigator.getUserMedia -->
<!-- Video -->
<uses-permission android:name="android.permission.CAMERA" />

En el caso anterior hemos dejado solo los permisos de lectura y escritura de archivos, así como de acceso a la red y la cámara.

Ahora podemos complementar la aplicación para capturar la imagen utilizando la librería capacitor.

import { CameraResultType, CameraSource, Plugins } from "@capacitor/core";
// ...

const { Camera } = Plugins;

export const AgregarRecuerdo: React.FC = () => {

    const [foto, setFoto] = useState<{
        path: string;
        preview: string;
    }>();

    const tomarFotoHandler = async () => {
        const foto = await Camera.getPhoto({
            resultType: CameraResultType.Uri,
            source: CameraSource.Camera,
            quality: 90,
            width: 500,
        });
        if (!foto || !foto.path || !foto.webPath) {
            return;
        }
        setFoto({
            path: foto.path,
            preview: foto.webPath,
        });
    };

    return (
        <IonPage>
            <IonHeader>
                <IonToolbar>
                    <IonButtons slot="start">
                        <IonBackButton defaultHref="/recuerdos/buenos" />
                    </IonButtons>
                    <IonTitle>Agregar Nuevo Recuerdo</IonTitle>
                </IonToolbar>
            </IonHeader>
            <IonContent className="ion-padding">
                <IonGrid>
                    <IonRow>
                        <IonCol>
                            <IonItem>
                                <IonLabel position="floating">Titulo</IonLabel>
                                <IonInput type="text" />
                            </IonItem>
                        </IonCol>
                    </IonRow>
                    {!foto ? (
                        <IonRow>
                            <IonCol>
                                <h2>No ha seleccionado una imagen aun.</h2>
                            </IonCol>
                        </IonRow>
                    ) : (
                        <IonRow>
                            <IonCol>
                                <img src={foto?.preview} alt="Vista previa de la foto" />
                            </IonCol>
                        </IonRow>
                    )}
                    <IonRow>
                        <IonCol>
                            <IonButton fill="outline" onClick={tomarFotoHandler}>
                                <IonIcon icon={camera} slot="start" />
                                <IonLabel slot="end">Tomar foto</IonLabel>
                            </IonButton>
                        </IonCol>
                    </IonRow>
                    // ...
                </IonGrid>
            </IonContent>
        </IonPage>
    );
};

Agregamos el paquete @ionic/react-hooks.

yarn add @ionic/react-hooks

🍿 ¿Cómo almacenar los datos en un Context en React?

Para almacenar la información en el nivel mas superior creamos un context dentro de RecuerdosContext.

import React from "react";

export interface Recuerdo {
    id: string;
    path: string;
    titulo: string;
    tipo: "bueno" | "malo";
}

export const RecuerdosContext = React.createContext<{
    recuerdos: Recuerdo[];
    agregarRecuerdo: (path: string, titulo: string, tipo: "bueno" | "malo") => void;
}>({
    recuerdos: [],
    agregarRecuerdo: () => {},
});

Nuestro Context contiene un arreglo recuerdos: Recuerdo[] y agregarRecuerdo que permitirá agregar valores a este arreglo.

Creamos un ContextContainer implementando RecuerdosContext para tal caso.

import React, { useState } from "react";

import { RecuerdosContext, Recuerdo } from "./recuerdos.context";

export const RecuerdosContextProvider: React.FC = props => {
    const [recuerdos, setRecuerdos] = useState<Recuerdo[]>([]);

    const agregarRecuerdo = (path: string, titulo: string, tipo: "bueno" | "malo") => {
        const nuevoRecuerdo: Recuerdo = {
            id: Math.random().toString(),
            titulo,
            tipo,
            path,
        };
        setRecuerdos([...recuerdos, nuevoRecuerdo]);
    };

    return (
        <RecuerdosContext.Provider
            value={{
                agregarRecuerdo,
                recuerdos,
            }}>
            {props.children}
        </RecuerdosContext.Provider>
    );
};

Un ContextContainer permite englobar los valores de recuerdos: Recuerdo[] y manipularlos a través de addRecuerdo. En este ejemplo se controlan los estados utilizando el estado (state) useState<Recuerdo[]>.

Vamos a implementar nuestro ContextContainer dentro de nuestro router en App.

export const App: React.FC = () => (
    <IonApp>
        <IonReactRouter>
            <RecuerdosContextProvider>
                // ...
            </RecuerdosContextProvider>
        </IonReactRouter>
    </IonApp>
);

Realizamos la implementación utilizando los elementos de @capacitor/core.

import { CameraResultType, CameraSource, Filesystem, FilesystemDirectory, Plugins } from "@capacitor/core";
// ...
import { base64FromPath } from "@ionic/react-hooks/filesystem";
import { RecuerdosContext } from "../data/recuerdos.context";
import { useHistory } from "react-router";
const { Camera } = Plugins;

export const AgregarRecuerdo: React.FC = () => {

    // estado para controlar la foto elegida
    const [foto, setFoto] = useState<{
        path: string;
        preview: string;
    }>();

    // estado para controlar el recuerdo elegido
    const [tipoDeRecuerdo, setTipoDeRecuerdo] = useState<"bueno" | "malo">();

    // controlador del context (información global)
    const recuerdosCtx = useContext(RecuerdosContext);

    // control del manejo del history para mover la pagina
    const history = useHistory();

    // referencia al campo titulo para extraer su valor
    const tituloRef = useRef<HTMLIonInputElement>(null);

    // handler para controlar el cambio de tipo de recuerdo
    const seleccionarTipoDeRecuerdoHandler = (event: CustomEvent) => {
        const tipoDeRecuerdoSeleccionado = event.detail.value;
        setTipoDeRecuerdo(tipoDeRecuerdoSeleccionado);
    };

    // implementación del plugin Camera de capacitor para capturar fotos
    const tomarFotoHandler = async () => {
        const nuevaFoto = await Camera.getPhoto({
            resultType: CameraResultType.Uri,
            source: CameraSource.Camera,
            quality: 80,
            width: 500,
        });
        if (!nuevaFoto || !nuevaFoto.path || !nuevaFoto.webPath) {
            return;
        }
        setFoto({
            path: nuevaFoto.path,
            preview: nuevaFoto.webPath,
        });
    };

    // handler para agregar al recuerdo utilizando el context
    const agregarRecuerdoHandler = async () => {
        const titulo = tituloRef.current?.value;

        if (!titulo || titulo.toString().trim().length === 0 || !tipoDeRecuerdo || !foto) {
            return;
        }

        const nombreDeArchivo = new Date().getTime() + ".jpg";
        const base64 = await base64FromPath(foto!.preview);

        Filesystem.writeFile({
            path: nombreDeArchivo,
            data: base64,
            directory: FilesystemDirectory.Data,
        });

        recuerdosCtx.agregarRecuerdo(nombreDeArchivo, titulo.toString().trim(), tipoDeRecuerdo);

        history.replace("/recuerdos/buenos");
    };

    return (
        <IonPage>
            // ...
            <IonContent className="ion-padding">
                <IonGrid>
                    <IonRow>
                        <IonCol>
                            <IonItem>
                                <IonInput type="text" ref={tituloRef} placeholder="Titulo" />
                            </IonItem>
                        </IonCol>
                    </IonRow>
                    <IonRow>
                        <IonCol>
                            <IonSelect onIonChange={seleccionarTipoDeRecuerdoHandler} placeholder="Tipo de recuerdo">
                                <IonSelectOption value="bueno">Buen recuerdo</IonSelectOption>
                                <IonSelectOption value="malo">Mal recuerdo</IonSelectOption>
                            </IonSelect>
                        </IonCol>
                    </IonRow>
                    {!foto ? (
                        <IonRow>
                            <IonCol>
                                <h2>No ha seleccionado una imagen aun.</h2>
                            </IonCol>
                        </IonRow>
                    ) : (
                        <IonRow>
                            <IonCol>
                                <img src={foto?.preview} alt="Vista previa de la foto" />
                            </IonCol>
                        </IonRow>
                    )}
                    <IonRow>
                        <IonCol>
                            <IonButton fill="outline" onClick={tomarFotoHandler}>
                                <IonIcon icon={camera} slot="start" />
                                <IonLabel slot="end">Tomar foto</IonLabel>
                            </IonButton>
                        </IonCol>
                    </IonRow>
                    <IonRow>
                        <IonCol>
                            <IonButton fill="outline" onClick={agregarRecuerdoHandler}>
                                <IonIcon icon={add} />
                                <IonLabel>Agregar</IonLabel>
                            </IonButton>
                        </IonCol>
                    </IonRow>
                </IonGrid>
            </IonContent>
        </IonPage>
    );
};

Actualizar el componente BuenosRecuerdos para mostrar los recuerdos almacenados mediante el ContextContainer.

// ...
import React, { useContext } from "react";
import { RecuerdosContext } from "../data/recuerdos.context";

export const BuenosRecuerdos: React.FC = () => {
    const recuerdosCtx = useContext(RecuerdosContext);
    const buenosRecuerdos = recuerdosCtx.recuerdos.filter(recuerdo => recuerdo.tipo === "bueno");

    return (
        <IonPage>
            // ...
            <IonContent>
                <h2>Buenos Recuerdos</h2>
                <IonGrid>
                    {buenosRecuerdos.map(recuerdo => {
                        return (
                            <IonRow key={recuerdo.id}>
                                <IonCol>
                                    <IonCard>
                                        <img src={recuerdo.path} alt={recuerdo.titulo} />
                                        <IonCardHeader>
                                            <IonCardTitle>{recuerdo.titulo}</IonCardTitle>
                                        </IonCardHeader>
                                    </IonCard>
                                </IonCol>
                            </IonRow>
                        );
                    })}
                </IonGrid>
                // ...
            </IonContent>
        </IonPage>
    );
};

🍿 ¿Cómo almacenar los datos en el System Storage (Sitema de Archivos) con Ionic?

Primero dentro de RercuerdosContext vamos a expandir la interface Recuerdo para que podamos guardar un string como base64.

export interface Recuerdo {
    id: string;
    path: string;
    titulo: string;
    tipo: "bueno" | "malo";
    base64url: string;
}

Hay que complementar también RecuerdosContext para que la función agregarRecuerdo reciba el base64 además de initContext que nos permitirá tomar los valores iniciales del sistema de archivos e inyectarlos en el context.

export const RecuerdosContext = React.createContext<{
    recuerdos: Recuerdo[];
    agregarRecuerdo: (path: string, base64url: string, titulo: string, tipo: "bueno" | "malo") => void;
    initContext: () => void;
}>({
    recuerdos: [],
    agregarRecuerdo: () => {},
    initContext: () => {},
});

Para la implementación del ContextProvider agregamos una interface que excluya el base64 y solo contenga id, titulo, tipo y path como parte de la información de cada Recuerdo.

interface RecuerdoAlmacenable {
    id: string;
    titulo: string;
    tipo: "bueno" | "malo";
    path: string;
}

Además requerimos actualizar el ContextProvider de forma que:

  • Tenga un observer que actualice los valores en el filesystem cada que cambie el arreglo Recuerdos.
  • Invoque la carga de datos al cargar el ContextProvider y solo durante la primera llamada a este.
export const RecuerdosContextProvider: React.FC = props => {
    const [recuerdos, setRecuerdos] = useState<Recuerdo[]>([]);

    useEffect(() => {
        const recuerdosAlmacenables: RecuerdoAlmacenable[] = recuerdos.map(recuerdo => {
            return {
                id: recuerdo.id,
                titulo: recuerdo.titulo,
                tipo: recuerdo.tipo,
                path: recuerdo.path,
            };
        });
        Storage.set({ key: "recuerdos", value: JSON.stringify(recuerdosAlmacenables) });
    }, [recuerdos]);

    const agregarRecuerdo = (path: string, base64url: string, titulo: string, tipo: "bueno" | "malo") => {
        const nuevoRecuerdo: Recuerdo = {
            id: Math.random().toString(),
            titulo,
            tipo,
            path,
            base64url,
        };
        setRecuerdos([...recuerdos, nuevoRecuerdo]);
    };

    const initContext = useCallback(async () => {
        const datosEnElStorage = await Storage.get({ key: "recuerdos" });

        const recuerdosAlmacenados: RecuerdoAlmacenable[] = datosEnElStorage.value
            ? JSON.parse(datosEnElStorage.value)
            : [];

        const recuerdos: Recuerdo[] = [];
        for (const recuerdoAlmacenado of recuerdosAlmacenados) {
            const archivo = await Filesystem.readFile({
                path: recuerdoAlmacenado.path,
                directory: FilesystemDirectory.Data,
            });
            recuerdos.push({ ...recuerdoAlmacenado, base64url: `data:image/jpeg;base64, ${archivo.data}` });
        }

        setRecuerdos(recuerdos);
    }, []);

    return (
        <RecuerdosContext.Provider
            value={{
                agregarRecuerdo,
                recuerdos,
                initContext,
            }}>
            {props.children}
        </RecuerdosContext.Provider>
    );
};

Movemos la implementación del ContextContainer del router al index en donde englobaremos el componente App.

import React from "react";
import ReactDOM from "react-dom";
import { App } from "./app";
import { RecuerdosContextProvider } from "./data/recuerdos.context-provider";
import * as serviceWorker from "./serviceWorker";

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

Removemos la implementación del ContextContainer del router e implementamos el observer useEffect para que se invoque solo al arrancar el el initContext().

export const App: React.FC = () => {
    const recuerdosCtx = useContext(RecuerdosContext);

    const { initContext } = recuerdosCtx;

    useEffect(() => {
        initContext();
    }, [initContext]);

    return (
        <IonApp>
            <IonReactRouter>
                <IonTabs>
                    // ...
                </IonTabs>
            </IonReactRouter>
        </IonApp>
    );
};

Actualizamos la llamada a agregarRecuerdo dentro de AgregarRecuerdo.

const agregarRecuerdoHandler = async () => {

    // ...

    recuerdosCtx.agregarRecuerdo(nombreDeArchivo, base64, titulo.toString().trim(), tipoDeRecuerdo);

    history.replace("/recuerdos/buenos");
};

Actualizar las referencias de path a base64url.

<IonGrid>
    {buenosRecuerdos.map(recuerdo => {
        return (
            <IonRow key={recuerdo.id}>
                <IonCol>
                    <IonCard>
                        <img src={recuerdo.base64url} alt={recuerdo.titulo} />
                        <IonCardHeader>
                            <IonCardTitle>{recuerdo.titulo}</IonCardTitle>
                        </IonCardHeader>
                    </IonCard>
                </IonCol>
            </IonRow>
        );
    })}
</IonGrid>

Actualizamos también la referencia del botón IonFab.

{isPlatform("android") && (
    <IonFab vertical="bottom" horizontal="end" slot="fixed">
        <IonFabButton routerLink="/recuerdos/agregar">
            <IonIcon icon={add} />
        </IonFabButton>
    </IonFab>
)}

🍿 Refactorización del código consolidando la lista y los items

Vamos a crear dos nuevos componentes, uno RecuerdoLista y otro RecuerdoItem.

En RecuerdoItem vamos a extraer el código relativo a cada una de las filas de los recuerdos.

import { IonCard, IonCardHeader, IonCardTitle } from "@ionic/react";
import React from "react";
import { Recuerdo } from "../data/recuerdos.context";

export const RecuerdoItem: React.FC<{ item: Recuerdo }> = props => {
    return (
        <IonCard>
            <img src={props.item.base64url} alt={props.item.titulo} />
            <IonCardHeader>
                <IonCardTitle>{props.item.titulo}</IonCardTitle>
            </IonCardHeader>
        </IonCard>
    );
};

Para enviar los parámetros utilizados por este componente, utilizamos la propiedad item del tipo Recuerdo.

En RecuerdoLista realizamos la implementación del recorrido del arreglo de recuerdos.

import { IonCol, IonGrid, IonRow } from "@ionic/react";
import React from "react";
import { Recuerdo } from "../data/recuerdos.context";
import { RecuerdoItem } from "./recuerdo-item.component";

export const RecuerdosLista: React.FC<{ items: Recuerdo[] }> = props => {
    return (
        <IonGrid>
            {props.items.map(recuerdo => {
                return (
                    <IonRow key={recuerdo.id}>
                        <IonCol>
                            <RecuerdoItem item={recuerdo} />
                        </IonCol>
                    </IonRow>
                );
            })}
        </IonGrid>
    );
};

En este caso dentro de items tenemos un arreglo de Recuerdo[].

Ahora dentro de los componentes BuenosRecuerdos y MalosRecuerdos realiamos la implementación.

// ...

export const BuenosRecuerdos: React.FC = () => {
    const recuerdosCtx = useContext(RecuerdosContext);
    const recuerdos = recuerdosCtx.recuerdos.filter(recuerdo => recuerdo.tipo === "bueno");

    return (
        <IonPage>
            // ...
            <IonContent>
                <h2>Buenos Recuerdos</h2>
                <RecuerdosLista items={recuerdos} />
                {isPlatform("android") && (
                    <IonFab vertical="bottom" horizontal="end" slot="fixed">
                        <IonFabButton routerLink="/recuerdos/agregar">
                            <IonIcon icon={add} />
                        </IonFabButton>
                    </IonFab>
                )}
            </IonContent>
        </IonPage>
    );
};
// ...

export const MalosRecuerdos: React.FC = () => {
    const recuerdosCtx = useContext(RecuerdosContext);
    const recuerdos = recuerdosCtx.recuerdos.filter(recuerdo => recuerdo.tipo === "malo");

    return (
        <IonPage>
            // ...
            <IonContent>
                <h2>Malos Recuerdos</h2>
                <RecuerdosLista items={recuerdos} />
                {isPlatform("android") && (
                    <IonFab vertical="bottom" horizontal="end" slot="fixed">
                        <IonFabButton routerLink="/recuerdos/agregar">
                            <IonIcon icon={add} />
                        </IonFabButton>
                    </IonFab>
                )}
            </IonContent>
        </IonPage>
    );
};

🍿 Uso de la cámara dentro del navegador

Para poder utilizar la cámara a nivel del navegador requerimos instalar el siguiente componente.

yarn add @ionic/pwa-elements

Una ves instalado el paquete @ionic/pwa-elements realizamos la implementación sobre el objeto window.

import { defineCustomElements } from "@ionic/pwa-elements/loader";
// ...

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

serviceWorker.unregister();

defineCustomElements(window);

Como capacitor no regresa una propiedad path al subir una imagen pero como hemos creado un nombre personalizado podemos solo basarnos en dicho nombre para retraer las imágenes.

// ...

export const AgregarRecuerdo: React.FC = () => {

    const [foto, setFoto] = useState<{
        path: string | undefined;
        preview: string;
    }>();

    // ...

    const seleccionarTipoDeRecuerdoHandler = (event: CustomEvent) => {
        const tipoDeRecuerdoSeleccionado = event.detail.value;
        setTipoDeRecuerdo(tipoDeRecuerdoSeleccionado);
    };

    const tomarFotoHandler = async () => {
        const nuevaFoto = await Camera.getPhoto({
            resultType: CameraResultType.Uri,
            source: CameraSource.Camera,
            quality: 80,
            width: 500,
        });
        if (!nuevaFoto || !nuevaFoto.webPath) {
            return;
        }
        setFoto({
            path: nuevaFoto.path,
            preview: nuevaFoto.webPath,
        });
    };

    const agregarRecuerdoHandler = async () => {
        const titulo = tituloRef.current?.value;

        if (!titulo || titulo.toString().trim().length === 0 || !tipoDeRecuerdo || !foto) {
            return;
        }

        const nombreDeArchivo = new Date().getTime() + ".jpg";
        const base64 = await base64FromPath(foto!.preview);

        await Filesystem.writeFile({
            path: nombreDeArchivo,
            data: base64,
            directory: FilesystemDirectory.Data,
        });

        recuerdosCtx.agregarRecuerdo(nombreDeArchivo, base64, titulo.toString().trim(), tipoDeRecuerdo);

        history.replace("/recuerdos/buenos");
    };

    return (
        <IonPage>
            // ...
        </IonPage>
    );
};

Si probamos nuevamente la aplicación nos daremos cuenta que funciona correctamente. En el caso del navegador Chrome los datos se guardan dentro del IndexedDB, para acceder a este hay que abrir Application > Storage > IndexedDB.