Inicio / Artículos / Cambio persistente de temas en React Native con Context y Hooks

Cambio persistente de temas en React Native con Context y Hooks

Crea un cambiador de temas para React Native utilizando Context, useState y useEffect: navegación por pestañas, selector de temas, persistencia con AsyncStorage y una pantalla de carga durante el inicio.

1618 palabras

Cambiar de tema parece ser un problema de estilo, pero en realidad es un problema de estado: el tema actual debe ser legible desde cualquier pantalla, poderse cambiar desde una de ellas y seguir existiendo después del reinicio de la aplicación. Eso lo convierte en un buen ejercicio para aprender Hooks con algo más realista que un contador. Esta guía crea una pequeña aplicación React Native con dos pestañas, un selector de temas y almacenamiento persistente, utilizando useContext, useState y useEffect para reemplazar lo que antes requería componentes de clase y una cantidad considerable de código genérico.

Por qué los Hooks son adecuados para este problema

Los Hooks llegaron a React 16.8, y React Native obtuvo soporte estable con la versión 0.59. Antes de ellos, compartir un tema implicaba un proveedor basado en clases que gestionaba el estado, métodos de ciclo de vida para cargar las preferencias guardadas, y propiedades de renderizado o consumidores dispersos por todo el árbol. Con los Hooks, el proveedor se convierte en un componente funcional sencillo, el estado reside en useState, la carga se realiza en useEffect, y los consumidores leen el valor con useContext. La lógica termina en un único archivo pequeño en lugar de estar distribuida entre métodos de ciclo de vida.

Las versiones mencionadas aquí reflejan cuándo se hizo posible por primera vez este enfoque; las versiones actuales de React Native ya soportan los Hooks por defecto, por lo que cualquier proyecto reciente funciona sin problemas.

Configuración del proyecto

Necesita un proyecto de React Native con versión 0.59 o superior. Se puede generar uno nuevo con react-native init RNThemeProvider (las herramientas más recientes utilizan la CLI de la comunidad o Expo en su lugar; consulte la documentación actual para comenzar).

Se añaden dos bibliotecas adicionales:

  • react-navigation proporciona la navegación basada en pestañas.
  • AsyncStorage almacena el tema seleccionado en el dispositivo. Fue eliminado del núcleo de React Native y ahora se distribuye como un paquete separado de la comunidad (actualmente publicado como @react-native-async-storage/async-storage), por lo que debe instalarse por separado.

En versiones anteriores de React Native, también era necesario vincular manualmente los módulos nativos después de la instalación; con el enlazado automático en las versiones modernas, este paso suele ser innecesario.

Organización de los archivos

Una estructura pequeña y predecible mantiene la lógica de los temas separada de las pantallas y la navegación:

- src
  — components
    — TabBar.js # custom bottom tabbar component
  — core
    — themeProvider.js # custom hook for theming
    — themes.json # JSON array containing our themes
  — screens
    — Main.js # first tab
    — Settings.js # second tab
  — App.js # navigation part
— index.js # entry point for react-native
— package.json # dependencies

La carpeta core alberga todo lo relacionado con los temas: themes.json con las definiciones de los temas y themeProvider.js con el contexto, el proveedor y las herramientas auxiliares. Las pantallas se encuentran en screens, la barra de pestañas personalizada en components, y App.js se encarga de conectar la navegación.

Construyendo la estructura de navegación

Comience sin ningún tipo de tematización. En App.js, cree un navegador de pestañas en la parte inferior con dos pestañas: Main para el contenido de la aplicación y Settings para las preferencias. Cada pestaña apunta a un componente funcional simple en Main.js y Settings.js que, por ahora, solo muestra texto de ejemplo.

Ejecuta la aplicación en esta etapa. Si aparecen dos pestañas y puedes alternar entre ellas, el esqueleto está listo y cada paso posterior solo añade funcionalidades. Las API de navegación han cambiado en las versiones principales de react-navigation, por lo que debes seguir la configuración correspondiente a la versión que instales en lugar de copiar literalmente ejemplos antiguos.

Definición de temas y la interfaz del selector

Temas como datos

Cada tema es un objeto en themes.json que cuenta con tres campos: una key única que lo identifica, un color de fondo y un color de texto. Al almacenar los temas en formato JSON en lugar de código, resulta fácil ampliarlos; un generador de paletas como Coolors es una forma rápida de encontrar combinaciones de colores que funcionen bien juntas.

themeProvider.js importa ese archivo y, por ahora, exporta dos elementos: el array completo de temas que se muestra en la pantalla de configuración y un tema por defecto (la segunda entrada del array). La lógica del proveedor se implementará más adelante.

Las pantallas y la barra de pestañas

La pantalla Settings utiliza un FlatList que muestra una fila por cada tema, con el título formateado según el tema actual. La pantalla Main también aplica el tema actual a su fondo y texto.

Finalmente, la barra de pestañas también debe reflejar el tema. Un componente personalizado TabBar en components/TabBar.js muestra las pestañas y utiliza el color del tema para la pestaña activa. Este componente se registra en el navegador de pestañas de App.js a través de las opciones del navegador para incluir un componente personalizado de barra de pestañas.

En este punto la aplicación parece tener un tema definido, pero solo con el valor predeterminado codificado de antemano. Todavía no hay reacción alguna a los toques. Ahí es donde entran en juego los Hooks.

