Los servicios en desarrollo de software son funciones esenciales que permiten que un sistema ejecute tareas específicas como hacer un request, enviar eventos a una máquina padre, o detectar entradas del teclado. Estas funciones son cruciales para desarrollar aplicaciones dinámicas, y su implementación en un proyecto puede llevarlo al siguiente nivel. Los servicios ayudan a establecer una comunicación fluida y eficiente entre diferentes componentes del sistema, optimizando el rendimiento y mejorando la experiencia del usuario.
¿Cuáles son los tipos de servicios más comunes?
Existen diversos tipos de servicios que cumplen diferentes funciones en un sistema. A continuación, se describen los más comunes:
Servicios de promesas: Utilizados para realizar solicitudes, como obtener un listado de países mediante un request.
Servicios de callbacks: Estos permiten una comunicación directa entre un componente padre y su hijo mediante callbacks.
Servicios observables: Envía un array de eventos como, por ejemplo, eventos de 'mouse down' o de teclado, manejando así los eventos de una manera semejante a un listener.
Servicio de invocar a otras máquinas: Permiten que una máquina padre invoque múltiples hijos, generando un puente para enviar eventos entre padres e hijos con send y send parent.
Estos servicios aportan flexibilidad y manejo adecuado de eventos, extendiendo las posibilidades de interacción.
¿Cómo implementar un servicio en un proyecto real?
Para ilustrar la implementación de un servicio, se puede considerar un ejemplo práctico donde se busca obtener un listado de países para un componente de búsqueda. Aquí se describe el procedimiento de implementación:
Crear una función para el request: Desarrolla dentro del proyecto una función que realice un fetch a una API, retornando la lista de países. Esta función se podrá guardar en un archivo llamado api.js dentro de una carpeta utils.
Invocar el servicio: En la máquina, dentro de la parte involved, se añade un invoke para llamar al servicio. Se debe definir un id (como get countries) y especificar el source, que es la función que se llamará.
Gestionar resultados y errores: Se utiliza on done para definir acciones al completar exitosamente la solicitud, como almacenar los países en el contexto. En caso de error, on error maneja la situación especificando el target y la acción a seguir.
Modificar el interfaz de usuario: Ajusta el componente de búsqueda para reflejar el estado actualizado, reemplazando listas estáticas con el array obtenido del request.
Implementar servicios de esta manera asegura que el sistema sea robusto, flexible y capaz de manejar pedidos externos de manera eficaz, lo que mejora la interacción del usuario con la aplicación.
¿Cómo manejar las transiciones con delay?
Las transiciones con delay son una técnica avanzada para crear cambios de estado en un sistema después de un determinado periodo. Para implementar estas transiciones, se sigue un procedimiento sencillo:
Definir el estado objetivo: En el sistema de estados, identifica el estado donde se ejecutará la transición con delay, como tickets.
Agregar el delay y la acción a ejecutar: Utiliza el atributo after para establecer un intervalo de tiempo, por ejemplo, 5000 milisegundos, antes de dirigirse a un target específico, ejecutando acciones definidas, como clean context.
Al configurar esta funcionalidad, después del tiempo especificado se efectuará la transición, limpiando el contexto o realizando cualquier otra acción necesaria. Este uso del delay anima al sistema a moverse automáticamente, mejorando la usabilidad y control del sistema.
La implementación de servicios y transiciones en los sistemas asegura que las aplicaciones trabajen de manera eficiente, con una comunicación efectiva entre componentes, manejando y respondiendo a solicitudes de manera ágil y eficaz. La práctica constante y el aprendizaje de estas técnicas son esenciales para el desarrollo de aplicaciones avanzadas y el dominio de herramientas modernas de programación. ¡Sigue explorando y potenciando tus habilidades en desarrollo de software!
Es una función que se invocan para realizar cierto procedimiento
send
Función para enviar eventos, para generar las transiciones
Apuntes
Dentro de los servicios existen diferentes categorías
Promesas ⇒ Se pueden utilizar para realizar request a API’s
Callbacks ⇒ Son servicios que se comunican entre el padre e hijo mediante un callback
Observables ⇒ Son servicios que mandan un array de eventos, dichos eventos se suelen tener un comportamiento similiar a un eventListener
Invocar otras máquinas ⇒ Es un tipo de servicio que hace posible que una máquina padre invoca múltiples máquinas hijas
Se crea un puente entre ambas máquinas, que permite que el padre mediante la función send envíe eventos a sus hijos y a la vez los hijos puedan enviar eventos a sus padres con la función sendParent
En la version 5 de XState el invoke quedaría se la siguiente forma:
⚠️ Importante (se debe importar fromPromises de xstate)
Entonces no puedes mapear algo undefined, entonces lo que debe de hacer para contrasrestar ese error es colocar el signo de pregunta "?" antes del map, de la siguiente forma, este signo hace que asegure que en este caso el array traiga valor antes de mapearlo.
En este caso para llamar a fetchCountries con xState v5 para invocar a fetchCountries he tenido que importar 'fromPromise' de xstate, según aparece en
el import quedaría
import{ createMachine, assign, fromPromise }from"xstate";```y la llamada en el src
```js
src:fromPromise(()=>fetchCountries()),```para asignar el listado en el onDone lo hice así:
```js
onDone:{target:'success',actions:assign({countries:({_,event})=> event.output})},
podrías subir mas información de tu código, lo hice como vos decís pero en el countries me queda async fetchCountries(). En vez del array con los países.
Tengan cuidado con las letras mayúsculas, yo tenia Invoke en lugar de invoke y me tomo una eternidad encontrar el error.
Dejo el código de la clase con Vite + TypeScript
api.ts
exportconstfetchCountries=async()=>{const response =awaitfetch('https://restcountries.com/v3.1/region/ame');const data =await response.json();return data;};
archivo bookingMachine.ts
export type BookingEvent=|{type:'START'}|{type:'CONTINUE'; selectedCountry: string }|{type:'ADD'; newPassenger: string }|{type:'DONE'}|{type:'FINISH'}|{type:'RETRY'}|{type:'CANCEL'};
Si estas siguiendo el curso con la version V5 de XState seguramente aqui no te funciono nada
bookingMachine.jsx
import{ createMachine, assign, fromPromise }from"xstate";import{ fetchCountries }from"../Utils/api";const fillCountries ={initial:"loading",states:{loading:{invoke:{id:'getCountries',// FIX: Envolvemos la función con fromPromisesrc:fromPromise(()=>fetchCountries()),onDone:{target:'success',actions:assign({// En V5, el resultado de una promesa está en event.outputcountries:({ event })=> event.output,})},onError:{target:'failure',actions:assign({// Capturamos el error real para mostrarloerror:({ event })=> event.error?.message ||'Error al cargar países',})}}},success:{},failure:{on:{RETRY:{target:"loading"},},},},};const bookingMachine =createMachine({id:"buy plane tickets",initial:"initial",context:{passengers:[],selectCountry:null,countries:[],error:'',},states:{initial:{entry:'CleanContexto',on:{START:{target:"search"},},},search:{on:{CONTINUE:{target:"passengers",actions:assign({selectCountry:({ _, event })=> event.selectCountry}),},CANCEL:"initial",},...fillCountries,},tickets:{on:{FINISH:"initial",},},passengers:{on:{DONE:"tickets",CANCEL:"initial",ADD:{target:"passengers",actions:assign({passengers:({ context, event })=>[...context.passengers, event.newPassenger]})}},},},},{actions:{CleanContexto:assign(()=>({passengers:[],selectedCountry:null,}))},});exportdefault bookingMachine;
Search.jsx
importReact,{ useState }from'react';import'./Search.css';exportconstSearch=({ state, send })=>{const[flight, setFlight]=useState('');// Extraemos los países y el error del contexto de la máquinaconst{ countries, error }= state.context;constgoToPassengers=()=>{send({type:'CONTINUE',selectCountry: flight
});}consthandleSelectChange=(event)=>{setFlight(event.target.value);};// 1. Estado de Carga: Mientras la API respondeif(state.matches('search.loading')){return<p className='Search-loading'>Cargando destinos...</p>;}// 2. Estado de Error: Si la petición fallóif(state.matches('search.failure')){return(<div className='Search-error'><p>{error ||'Hubo un error al obtener los países'}</p><button onClick={()=>send({type:'RETRY'})} className='button'>Reintentar</button></div>);}// 3. Estado de Éxito: Pintamos el select con datos de la APIreturn(<div className='Search'><p className='Search-title title'>Busca tu destino</p><select
id="country" className='Search-select' value={flight} onChange={handleSelectChange}><option value="" disabled>Escoge un país</option>{/* Usamos el nombre común de la API restcountries */}{countries.map((country)=>(<option
value={country.name.common} key={country.cca3|| country.name.common}>{country.name.common}</option>))}</select><button
onClick={goToPassengers} disabled={flight ===''} className='Search-continue button'>Continuar</button></div>);};
IMPORTANTE
Si estas siguiendo el curso con la version V5 de XState seguramente aqui no te funciono nada, tuve que reescribir varias cosas, consultar documentación
fillCountry machine se tiene que declarar con createMachine ya no se puede inyectar dentro de search directamente.
Cambiado el invoque de onDone, dentro de seach la forma tradicional todo el tiempo obtenia undefined tuve que usar childActor, y leer el snapshot
src:fromPromise(async({ input })=>{// Crear actor del child machineconst childActor =createActor(fillCountriesMachine);// Crear una promesa que se resuelve cuando el child terminareturnnewPromise((resolve, reject)=>{ childActor.subscribe({next:(snapshot)=>{// Si llega a un estado final, resolver con el contexto en lugar del outputif(snapshot.status==="done"){// Como output es undefined, usar el contexto directamenteconst result ={countries: snapshot.context.countries||[],error: snapshot.context.error||null,};resolve(result);}},error:(error)=>{console.error("Child error:", error);reject(error);},});// Iniciar el child actor childActor.start();});}),onDone:{actions:assign({countries:({ event })=>{if(event.output&& event.output.countries){return event.output.countries;}return[];},error:({ event })=>{if(event.output&& event.output.error){return event.output.error;}return"";},}),},
Resumiendo asi no la sufrien una version compatible seria la siguiente, dejo un gist en github del bookingMachine.js compatible con Xstate V5.
Los servicios son funciones que puedes invocar, por ejemplo, para hacer un request, enviar eventos a una máquina padre, invocar a otra máquina o leer eventos de teclado.
Hay muchas categorías:
Promesas
Callbacks
Observables
Invocar otras máquinas
En general, se llaman de la misma manera. En nuestra máquina podemos pasarle el atributo invocar y le decimos qué servicio va a invocar, y cuales son los posibles estados que luego va a tener ese servicio.
Los servicios de callbacks comunican al padre y el hijo. Los observables servicios que mandan como un array de eventos, como por ejemplo los de un mouse down, o un evento de teclado. El servicio de invocar otras máquinas hace que una máquina padre pueda invocar múltiples hijos; por medio una función send el padre puede enviar eventos a sus hijos, y viceversa, los hijos pueden mandar eventos a su padre con una función sendParent.
En nuestro ejemplo, vamos a hacer un servicio de promesas. Lo podemos usar para hacer un request. En nuestro caso vamos a obtener el listado de países para mostrarlos en la búsqueda.
Tenemos a nuestro fetchCountries en un archivo llamado api.js dentro de una carpeta Utils . Este hace fetch de una url que nos da un listado de países.
En la máquina, vamos a tener fillCountries , donde mediante un invoke invocamos un servicio. Ponemos un id y un source src , en nuestro caso el source corresponde a nuestro fetchCountries y lo importamos.
Agregamos las propiedades de countries y error al contexto.
Luego tenemos las propiedades onDone y onError que se ejecutan cuando el request finalice de manera exitosa o fallida. Si el request es exitoso nos movemos al estado de success y actualizamos la data de countries en el contexto. Mientras que si el request es fallido, entonces nos movemos al estado de failure y actualizamos el mensaje de error en el contexto. En caso de un failure se tiene a un RETRY que lo que hace es pasar a loading y volver a invocar todo el proceso de hacer el request.
En mi caso, tuve que modificar StepsLayout porque ahora el estado de Search es un objeto. Por lo que aumenté esa condición a mi función renderContent .
Finalmente, agregamos una transición con delay a la máquina con un after en tickets , que haga que cuando pasen 5 segundos nos redirija a intitial con el contexto reseteado.