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

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

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:

  1. Instala Create React App globalmente (si aún no lo has hecho) ejecutando el siguiente comando:

    npm install -g create-react-app
  2. Crea una nueva aplicación React utilizando la plantilla TypeScript:

    npx create-react-app prueba-zara --template typescript
  3. Navega a la carpeta del proyecto:

    cd prueba-zara
  4. 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

ficheros y carpetas

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:

Si ejecutamos npm run test se pinta los test en jest. El único test que existe es App.test.tsx.

Primer test

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).

  1. 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
  1. 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.

  1. 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:

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:

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

  1. Crea la carpeta src/hooks

  2. Crea el fichero src/hooks/useFetch.test.ts

  3. 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-library

    npm install --save-dev jest-fetch-mock
  4. 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();
  5. 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és false y que los datos son devueltos.

    Si ejecutas npm run test, nos devolverá un error ya que el fichero useFetch.ts aún no tiene el código que necesitamos.

  6. 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.

  7. 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).

  8. 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;

Creación de Vista Principal