Compartir el tema con useContext

React Context permite que un valor fluya a través del árbol sin tener que pasar propiedades en cada nivel. Si el concepto de Context es nuevo para usted, la documentación de React Context explica su funcionamiento.

Dentro de themeProvider.js, cree un contexto para el tema y un componente proveedor, ThemeContextProvider. Envuelva al navegador en App.js con ese proveedor para que todas las pantallas y la barra de pestañas queden bajo su control.

Para facilitar el uso del contexto, añada un componente de orden superior withTheme al mismo archivo. Este lee el contexto con useContext y transfiere la temática al componente envuelto como prop. Actualice Main, Settings y TabBar para que se exporten a través de withTheme; de esta manera recibirán la temática actual sin saber de dónde proviene. La guía de componentes de orden superior explica este patrón con más detalle.

A HOC funciona bien cuando los componentes ya esperan props. En un códigobase basado en Hooks, un pequeño hook useTheme que devuelve useContext(ThemeContext) suele ser más sencillo, evita una capa adicional de envoltorio y hace que la dependencia sea visible dentro del componente. Nuestra guía sobre hooks personalizados y reutilización de lógica explica por qué los hooks comparten lógica en lugar de estado, y ese es precisamente el motivo por el cual aquí todavía se necesita el contexto.

Cambiar el tema con useState

Ahora hagamos que el selector funcione. Dentro de ThemeContextProvider, guardemos el tema actual en useState, inicializado con el tema por defecto. Incluyamos tanto el tema como una función setTheme en el valor del contexto.

En la pantalla de Ajustes, llama a setTheme cuando se toque una fila en el FlatList. Dado que el estado del proveedor cambia, cada componente que lee el contexto se vuelve a renderizar con los nuevos colores: las pantallas y la barra de pestañas cambian de inmediato.

Un detalle que vale la pena adoptar: si el valor del contexto es un objeto nuevo en cada renderizado del proveedor, todos los consumidores se vuelven a renderizar cada vez que lo hace el proveedor. Memorizar el valor con useMemo, utilizando como clave el tema, evita esto. En una aplicación tan pequeña apenas importa, pero se vuelve relevante cuando muchos componentes consumen el contexto.

Persistir la elección con AsyncStorage

Tocar un tema ahora funciona, pero la selección se pierde al volver a cargar la página. Amplía la función setTheme de modo que, además de actualizar el estado, escriba la selección en AsyncStorage. Almacenar la key del tema en lugar del objeto completo es la opción más fiable: si posteriormente se modifican los colores de un tema en themes.json, los usuarios recibirán la versión actualizada en lugar de una copia obsoleta.

Restaurar el tema al iniciar la aplicación con useEffect

El último paso es leer el tema guardado cuando la aplicación se inicia. useEffect se ejecuta después de que el componente se renderice, por lo que es el lugar adecuado para efectos secundarios como la lectura del almacenamiento. En términos de clases, cubre lo que anteriormente manejaban componentDidMount y componentDidUpdate; la comparación a veces mencionada con componentWillReceiveProps es engañosa, ya que los efectos se ejecutan después de la renderización y no antes de que lleguen las nuevas propiedades.

Dentro de ThemeContextProvider, añada un efecto que lea la clave guardada en AsyncStorage, encuentre el tema correspondiente y llame al setter de estado. Dos detalles garantizan que esto funcione correctamente:

  • Pasa un array de dependencias vacío. El efecto debe ejecutarse una sola vez, cuando se monte el proveedor, y no después de cada renderizado. La referencia de la API de Hooks explica cómo el array de dependencias controla cuándo se dispara un efecto.
  • Gestiona el estado de carga. AsyncStorage es asíncrono, por lo que el primer renderizado ocurre antes de conocer el tema guardado. Debes controlar si la carga ya ha finalizado y no mostrar nada (o una vista temporal) hasta que lo haga. De lo contrario, los usuarios verán el tema por defecto durante un instante antes de que aparezca su elección.

También debes manejar el caso en que aún no se haya almacenado nada, o la clave guardada ya no corresponda a un tema, recurriendo al tema por defecto. De esta manera, la selección se mantiene incluso después de reiniciar la aplicación.

Conclusión

El proveedor final es un componente funcional único que gestiona el estado del tema, mantiene persistentes los cambios, los restaura al iniciar y expone todo a través del contexto. En comparación con la versión basada en clases, la lógica es más breve y se lee de arriba hacia abajo.

Varios puntos son aplicables también a otras funcionalidades:

  • Mantenga las preferencias de interfaz compartidas en un proveedor de contexto cerca de la raíz, y exponga un método para establecer valores junto con el propio valor.
  • Mantenga persistentes los identificadores, no objetos completos, de modo que los cambios en los datos no dejen copias obsoletas en los dispositivos.
  • Trate las lecturas asincrónicas al iniciar como un estado de carga en lugar de mostrar valores predeterminados y cambiarlos posteriormente.
  • Una vez que se sienta cómodo con estos tres Hooks, explore useReducer para estados más complejos, useRef para valores mutables que no deben provocar actualizaciones de la interfaz, y useLayoutEffect para tareas que deben realizarse antes de la renderización. Convertir un componente de clase existente es una buena forma de practicar.
  • Lecturas relacionadas