101 - Introducción a react
Con motivo de una prueba para Zara se dió la oportunidad de crear un curso introductorio a react. Los requirimientos se pueden descargar aquí.
Requisitos
- Node
- Visual Code
- Navegador tipo chrome
- Git instalado
- Conexión a internet
Creación de un entorno de desarrollo con VSCode para react
Como IDE opensource y gratuito de desarrollo, se decide utilizar Visual Code dentro un S.O. linux Ubuntu 22.04. La instalación del IDE está fuera del ámbit o de este tutorial, pero aún así se referencian los plugins utilizados que nos van a ayudar a desarrollar en react.
Para mantener una instalación limpia de Visual Code para desarrollar con React, se crea un perfil específico para ello donde guardar todas las extensiones y plugins instalados.
Extensiones y plugins
- VS Code ES7+ React/Redux/React-Native/JS snippets:
ext install dsznajder.es7-react-js-snippets
- Prettier - Code formatter:
ext install esbenp.prettier-vscode
- Import Cost
- Auto Rename Tag
- Git Extension Pack
- Tailwind CSS IntelliSense
Además, configuramos prettier como formatter por defecto y también configuramos que el IDE formatee el código al guardar.
Starter
Crear una aplicación React con TypeScript, Prettier y ESLint desde cero puede ser un proceso tedioso. Afortunadamente, existen herramientas y starters que pueden facilitar este proceso. Uno de los mejores y más populares es “Create React App” con una plantilla TypeScript.
A continuación, se detallan los pasos para configurar un proyecto con Create React App, TypeScript, Prettier y ESLint:
-
Instala Create React App globalmente (si aún no lo has hecho) ejecutando el siguiente comando:
npm install -g create-react-app
-
Crea una nueva aplicación React utilizando la plantilla TypeScript:
npx create-react-app prueba-zara --template typescript
-
Navega a la carpeta del proyecto:
cd prueba-zara
-
Abre el Visual Code con el perfil que has definido… en mi caso sería
code-react .
Ahora si exploramos los ficheros y las carpetas creadas
Lo primero que tenemos que tener en cuenta es el fichero package.json
que indica la estructura de cualquier proyecto node. En él vemos definidos los siguientes scripts:
start
: lanza el proyectobuild
: construye el proyectotest
: ejecuta los testseject
: Quita las dependencias del starter y crea los ficheros necesarios para que hagas todas las manipulaciones manualmente.
Si ejecutamos npm run test
se pinta los test en jest
. El único test que existe es App.test.tsx
.
Ya tenemos un proyecto listo para programar!!!
Configuración de prettier y eslint
Además de como nos viene el código vamos a hacer unos ajustes en la configuración de eslint (cómo se compila y reglas de compilación) y prettier (cómo se formatea).
- Instalamos las dependencias del proyecto en la sección de desarrollo para tenerlas disponibles dentro de
node_modules
:
npm install --save-dev eslint-config-prettier eslint-plugin-prettier prettier
- Crea un archivo
.eslintrc.json
en la raíz del proyecto y agrega la siguiente configuración:
{
"extends": ["react-app", "react-app/jest", "plugin:prettier/recommended"],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}
Esto configurará ESLint para que funcione con Prettier y emitirá errores si el código no sigue el estilo definido por Prettier.
- Crea un archivo .prettierrc en la raíz del proyecto y agrega la configuración de Prettier que prefieras. Por ejemplo:
{
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"semi": true
}
Estos pasos junto con la instalación de plugins que hemos hecho antes, nos proveerá de un proyecto React configurado con TypeScript, Prettier y ESLint. Siempre que guardes tus archivos, se formatearán automáticamente según las reglas de Prettier, y ESLint te ayudará a identificar y solucionar problemas de estilo y errores en tu código.
Guardar los pasos con git
Finalmente, para guardar los primeros pasos, creamos un nuevo commit en nuestro repositorio git local:
git st
git add .
git commit -m "feat: setup proyect. Add prettier and eslint"
Lectura de los requisitos y primeros pasos
Haciendo una lectura y resumen de requisitos, podemos extraer la siguiente lista:
- Una aplicación para escuchar podcasts
- Consistente en 3 vistas (Principal, Detalle podcast y detalle capítulo del podcast)
- SPA (aplicación web HTML)
- No responsive
- No PWA ni requirimientos offlines
Lo primero que atacaremos es la conexión a los podcasts. La página de requisitos indica que los leamos de https://itunes.apple.com/us/rss/toppodcasts/limit=100/genre/1310/json
.
Como el ámbito de esta prueba es pequeño, y la única necesidad de peticiones AJAX es para recuperar una lista de podcast, vamos a hacer uso de la API nativa de JS fetch
. Las carácterísticas principales se pueden resumir en:
- Fetch es una API nativa de JavaScript que se incluye en los navegadores modernos, por lo que no es necesario instalar ninguna biblioteca adicional.
- Devuelve promesas y es compatible con el uso de async/await.
- El manejo de errores puede ser poco intuitivo, ya que no rechaza automáticamente las promesas en caso de errores HTTP (por ejemplo, estado 404). Es necesario verificar manualmente si response.ok es true o false.
- No tiene soporte nativo para cancelar solicitudes, aunque se puede lograr mediante la API
AbortController
.
Atacar a la API
Como además queremos hacer una aproximación por TDD al desarrollo de la prueba, lo primero que tendremos que crear es nuestro test. Para ello
-
Crea la carpeta
src/hooks
-
Crea el fichero
src/hooks/useFetch.test.ts
-
Como queremos ahorramos tener que escribir un mock para
fetch
y tener que preocuparnos de las dificultades de testear un hook vamos a hacer uso de las librerías jest-mock-fetch y react-hooks-testing-librarynpm install --save-dev jest-fetch-mock
-
Actualizar el fichero
setupTests.ts
para incluir jest-mock-fetch.// jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; import fetchMock from 'jest-fetch-mock'; fetchMock.enableMocks();
-
Rellena el fichero
src/hooks/useFetch.test.ts
con el siguiente código:import { renderHook, act, waitFor } from '@testing-library/react'; import { useFetch } from './useFetch'; describe('useFetch', () => { beforeEach(() => { fetchMock.resetMocks(); }); it('fetches data and updates state correctly', async () => { const mockData = [ { id: 1, name: 'User 1' }, { id: 2, name: 'User 2' }, ]; fetchMock.mockResponseOnce(JSON.stringify(mockData)); const { result } = renderHook(() => useFetch<{ id: number; name: string }[]>('https://api.example.com/users'), ); expect(result.current.isLoading).toBeTruthy(); expect(result.current.error).toBeNull(); expect(result.current.data).toBeNull(); await act(() => !result.current.isLoading); expect(result.current.isLoading).toBeFalsy(); expect(result.current.error).toBeNull(); expect(result.current.data).toEqual(mockData); }); });
En este primer test, comprobamos que una vez que se hace la petición, el estado del loading pasa a
true
y despuésfalse
y que los datos son devueltos.Si ejecutas
npm run test
, nos devolverá un error ya que el ficherouseFetch.ts
aún no tiene el código que necesitamos. -
Modifica el fichero
useFetch.ts
:// useFetch.ts import { useState, useEffect } from 'react'; interface FetchResult<T> { data: T | null; isLoading: boolean; error: Error | null; } export function useFetch<T>(url: string): FetchResult<T> { const [data, setData] = useState<T | null>(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<Error | null>(null); useEffect(() => { const fetchData = async () => { setIsLoading(true); try { const response = await fetch(url); const data: T = await response.json(); setData(data); setError(null); } catch (error) { setError( error instanceof Error ? error : new Error('Error fetching data.'), ); } finally { setIsLoading(false); } }; fetchData(); }, [url]); return { data, isLoading, error }; }
Si ejecutas
npm run test
automáticamente se habrán rejecutado los test mostrandote un bonito verde con todos los test pasados. -
Pero de todas formas aún queda la gestión de errores por parte de nuestro hook. Modifica el fichero
useFetch.test.ts
probar esta posibilidad:// useFetch.test.ts ... it('handles fetch errors correctly', async () => { fetchMock.mockRejectOnce(new Error('Failed to fetch')); const { result } = renderHook(() => useFetch<{ id: number; name: string }[]>('https://api.example.com/users'), ); expect(result.current.isLoading).toBeTruthy(); expect(result.current.error).toBeNull(); expect(result.current.data).toBeNull(); await waitFor(() => !result.current.isLoading); expect(result.current.isLoading).toBeFalsy(); expect(result.current.error).not.toBeNull(); expect(result.current.error?.message).toEqual('Failed to fetch'); expect(result.current.data).toBeNull(); });
Ahora verás que el test ha vuelto a pasar a rojo, ya que nuestro
useFetch.ts
no tiene en cuenta si hay errores al hacer la petición (solo tenia en cuenta errores de no conexión). -
Actualiza el fichero
useFetch.ts
para pasar el test,// useFetch.ts import { useState, useEffect } from 'react'; interface FetchResult<T> { data: T | null; isLoading: boolean; error: Error | null; } export function useFetch<T>(url: string): FetchResult<T> { const [data, setData] = useState<T | null>(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<Error | null>(null); useEffect(() => { const fetchData = async () => { setIsLoading(true); try { const response = await fetch(url); if (!response.ok) { throw new Error(`Error fetching data: ${response.statusText}`); } const data: T = await response.json(); setData(data); setError(null); } catch (error) { setError( error instanceof Error ? error : new Error('Error fetching data.'), ); } finally { setIsLoading(false); } }; fetchData(); }, [url]); return { data, isLoading, error }; }
Creación del router y vista principal
Lo primero, tendremos que instalar las dependencias del router. Difieren en los enfoques y casos de usos que pretenden abordar.
npm install --save react-router-dom @types/react-router-dom
La otra librería conocida de router para react es React Navigation, mucho más utilizada en react native.
React Router DOM (https://reactrouter.com/web/guides/quick-start) es una biblioteca de enrutamiento para aplicaciones web desarrolladas con React. Está diseñada para facilitar la navegación entre componentes y la gestión de rutas en aplicaciones de una sola página (SPA). React Router DOM ofrece varias características y componentes, como Route, Link, NavLink, Switch y Redirect, que permiten configurar el enrutamiento y la navegación en aplicaciones web React.
React Navigation (https://reactnavigation.org/) es una biblioteca de enrutamiento y navegación para aplicaciones móviles construidas con React Native. Está diseñada para funcionar en aplicaciones iOS y Android y ofrece una experiencia de navegación nativa y fluida. React Navigation proporciona una serie de componentes y navegadores, como StackNavigator, TabNavigator y DrawerNavigator, que ayudan a configurar y personalizar la navegación entre las pantallas de la aplicación.
Con éstos bloques, vamos a definir un routing de nuestra app para que en función de la URL, se muestre un componente u otro.
Primero, creamos los ficheros src/views/Main.tsx
y src/views/Podcast.tsx
:
// Main.tsx
import React from "react";
const Main = () => {
return <div>Main</div>;
};
export default Main;
import React from "react";
const Podcast = () => {
return <div>Podcast</div>;
};
export default Podcast;
Luego modificamos el fichero App.tsx
para que tenga el siguiente aspecto:
// App.tsx
import React from "react";
import { RouterProvider } from "react-router-dom";
import "./App.css";
import { router } from "./router";
function App() {
return <RouterProvider router={router} />;
}
export default App;
y adaptamos el test para que busque la palabra Main:
// App.test.tsx
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/Main/i);
expect(linkElement).toBeInTheDocument();
});
Si ejecutamos npm start
vemos que se abre una pestaña del navegador en http://localhost:3000
. Es nuestra aplicación. Si cambiamos la ruta a http://localhost:3000/podcast/whatever
vemos que en lugar de Main aparece Podcast, se está renderizando otro componente.
Guardamos en git:
git st
git add .
git commit -m "feat: add react router, Main and Podcast components"
Trabajar en las vistas y componentes
Utilizar tailwind
Solo por el propósito de demostrar que el autor conoce tailwindCSS y sabe utlizarlo, se utilizará esa librería para estilar los componentes. Otras alternativas sería utilizar css plano (ya está integrado con el starter utilizado) o scss.
Primero tenemos que instalar las dependencias:
npm install tailwindcss@latest postcss@latest autoprefixer@latest
Después crea un archivo de configuración para Tailwind CSS:
npx tailwindcss init -p
A continuación modifica el fichero src/index.css
para importar tailwindCSS en el proyecto:
@tailwind base;
@tailwind components;
@tailwind utilities;