Fetching data con React y XState

15 marzo, 2020

Foto por: Gratisography

Cuando estamos trabajando con frameworks del lado del cliente tales como React, Angular, Vue, etc una de las tareas más comunes que tenemos que realizar es hacer peticiones asíncronas al servidor. Y para poder mostrar los diferentes estados de la petición solemos usar valores booleanos a modo de “flags”, por ejemplo isLoading o hasError, en este artículo veremos una forma más eficiente y sobre todo predecible usando XState y React.

Creando componente GitHubUserProfile

Vamos a crear un componente el cual va a mostrar la información relacionada con un usuario en GitHub, datos como su nombre, bio, login, etc.

import React from 'react'

function GitHubUserProfile(props) {
  const { name, login, bio, followers, avatar_url, created_at } = props.user
  return (
    <div className="ui card">
      <div className="image">
        <img src={avatar_url} alt={name} />
      </div>
      <div className="content">
        <a
          target="_blank"
          rel="noopener noreferrer"
          href={`https://github.com/${login}`}
          className="header"
        >
          {name}
        </a>
        {created_at && (
          <div className="meta">
            <span className="date">Joined in {created_at.substring(0, 4)}</span>
          </div>
        )}
        <div className="description">{bio}</div>
      </div>
      <div className="extra content">
        <a
          target="_blank"
          rel="noopener noreferrer"
          href="https://github.com/gustavocd?tab=followers"
        >
          <i className="user icon" />
          {followers} Followers
        </a>
      </div>
    </div>
  )
}

export default GitHubUserProfile

Creando la state machine con XState

Continuaremos creando la state machine que nos permitirá modelar los diferentes estados de nuestra petición.

import { Machine, assign } from 'xstate'

const GITHUB_URL = 'https://api.github.com/users'

const fetchUser = username =>
  fetch(`${GITHUB_URL}/${username}`)
    .then(blob => blob.json())
    .then(data => {
      if (data.message) {
        return
      }
      return data
    })
    .catch(console.log)

const userProfileMachine = Machine({
  id: 'userProfile',
  initial: 'idle',
  context: {
    username: 'gustavocd',
    user: null,
    error: null,
  },
  states: {
    idle: {
      on: {
        FETCH: 'loading',
      },
    },
    loading: {
      invoke: {
        id: 'getUser',
        src: (context, event) => fetchUser(context.username),
        onDone: {
          target: 'success',
          actions: assign({ user: (context, event) => event.data }),
        },
        onError: {
          target: 'failure',
          actions: assign({ error: (context, event) => event.data }),
        },
      },
    },
    success: {},
    failure: {
      on: {
        RETRY: 'loading',
      },
    },
  },
})

Implementando la State Machine en el componente App

El paso final es usar nuestra state machine en el componente principal de nuestra app, usaremos un paquete llamado @xstate/react para poder usar react hooks.

import React, { useEffect } from 'react'
import { useMachine } from '@xstate/react'
import { Machine, assign } from 'xstate'
import GitHubUserProfile from './components/GitHubUserProfile'

/*
  Here is defined the userProfileMachine,
  I didn't rewrite here to simplify the example
*/

function App() {
  const [current, send] = useMachine(userProfileMachine)

  useEffect(() => {
    send('FETCH')
  }, [send])

  if (current.matches('loading') || current.matches('idle')) {
    return <span>Loading...</span>
  }

  return <GitHubUserProfile user={current.context.user} />
}

export default App

Ejemplo completo en codesandbox


Gustavo Castillo | Desarrollador Web | Aprende Enseñando y Compartiendo.