Miguel Angel Reyes Moreno
César Palma
Cayo Legal
Moises Hernandez
Mariangelica Useche
Team PlatziRubén Ernesto Aragón Gil
Juan Camilo Lentino Villalba
Alex Guaman
Agustín Nicolás Stringa
Kevin Figuera
Andrés Felipe Eslava Zuluaga
Max Andy Diaz Neyra
David Rangel
Jose Alexis Nuñez Torres
Company_adminHarold Zurita Simon
Sebastian Carballo
DANIEL STEVEN RODRIGUEZ VERANO
Julian Andres Cruz Gonzalez
Ivan Camilo Buitrago Buitrago
Juan Carlos Montilla Sánchez
La cantidad de código que hay que escribir se ha incrementado mucho para algo sencillo. Pero seguro con la práctica y la repetición será más fácil de recordar los pasos a seguir.
con el tiempo aprenderemos alternativas mas afines a ciertas necesidades
Yo por ejemplo cree un estado de loading booleano, valor inicial true. Si el valor de loading es True devolves el Spin. Una vez que fetchPokemons termina su trabajo pasas loading a false
Muy bueno el curso, pero no me gusto ant design, yo sigo usando tailwind pero como se menciono al inicio solo es para maquetacion rápida.
Tailwind es genial <3
Tailwind me gusta, pero me asusta 😶
también podemos colocar el dispatch de la acción que controla el loading directamente en la acción getPokemonWithDetails
export const getPokemonsWithDetails = (pokemons = [])=> async (dispatch)=> { dispatch(setLoading(true)) const pokemonsDetailed = await Promise.all(pokemons.map(pokemon => getPokemonDetail(pokemon))) dispatch(setPokemons(pokemonsDetailed)) dispatch(setLoading(false)) }
de esta forma, la misma acción controla la petición a la API y la carga del spinner, que siempre van de la mano uno con el otro, y nuestro useEffect del componente App quedaría mucho mas limpio
const App = ()=> { ... useEffect(()=> { const fetchPokemons = async ()=> { const pokemonsRes = await getPokemon() dispatch(getPokemonsWithDetails(pokemonsRes)) } fetchPokemons() }, []) }
También me parece que es mejor porque se controla que se setee primero los pokemons antes de setear el login, en el caso del video por el asincronismo se pierde muy rápido el loader.
Estaba pensando en hacerlo pero no sabía qué tan legal era 🤣 Es cierto, de otro modo se pierde muy rapido el loader.
Mi practica
App.jsx
import {Col, Spin} from 'antd'; import Searcher from "./components/Searcher"; import {useEffect} from 'react' import PokemonList from "./components/PokemonList.jsx"; import {getPokemon} from "./api"; import {getPokemonWithDetails, setLoadings} from "./actions/index.js"; import Logo from "./assets/logo.svg"; import './App.css' import {useDispatch, useSelector} from "react-redux"; function App() { const pokemons = useSelector(state => state.pokemons); const loading = useSelector(state => state.loading); const dispatch = useDispatch(); useEffect(() => { const fetchPokemons = async () => { dispatch(setLoadings(true)) const response = await getPokemon(); dispatch(getPokemonWithDetails(response)); dispatch(setLoadings(false)) } fetchPokemons(); }, []); return ( <div className="App"> <Col span={4} offset={10}> <img src={Logo} alt="pokeapi"/> </Col> <Col span={8} offset={8}> <Searcher/> </Col> {loading ? ( <Col offset={12} className="Spinner"> <Spin spinning size="large"/> </Col> ) : ( <PokemonList pokemons={pokemons}/> )} </div> ) } export default App;
App.css
@import "antd/dist/antd.css"; .App{ margin-top: 3rem; } .App img { margin-bottom: 3rem; } .App img { margin-bottom: 3rem; } .Spinner { margin-top: 3rem; }
reducers/pokemons.js
import {SET_LOADING,SET_POKEMON} from "../actions/types.js"; const initialState = {pokemons: [], loading: false}; export const pokemonsReducer = (state,action) => { switch (action.type) { caseSET_POKEMON: return {...state, pokemons:action.payload} break; caseSET_LOADING: return {...state, loading:action.payload} break; default: return {...state}; break; } }
actions/types.js
export constSET_POKEMON= 'SET_POKEMON'; export constSET_LOADING= 'SET_LOADING';
actions/index.js
import {SET_POKEMON,SET_LOADING} from "./types"; import {getPokemonDetails} from "../api"; export const setPokemons = (payload) => ({ type:SET_POKEMON, payload }); export const setLoadings = (payload) => ({ type:SET_LOADING, payload, }); export const getPokemonWithDetails = (pokemons= []) => async (dispatch) => { const pokemonsDetailed = awaitPromise.all(pokemons.map(pokemon=>getPokemonDetails(pokemon))); dispatch(setPokemons(pokemonsDetailed)); };
Aporte en TypeScript de la clase
.
// src/actions/types.ts export const SET_POKEMONS_DETAILS = "SET_POKEMONS_DETAILS"; export const SET_LOADING = "SET_LOADING"; // src/actions/index.ts import { IPokemonDetails, IPokemonType } from "../types"; import { Dispatch } from "redux"; import { SET_LOADING, SET_POKEMONS_DETAILS } from "./types"; import { getPokemonDetails } from "../api"; export const setPokemonsDetails = ( payload: (IPokemonDetails | undefined)[] ) => ({ type: SET_POKEMONS_DETAILS, payload, }); export const setLoading = (payload: boolean) => ({ type: SET_LOADING, payload, }) export const getPokemonsDetailsAction = (pokemons: IPokemonType[]) => // Dispatch se debe tipar para que no ocurran inconvenientes con lo que devuelve async (dispatch: Dispatch<any>) => { const pokemonDetails = await Promise.all( pokemons.map((pokemon) => pokemon && getPokemonDetails(pokemon)) ); dispatch(setPokemonsDetails(pokemonDetails)); };
.
// src/reducers/pokemons.ts import { SET_LOADING, SET_POKEMONS_DETAILS } from "../actions/types"; import { InitialPokemonsStateType } from "../types"; const initialState: InitialPokemonsStateType = { pokemons: [], loading: false, }; export const pokemonsReducer = ( state: InitialPokemonsStateType = initialState, action: any ) => { switch (action.type) { case SET_POKEMONS_DETAILS: return { ...state, pokemons: action.payload, }; case SET_LOADING: return { ...state, loading: action.payload, }; default: return state; } };
.
// src/App.tsx import React, { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Col, Spin } from "antd"; import Searcher from "./components/Searcher"; import PokemonList from "./components/PokemonList"; import { getPokemon } from "./api"; import { getPokemonsDetailsAction, setLoading } from "./actions"; import { InitialPokemonsStateType, IPokemonDetails } from "./types"; import logo from "./statics/logo.svg"; import "./App.css"; import { Dispatch } from "redux"; type AppType = { pokemons: IPokemonDetails[]; }; function App() { const pokemons = useSelector((state: AppType) => state.pokemons); const loading = useSelector( (state: InitialPokemonsStateType) => state.loading ); // Dispatch se debe tipar para que no ocurran inconvenientes con lo que devuelve const dispatch = useDispatch<Dispatch<any>>(); useEffect(() => { const fetchPokemons = async () => { dispatch(setLoading(true)); const response = await getPokemon(); response && dispatch(getPokemonsDetailsAction(response)); dispatch(setLoading(false)); }; fetchPokemons(); }, [dispatch]); return ( <div className="App"> <Col span={4} offset={10}> <img src={logo} alt="Pokedux" /> </Col> <Col span={8} offset={8}> <Searcher /> </Col> {loading ? ( <Col span={8} offset={8} style={{ marginTop: "2rem" }}> <Spin spinning size="large" /> </Col> ) : ( <PokemonList pokemons={pokemons} /> )} </div> ); } export default App;
.
// src/types/index.ts export interface IPokemonDetails { abilities: []; name: string; sprites: SpritesType; } export interface IPokemonType { name: string; url: string; } export type InitialPokemonsStateType = { pokemons: IPokemonType[]; loading: boolean; }; export type SpritesType = { front_default: string; };
Dejo mi código TS de la clase:
// App.tsx import { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { Search } from './components/Search' import { PokemonList } from './components/PokemonList' import { Col, Spin } from 'antd' import { getPokemons } from './api' import { getPokemonsWithDetails, setLoading, setPokemons } from './actions' import { TypeState } from './reducers/pokemon' import logo from './statics/logo.svg' import './App.css' function App() { const pokemons = useSelector((state:TypeState)=>state.pokemons) const loading = useSelector((state:TypeState)=>state.loading) const dispath = useDispatch(); useEffect(()=>{ async function fetchPokemon(){ dispath(setLoading(true)) const pkmns = await getPokemons(); dispath(getPokemonsWithDetails(pkmns)) dispath(setLoading(false)) } fetchPokemon(); },[]) return ( <div className="App"> <Col span={4} offset={10}> <img src={logo} alt="Pokedux" /> </Col> <Col span={8} offset={8}> <Search /> </Col> {loading? <Col offset={12} > <Spin spinning size='large' /> </Col> : <PokemonList pokemons={pokemons} /> } </div> ) } export default App;
// action/type.ts export const SET_POKEMONS = 'set_pokemons' export const SET_LOADING = 'set_loading'
// action/index.ts import { getPokemonDetails, PokemonDetailType, PokemonType } from "../api"; import { SET_LOADING, SET_POKEMONS } from "./types"; export const setPokemons = (payload: PokemonDetailType[])=>({ type: SET_POKEMONS, payload }) export const setLoading = (payload:boolean)=>({ type: SET_LOADING, payload }) export const getPokemonsWithDetails = (pokemons:PokemonType[]=[]) => async (dispatch: any) =>{ const pkmnsDetailed = await Promise.all((pokemons as PokemonType[]).map(pkmn=>getPokemonDetails(pkmn))) dispatch(setPokemons(pkmnsDetailed)) }
import { SET_LOADING, SET_POKEMONS } from "../actions/types" import { PokemonDetailType } from "../api" export type TypeState = { pokemons: PokemonDetailType[], loading : boolean } const initialState: TypeState = { pokemons: [], loading: false } export const pokemonReducer = (state:TypeState = initialState, action:any)=>{ switch(action.type){ case SET_POKEMONS: return {...state, pokemons: action.payload} case SET_LOADING: return {...state, loading: action.payload} default: return state } }
Les comparto como sería con TypeScript: app/types/reducerTypes.ts
// app/types/reducerTypes.ts import type { SET_LOADING, SET_POKEMONS } from './actionTypes'; import type { PokeDetail } from './pokeTypes'; export interface PokemonState { pokemons: PokeDetail[]; loading: boolean; } export interface SetPokemonAction { type: typeof SET_POKEMONS; payload: PokeDetail[]; } export interface SetLoadingAction { type: typeof SET_LOADING; payload: boolean; } export type PokemonAction = SetPokemonAction | SetLoadingAction; export interface PokemonState { pokemons: PokeDetail[]; loading: boolean; } // app/reducers/pokemonsReducer.ts import { SET_LOADING, SET_POKEMONS } from '~/types/actionTypes'; import type { PokemonAction, PokemonState } from '~/types/reducerTypes'; const initialState: PokemonState = { pokemons: [], loading: false, }; export const pokemonsreducer = ( state: PokemonState = initialState, action: PokemonAction, ) => { switch (action.type) { case SET_POKEMONS: return { ...state, pokemons: action.payload }; case SET_LOADING: return { ...state, loading: action.payload }; default: return state; } }; // app/actions/index.ts import type { Dispatch } from '@reduxjs/toolkit'; import { SET_LOADING, SET_POKEMONS } from '~/types/actionTypes'; import type { PokeDetail, PokemonDataType } from '~/types/pokeTypes'; import { getPokemonDetails } from '~/api/getPokemonDetails'; export const setPokemons = (payload: PokeDetail[]) => ({ type: SET_POKEMONS, payload, }); export const setLoading = (payload: boolean) => ({ type: SET_LOADING, payload, }); export const getPokemonsWithDetails = (pokemons: PokemonDataType[] = []) => async (dispatch: Dispatch) => { if (pokemons.length > 0) { const pokeDetailsRaw = await Promise.all( pokemons.map((poke: PokemonDataType) => getPokemonDetails(poke)), ); const pokeDetails = pokeDetailsRaw.filter( (poke): poke is PokeDetail => poke !== undefined, ); dispatch(setPokemons(pokeDetails)); } }; // app/welcome/welcome.tsx import { useEffect } from 'react'; import { Col, Spin } from 'antd'; import { useDispatch, useSelector } from 'react-redux'; import logo from '../statics/logo.svg'; import { PokeList } from '~/components/PokeList'; import { Searcher } from '~/components/Searcher'; import { getPokemon } from '~/api/getPokemon'; import { getPokemonDetails } from '~/api/getPokemonDetails'; import { type RootState, type AppDispatch } from '~/store/store'; import { getPokemonsWithDetails, setLoading } from '~/actions'; export function Welcome() { const pokemons = useSelector( (state: RootState) => state.pokemonData.pokemons, ); const loading = useSelector((state: RootState) => state.pokemonData.loading); const dispatch = useDispatch<AppDispatch>(); useEffect(() => { const fetchPokemons = async () => { dispatch(setLoading(true)); const pokeRes = await getPokemon(); if (pokeRes) { dispatch(getPokemonsWithDetails(pokeRes)); } dispatch(setLoading(false)); }; fetchPokemons(); }, [getPokemon, getPokemonsWithDetails, getPokemonDetails]); return ( <main> <h1>Pokedux</h1> <Col span={4} offset={10} className='my-3'> <img src={logo} alt='Pokedux' /> </Col> <Col span={8} offset={8} className='mt-3'> <Searcher /> </Col> {loading ? ( <Col offset={12}> <Spin spinning size='large' className='mt-3' /> </Col> ) : ( pokemons && <PokeList pokemons={pokemons} /> )} </main> ); }
import type {JSX} from "react"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faSpinner} from "@fortawesome/free-solid-svg-icons"; type Props = { message: string } const Loader: (props: Props) => JSX.Element = ({message}: Props): JSX.Element => { return ( <div className='w-full h-screen flex flex-col items-center justify-center'> <p className='animate-pulse uppercase tracking-wider text-rose-800 font-bold text-md'>{message}</p> <FontAwesomeIcon icon={faSpinner} className='animate-spin text-4xl text-rose-800'/> </div> ); }; export {Loader};
import {toast, Toaster} from "react-hot-toast"; import {type JSX, useEffect} from "react"; import {InputSearch} from "./components/input-search"; import {Loader} from "./components/loader"; import {PokemonList} from "./components/pokemon-list"; import {Tittle} from "./components/tittle"; import {createPortal} from "react-dom"; import {pokemonThunks} from "./actions/pokemon/pokemon.thunk.ts"; import {useAppDispatch, useAppSelector} from "./store.tsx"; const Page: () => JSX.Element = (): JSX.Element => { const dispatch = useAppDispatch(); const {result, status, error} = useAppSelector((state) => state.pokemon); useEffect((): void => { toast.promise(dispatch(pokemonThunks.all(4)), { success: 'Pokemon capturados!', error, loading: 'Buscando...', }) }, [dispatch]) if (status === 'loading') return (createPortal(<Loader message='Conectando con profesor hook...'/>, document.body)) return ( <section className='w-full p-2'> <Tittle title='pokedex'/> <InputSearch/> {status === 'failed' && <p>Error critico xd</p>} {(status === 'succeeded' && result.length > 0) && <PokemonList className='flex flex-col gap-y-4 mt-5' pokemon={result}/>} <Toaster position='top-right'/> </section> ); }; export default Page;
📌 Implementación de un Loading Spinner en la Aplicación
Para mostrar un spinner de carga mientras obtenemos los pokemones, implementamos un estado global loading en Redux y lo integramos con un custom hook.
🧩 1. Agregar el estado loading al reducer
Creamos un nuevo estado loading dentro del initialState, y añadimos un nuevo case para manejarlo.
import { SET_LOADING, SET_POKEMONS } from "../actions/types"; const initialState = { pokemons: [], loading: false, }; export const pokemonsReducer = (state = initialState, action) => { switch (action.type) { case SET_POKEMONS: return { ...state, pokemons: action.payload }; case SET_LOADING: return { ...state, loading: action.payload }; default: return state; } };
👉 Con esto, nuestra aplicación ya puede almacenar si está “cargando” o no.
🏷️ 2. Crear el Action Type y la Action setLoading
Definimos un nuevo tipo de acción:
export const SET_LOADING = "SET_LOADING";
Luego su action correspondiente:
import { SET_LOADING, SET_POKEMONS } from "./types"; import { getPokemonDetails } from "../api"; export const setPokemons = (payload) => ({ type: SET_POKEMONS, payload, }); export const setLoading = (payload) => ({ type: SET_LOADING, payload, });
Y actualizamos la función que obtiene los detalles:
export const getPokemonsWithDetails = (pokemons = []) => async (dispatch) => { const pokemonsDetailed = await Promise.all( pokemons.map((pokemon) => getPokemonDetails(pokemon)) ); dispatch(setPokemons(pokemonsDetailed)); };
🧠 3. Mover la lógica de carga y fetch a un Custom Hook (usePokemons)
Para mantener la UI limpia y separar la lógica del estado global, utilizamos un custom hook que:
pokemons y loading desde Reduxpokemons, loading y fetchPokemonsimport { useDispatch, useSelector } from 'react-redux'; import { getPokemonsWithDetails, setLoading } from '../actions'; import { getPokemon } from '../api'; import { useEffect } from 'react'; export const usePokemons = () => { const pokemons = useSelector((state) => state.pokemons); const loading = useSelector((state) => state.loading); const dispatch = useDispatch(); const fetchPokemons = async () => { dispatch(setLoading(true)); const pokemonsRes = await getPokemon(); dispatch(getPokemonsWithDetails(pokemonsRes)); dispatch(setLoading(false)); }; useEffect(() => { fetchPokemons(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { pokemons, loading, fetchPokemons, }; };
✨ Ahora el componente principal queda muy limpio.
🎨 4. Mostrar el Spinner según el estado loading
En App.js, solo mostramos la lista de pokemones cuando loading es false; en caso contrario, mostramos el loader.
import { Col, Spin } from "antd"; import Searcher from "./components/Searcher"; import PokemonList from "./components/PokemonList"; import logo from "./statics/logo.svg"; import "./App.css"; import { usePokemons } from "./hooks/usePokemons"; function App() { const { pokemons, loading } = usePokemons(); return ( <div className="App"> <Col span={4} offset={10}> <img src={logo} alt="Pokedux" /> </Col> <Col span={8} offset={8}> <Searcher /> </Col> {loading ? ( <Col offset={12}> <Spin spinning size="large" /> </Col> ) : ( <PokemonList pokemons={pokemons} /> )} </div> ); } export default App;
My english version branch:
Branch name:
feat/loader-uxui
Commit message:
"feat: Implement loader and state management for UXUI
This commit introduces a loader component and updates state management to improve the user experience. The loader will be displayed while data is being fetched, providing visual feedback to the user and enhancing the overall UXUI."
para los que este utilizando tailwindCss, tambien existe un ant Desinger para este
El loading de esa forma no es tan preciso, yo al momento de hacer el set_pokemons en el reducer cambie el estado del loader a false con eso es más preciso.
para una mejor experiencia de usuario considero que seria mejor un loader squeleton
Yo utilicé un loader de una pokeball para darle mejor interactividad