c
Comunicándose con el servidor
Hasta ahora hemos implementado funciones en nuestra aplicación sin ninguna comunicación real con el servidor. Por ejemplo, la lista de repositorios revisados que hemos implementado utiliza datos simulados y el formulario de inicio de sesión no envía las credenciales del usuario a ningún endpoint de autorización. En esta sección, aprenderemos cómo comunicarnos con un servidor mediante solicitudes HTTP, cómo usar Apollo Client en una aplicación React Native y cómo almacenar datos en el dispositivo del usuario.
Pronto aprenderemos a comunicarnos con un servidor en nuestra aplicación. Antes de llegar a eso, necesitamos un servidor con el que comunicarnos. Para ello, tenemos una implementación de servidor completa en el repositorio rate-repository-api. El servidor rate-repository-api satisface todas las necesidades de API de nuestra aplicación durante esta parte. Utiliza la base de datos SQLite que no necesita ninguna configuración y proporciona una API Apollo GraphQL junto con algunos endpoints de la API REST.
Antes de profundizar más en el material, configure el servidor rate-repository-api siguiendo las instrucciones de configuración en el README del repositorio . Tenga en cuenta que si está utilizando un emulador para el desarrollo, se recomienda ejecutar el servidor y el emulador en la misma computadora. Esto facilita considerablemente las solicitudes de red.
solicitudes HTTP
React Native proporciona Fetch API para realizar solicitudes HTTP en nuestras aplicaciones. React Native también es compatible con la antigua API XMLHttpRequest que hace posible el uso de bibliotecas de terceros como Axios. Estas API son las mismas que las del entorno del navegador y están disponibles globalmente sin necesidad de importarlas.
Las personas que han utilizado tanto la API Fetch como la API XMLHttpRequest probablemente estén de acuerdo en que la API Fetch es más fácil de usar y más moderna. Sin embargo, esto no significa que XMLHttpRequest API no tenga sus usos. En aras de la simplicidad, solo usaremos la API Fetch en nuestros ejemplos.
El envío de solicitudes HTTP mediante la API Fetch se puede realizar mediante la función fetch. El primer argumento de la función es la URL del recurso:
fetch('https://my-api.com/get-end-point');
El método de solicitud predeterminado es GET. El segundo argumento de la función fetch es un objeto de opciones, que puede utilizar, por ejemplo, para especificar un método de solicitud diferente, encabezados de solicitud o cuerpo de solicitud:
fetch('https://my-api.com/post-end-point', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstParam: 'firstValue',
secondParam: 'secondValue',
}),
});
Tenga en cuenta que estas URL están inventadas y (lo más probable) no enviarán una respuesta a sus solicitudes. En comparación con Axios, la API Fetch opera en un nivel un poco más bajo. Por ejemplo, no hay ninguna serialización ni análisis del cuerpo de solicitud o respuesta. Esto significa que, por ejemplo, debe configurar el encabezado Content-Type usted mismo y usar el método JSON.stringify para serializar el cuerpo de la solicitud.
La función fetch devuelve una promesa que resuelve un objeto Response. Tenga en cuenta que los códigos de estado de error como 400 y 500 no se rechazan como, por ejemplo, en Axios. En el caso de una respuesta con formato JSON, podemos analizar el cuerpo de la respuesta utilizando el método Response.json:
const fetchMovies = async () => {
const response = await fetch('https://reactnative.dev/movies.json');
const json = await response.json();
return json;
};
Para una introducción más detallada a la API de Fetch, lea el artículo Using Fetch en los documentos web de MDN.
A continuación, probemos la API Fetch en la práctica. El servidor rate-repository-api proporciona un punto final para devolver una lista paginada de repositorios revisados. Una vez que el servidor se esté ejecutando, debería poder acceder al endpoint en http://localhost:5000/api/repositories. Los datos están paginados en un formato de paginación basado en cursor común. Los datos reales del repositorio están detrás de la clave node en la matriz edges.
Desafortunadamente, no podemos acceder al servidor directamente en nuestra aplicación usando la URL http://localhost:5000/api/repositories. Para realizar una solicitud a este endpoint en nuestra aplicación, necesitamos acceder al servidor usando su dirección IP en su red local. Para saber cuál es, abra las herramientas de desarrollo de Expo ejecutando npm start. En las herramientas de desarrollo, debería poder ver una URL que comience con exp:// encima del código QR:
Copie la dirección IP entre exp:// y :, que en este ejemplo es 192.168.100.16. Construya una URL en formato http://<IP_ADDRESS>:5000/api/repositories y ábrala en el navegador.
Ahora que conocemos la URL del endpoint, usemos los datos reales proporcionados por el servidor en nuestra lista de repositorios revisados. Actualmente estamos usando datos simulados almacenados en la variable repositories. Elimine la variable repositories y reemplace el uso de los datos simulados con este fragmento de código en el archivo RepositoryList.jsx en el directorio components:
import React, { useState, useEffect } from 'react';
// ...
const RepositoryList = () => {
const [repositories, setRepositories] = useState();
const fetchRepositories = async () => {
// Replace the IP address part with your own IP address!
const response = await fetch('http://192.168.100.16:5000/api/repositories');
const json = await response.json();
console.log(json);
setRepositories(json);
};
useEffect(() => {
fetchRepositories();
}, []);
// Get the nodes from the edges array
const repositoryNodes = repositories
? repositories.edges.map(edge => edge.node)
: [];
return (
<FlatList
data={repositoryNodes}
// Other props
/>
);
};
export default RepositoryList;
Estamos usando el hook useState de React para mantener el estado de la lista de repositorios y el hook useEffect para llamar a la función fetchRepositories cuando el componente RepositoryList está montado. Extraemos los repositorios reales en la variable repositoryNodes y reemplazamos la variable repositories utilizada anteriormente en la propiedad data del componente FlatList con eso. Ahora debería poder ver los datos reales proporcionados por el servidor en la lista de repositorios revisados.
Por lo general, es una buena idea registrar la respuesta del servidor para poder inspeccionarlo como hicimos en la función fetchRepositories. Debería poder ver este mensaje de registro en las herramientas de desarrollo de la Expo si navega a los registros de su dispositivo como aprendimos en la sección Visualización de registros. Si está utilizando la aplicación móvil de la Expo para el desarrollo y la solicitud de red falla, asegúrese de que la computadora que está usando para ejecutar el servidor y su teléfono estén conectados a la misma red Wi-Fi. Si eso no es posible, use un emulador en la misma computadora en la que se ejecuta el servidor o configure un túnel al localhost, por ejemplo, usando Ngrok.
El código actual de obtención de datos en el componente RepositoryList podría refactorizarse. Por ejemplo, el componente conoce los detalles de la solicitud de red, como la URL del punto final. Además, el código de recuperación de datos tiene un gran potencial de reutilización. Refactoricemos el código del componente extrayendo el código de obtención de datos en su propio gancho. Cree un directorio hooks en el directorio src y en ese directorio hooks cree un archivo useRepositories.js con el siguiente contenido:
import { useState, useEffect } from 'react';
const useRepositories = () => {
const [repositories, setRepositories] = useState();
const [loading, setLoading] = useState(false);
const fetchRepositories = async () => {
setLoading(true);
// Replace the IP address part with your own IP address!
const response = await fetch('http://192.168.100.16:5000/api/repositories');
const json = await response.json();
setLoading(false);
setRepositories(json);
};
useEffect(() => {
fetchRepositories();
}, []);
return { repositories, loading, refetch: fetchRepositories };
};
export default useRepositories;
Ahora que tenemos una abstracción limpia para buscar los repositorios revisados, usemos el hook useRepositories en el componente RepositoryList:
import React from 'react';
// ...
import useRepositories from '../hooks/useRepositories';
const RepositoryList = () => {
const { repositories } = useRepositories();
const repositoryNodes = repositories
? repositories.edges.map(edge => edge.node)
: [];
return (
<FlatList
data={repositoryNodes}
// Other props
/>
);
};
export default RepositoryList;
Eso es todo, ahora el componente RepositoryList ya no es consciente de la forma en que se adquieren los repositorios. Quizás en el futuro, los adquiriremos a través de una API GraphQL en lugar de una API REST. Veremos que sucede.
Cliente GraphQL y Apollo
En la parte 8 aprendimos sobre GraphQL y cómo enviar consultas GraphQL a un servidor Apollo utilizando el Cliente Apollo en aplicaciones React. La buena noticia es que podemos usar Apollo Client en una aplicación React Native exactamente como lo haríamos con una aplicación web React.
Como se mencionó anteriormente, el servidor rate-repository-api proporciona una API GraphQL que se implementa con Apollo Server. Una vez que el servidor se está ejecutando, puede acceder a GraphQL Playground en http://localhost:5000/graphql. GraphQL Playground es una herramienta de desarrollo para realizar consultas GraphQL e inspeccionar el esquema y la documentación de las API GraphQL. Si necesita enviar una consulta en su aplicación siempre pruébela con GraphQL Playground antes de implementarla en el código. Es mucho más fácil depurar posibles problemas en la consulta en GraphQL Playground que en la aplicación. Si no está seguro de cuáles son las consultas disponibles o cómo utilizarlas, haga clic en la pestaña docs para abrir la documentación:
En nuestra aplicación React Native, utilizaremos el Apollo Boost, que es una forma con configuración cero para comenzar a usar Apollo Client. Como integración de React, usaremos la librería @apollo/react-hooks, que proporciona enlaces como useQuery y useMutation para usar Apollo Client. Comencemos instalando las dependencias:
npm install apollo-boost @apollo/react-hooks graphql
A continuación, creemos una función de utilidad para crear el cliente Apollo con la configuración requerida. Cree un directorio utils en el directorio src y en ese directorio utils cree un archivo apolloClient.js. En ese archivo, configure el Apollo Client para que se conecte al Apollo Server:
import ApolloClient from 'apollo-boost';
const createApolloClient = () => {
return new ApolloClient({
// Replace the IP address part with your own IP address!
uri: 'http://192.168.100.16:5000/graphql',
});
};
export default createApolloClient;
Por lo demás, la URL que se usa para conectarse al servidor Apollo es la misma que usó con la API Fetch, pero la ruta es /graphql. Por último, debemos proporcionar el Cliente Apollo utilizando el contexto ApolloProvider. Lo agregaremos al componente App en el archivo App.js:
import React from 'react';
import { NativeRouter } from 'react-router-native';
import { ApolloProvider } from '@apollo/react-hooks';
import Main from './src/components/Main';
import createApolloClient from './src/utils/apolloClient';
const apolloClient = createApolloClient();
const App = () => {
return (
<NativeRouter>
<ApolloProvider client={apolloClient}> <Main />
</ApolloProvider> </NativeRouter>
);
};
export default App;
Organización del código relacionado con GraphQL
Depende de usted cómo organizar el código relacionado con GraphQL en su aplicación. Sin embargo, por el bien de una estructura de referencia, echemos un vistazo a una forma bastante simple y eficiente de organizar el código relacionado con GraphQL. En esta estructura, definimos consultas, mutaciones, fragmentos y posiblemente otras entidades en sus propios archivos. Estos archivos se encuentran en el mismo directorio. A continuación, se muestra un ejemplo de la estructura que puede usar para comenzar:
Puede importar el template literal gql utilizado para definir consultas GraphQL desde la libería Apollo Boost. Si seguimos la estructura sugerida anteriormente, podríamos tener un archivo queries.js en el directorio graphql para las consultas GraphQL de nuestra aplicación. Cada una de las consultas se puede almacenar en una variable y exportar así:
import { gql } from 'apollo-boost';
export const GET_REPOSITORIES = gql`
query {
repositories {
${/* ... */}
}
}
`;
// other queries...
Podemos importar estas variables y usarlas con el hook useQuery como este:
import { useQuery } from '@apollo/react-hooks';
import { GET_REPOSITORIES } from '../graphql/queries';
const Component = () => {
const { data, error, loading } = useQuery(GET_REPOSITORIES);
// ...
};
Lo mismo ocurre con la organización de mutaciones. La única diferencia es que los definimos en un archivo diferente, mutations.js. Se recomienda utilizar fragmentos en las consultas para evitar volver a escribir los mismos campos una y otra vez.
Evolución de la estructura
Una vez que nuestra aplicación crezca, puede haber ocasiones en las que ciertos archivos crezcan demasiado para administrarlos. Por ejemplo, tenemos el componente A que renderiza los componentes B y C. Todos estos componentes están definidos en un archivo A.jsx en un directorio components. Nos gustaría extraer los componentes B y C en sus propios archivos B.jsx y C.jsx sin mayores refactores. Tenemos dos opciones:
- Crear archivos B.jsx y C.jsx en el directorio components. Esto da como resultado la siguiente estructura:
components/
A.jsx
B.jsx
C.jsx
...
- Crear un directorio A en el directorio components y cree los archivos B.jsx y C.jsx allí. Para evitar romper los componentes que importan el archivo A.jsx, mueva el archivo A.jsx al directorio A y cámbiele el nombre a index.jsx. Esto da como resultado la siguiente estructura:
components/
A/
B.jsx
C.jsx
index.jsx
...
La primera opción es bastante decente, sin embargo, si los componentes B y C no se pueden reutilizar fuera del componente A, es inútil hinchar el directorio components agregándolos como archivos separados. La segunda opción es bastante modular y no interrumpe ninguna importación porque la importación de una ruta como ./A coincidirá con A.jsx y A/index.jsx.
Variables de entorno
Es muy probable que cada aplicación se ejecute en más de un entorno. Dos candidatos obvios para estos entornos son el entorno de desarrollo y el entorno de producción. De estos dos, el entorno de desarrollo es el que estamos ejecutando la aplicación en este momento. Los diferentes entornos generalmente tienen diferentes dependencias, por ejemplo, el servidor que estamos desarrollando localmente puede usar una base de datos local, mientras que el servidor que se implementa en el entorno de producción usa la base de datos de producción. Para hacer que el entorno del código sea independiente, necesitamos parametrizar estas dependencias. Por el momento, estamos usando un valor codificado muy dependiente del entorno en nuestra aplicación: la URL del servidor.
Hemos aprendido anteriormente que podemos proporcionar programas en ejecución con variables de entorno. Estas variables se pueden definir en la línea de comandos o utilizando archivos de configuración del entorno como archivos .env y bibliotecas de terceros como Dotenv. Desafortunadamente, React Native no tiene soporte directo para variables de entorno. Sin embargo, podemos acceder a la configuración de Expo definida en el archivo app.json en tiempo de ejecución desde nuestro código JavaScript. Esta configuración se puede utilizar para definir y acceder a variables dependientes del entorno.
Se puede acceder a la configuración importando la constante Constants desde el módulo expo-constants como lo hemos hecho algunas veces antes. Una vez importada, la propiedad Constants.manifest contendrá la configuración. Intentemos esto registrando Constants.manifest en el componente App:
import React from 'react';
import { NativeRouter } from 'react-router-native';
import { ApolloProvider } from '@apollo/react-hooks';
import Constants from 'expo-constants';
import Main from './src/components/Main';
import createApolloClient from './src/utils/apolloClient';
const apolloClient = createApolloClient();
const App = () => {
console.log(Constants.manifest);
return (
<NativeRouter>
<ApolloProvider client={apolloClient}>
<Main />
</ApolloProvider>
</NativeRouter>
);
};
export default App;
Ahora debería ver la configuración en los registros.
El siguiente paso es usar la configuración para definir variables dependientes del entorno en nuestra aplicación. Comencemos cambiando el nombre del archivo app.json a app.config.js. Una vez que se cambia el nombre del archivo, podemos usar JavaScript dentro del archivo de configuración. Cambie el contenido del archivo para que el objeto anterior:
{
"expo": {
"name": "rate-repository-app",
// rest of the configuration...
}
}
Se convierte en una exportación, que contiene el contenido de la propiedad expo:
export default {
name: 'rate-repository-app',
// rest of the configuration...
};
Expo ha reservado una propiedad extra en la configuración para cualquier configuración específica de la aplicación. Para ver cómo funciona esto, agreguemos una variable env en la configuración de nuestra aplicación:
export default {
name: 'rate-repository-app',
// rest of the configuration...
extra: { env: 'development' },};
Reinicie las herramientas de desarrollo de Expo para aplicar los cambios y debería ver que el valor de la propiedad Constants.manifest ha cambiado y ahora incluye la propiedad extra que contiene nuestra configuración específica de la aplicación. Ahora se puede acceder al valor de la variable env a través de la propiedad Constants.manifest.extra.env.
Debido a que usar una configuración codificada es un poco tonto, usemos una variable de entorno en su lugar:
export default {
name: 'rate-repository-app',
// rest of the configuration...
extra: { env: process.env.ENV, },};
Como hemos aprendido, podemos establecer el valor de una variable de entorno a través de la línea de comando definiendo el nombre y el valor de la variable antes del comando real. Como ejemplo, inicie las herramientas de desarrollo de Expo y establezca la variable de entorno ENV como test así:
ENV=test npm start
Si echa un vistazo en los registros, debería ver que la propiedad Constants.manifest.extra.env ha cambiado.
También podemos cargar variables de entorno desde un archivo .env como hemos aprendido en las partes anteriores. Primero, necesitamos instalar la biblioteca Dotenv:
npm install dotenv
A continuación, agregue un archivo .env en el directorio raíz de nuestro proyecto con el siguiente contenido:
ENV=development
Finalmente, importe la biblioteca en el archivo app.config.js:
import 'dotenv/config';
export default {
name: 'rate-repository-app',
// rest of the configuration...
extra: {
env: process.env.ENV,
},
};
Debe reiniciar las herramientas de desarrollo de Expo para aplicar los cambios que ha realizado en el archivo .env.
Tenga en cuenta que nunca es una buena idea poner datos confidenciales en la configuración de la aplicación. La razón de esto es que una vez que un usuario ha descargado su aplicación, puede, al menos en teoría, aplicar ingeniería inversa a su aplicación y averiguar los datos confidenciales que ha almacenado en el código.
Almacenamiento de datos en el dispositivo del usuario
Hay ocasiones en las que necesitamos almacenar algunos datos persistentes en el dispositivo del usuario. Uno de esos escenarios comunes es almacenar el token de autenticación del usuario para que podamos recuperarlo incluso si el usuario cierra y vuelve a abrir nuestra aplicación. En el desarrollo web, hemos utilizado el objeto localStorage del navegador para lograr dicha funcionalidad. React Native proporciona un almacenamiento persistente similar, el AsyncStorage.
Podemos usar el comando expo install para instalar la versión del paquete @react-native-community/async-storage que es adecuada para nuestra versión de Expo SDK:
expo install @react-native-community/async-storage
La API de AsyncStorage es en muchos aspectos la misma que la API de localStorage. Ambos son almacenamientos de clave-valor con métodos similares. La mayor diferencia entre los dos es que, como su nombre lo indica, las operaciones de AsyncStorage son asincrónicas.
Debido a que AsyncStorage opera con claves de cadena en un espacio de nombres global, es una buena idea crear una abstracción simple para sus operaciones. Esta abstracción se puede implementar, por ejemplo, usando una clase. Como ejemplo, podríamos implementar un almacenamiento de carrito de compras para almacenar los productos que el usuario desea comprar:
import AsyncStorage from '@react-native-community/async-storage';
class ShoppingCartStorage {
constructor(namespace = 'shoppingCart') {
this.namespace = namespace;
}
async getProducts() {
const rawProducts = await AsyncStorage.getItem(
`${this.namespace}:products`,
);
return rawProducts ? JSON.parse(rawProducts) : [];
}
async addProduct(productId) {
const currentProducts = await this.getProducts();
const newProducts = [...currentProducts, productId];
await AsyncStorage.setItem(
`${this.namespace}:products`,
JSON.stringify(newProducts),
);
}
async clearProducts() {
await AsyncStorage.removeItem(`${this.namespace}:products`);
}
}
const doShopping = async () => {
const shoppingCartA = new ShoppingCartStorage('shoppingCartA');
const shoppingCartB = new ShoppingCartStorage('shoppingCartB');
await shoppingCartA.addProduct('chips');
await shoppingCartA.addProduct('soda');
await shoppingCartB.addProduct('milk');
const productsA = await shoppingCartA.getProducts();
const productsB = await shoppingCartB.getProducts();
console.log(productsA, productsB);
await shoppingCartA.clearProducts();
await shoppingCartB.clearProducts();
};
doShopping();
Debido a que las claves AsyncStorage son globales, generalmente es una buena idea agregar un espacio de nombres para las claves. En este contexto, el espacio de nombres es solo un prefijo que proporcionamos para las claves de abstracción de almacenamiento. El uso del espacio de nombres evita que las claves del almacenamiento colisionen con otras claves AsyncStorage. En este ejemplo, el espacio de nombres se define como el argumento del constructor y estamos usando el formato namespace:key para las claves.
Podemos agregar un elemento al almacenamiento usando el método AsyncStorage.setItem. El primer argumento del método es la clave del elemento y el segundo argumento su valor. El valor debe ser una cadena, por lo que necesitamos serializar valores que no son cadenas como hicimos con el método JSON.stringify. El método AsyncStorage.getItem se puede utilizar para obtener un elemento del almacenamiento. El argumento del método es la clave del elemento, cuyo valor se resolverá. El método AsyncStorage.removeItem se puede utilizar para eliminar el elemento con la clave proporcionada del almacenamiento.
Mejora de las solicitudes del cliente Apollo
Ahora que hemos implementado el almacenamiento para almacenar el token de acceso del usuario, es hora de comenzar a usarlo. Inicialice el almacenamiento en el componente App:
import React from 'react';
import { NativeRouter } from 'react-router-native';
import { ApolloProvider } from '@apollo/react-hooks';
import Main from './src/components/Main';
import createApolloClient from './src/utils/apolloClient';
import AuthStorage from './src/utils/authStorage';
const authStorage = new AuthStorage();const apolloClient = createApolloClient(authStorage);
const App = () => {
return (
<NativeRouter>
<ApolloProvider client={apolloClient}>
<Main />
</ApolloProvider>
</NativeRouter>
);
};
export default App;
También proporcionamos la instancia de almacenamiento para la función createApolloClient como argumento. Esto se debe a que a continuación, enviaremos el token de acceso a Apollo Server en cada solicitud. Apollo Server esperará que el token de acceso esté presente en el encabezado Authorization en el formato Bearer <ACCESS_TOKEN>. Podemos mejorar el funcionamiento del cliente Apollo utilizando la opción request. Enviemos el token de acceso al servidor Apollo en nuestro cliente Apollo modificando la función createApolloClient en el archivo apolloClient.js:
const createApolloClient = (authStorage) => { return new ApolloClient({
request: async (operation) => { try { const accessToken = await authStorage.getAccessToken(); operation.setContext({ headers: { authorization: accessToken ? `Bearer ${accessToken}` : '', }, }); } catch (e) { console.log(e); } }, // uri and other options...
});
};
Usando React Context para inyección de dependencia
La última pieza del rompecabezas de inicio de sesión es integrar el almacenamiento en el gancho useSignIn. Para lograr esto, el gancho debe poder acceder a la instancia de almacenamiento de tokens que hemos inicializado en el componente App. React Context es solo la herramienta que necesitamos para el trabajo. Cree un directorio context en el directorio src. En ese directorio cree un archivo AuthStorageContext.js con el siguiente contenido:
import React from 'react';
const AuthStorageContext = React.createContext();
export default AuthStorageContext;
Ahora podemos usar el AuthStorageContext.Provider para proporcionar la instancia de almacenamiento a los descendientes del contexto. Agreguémoslo al componente App:
import React from 'react';
import { NativeRouter } from 'react-router-native';
import { ApolloProvider } from '@apollo/react-hooks';
import Main from './src/components/Main';
import createApolloClient from './src/utils/apolloClient';
import AuthStorage from './src/utils/authStorage';
import AuthStorageContext from './src/contexts/AuthStorageContext';
const authStorage = new AuthStorage();
const apolloClient = createApolloClient(authStorage);
const App = () => {
return (
<NativeRouter>
<ApolloProvider client={apolloClient}>
<AuthStorageContext.Provider value={authStorage}> <Main />
</AuthStorageContext.Provider> </ApolloProvider>
</NativeRouter>
);
};
export default App;
Ahora es posible acceder a la instancia de almacenamiento en el hook useSignIn usando el hook useContext de React como este:
import { useContext } from 'react';// ...
import AuthStorageContext from '../contexts/AuthStorageContext';
const useSignIn = () => {
const authStorage = useContext(AuthStorageContext); // ...
};
Tenga en cuenta que acceder al valor de un contexto mediante el hook useContext solo funciona si el hook useContext se usa en un componente que es descendiente del componente Context.Provider.
La capacidad de proporcionar datos a los descendientes de componentes abre toneladas de casos de uso para React Context. Para obtener más información sobre estos casos de uso, lea el esclarecedor artículo de Kent C. Dodds Cómo usar React Context de manera efectiva para descubrir cómo combinar el hook useReducer con el contexto para implementar la gestión del estado. Puede encontrar una forma de utilizar este conocimiento en los próximos ejercicios.