Infinite Scrolling with Svelte 3, XState and IntersectionObserver
Tags: xstate, svelte, webdev
Published: 2021-02-01 19:01:01

---
title: "Infinite Scrolling with Svelte 3, XState and IntersectionObserver"
published: true
cover_image: https://images.pexels.com/photos/63901/pexels-photo-63901.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500
tags: xstate, svelte, webdev
---

## Introduction

A year ago I created [a post](https://dev.to/gcdcoder/infinite-scroll-with-svelte-3-and-intersectionobserver-44p8) related to infinite scrolling with Svelte and IntersectionObserver, this time I'm going to show you a similar project (we could say it's the second version) using XState.

> If you want to try it out go to this [Live Demo](https://infinite-scroll-gq7zymydr.vercel.app/)

## Create the project folder

Execute these commands in your terminal one by one in order to use the Svelte template.

```sh
# Install a Svelte project using sveltejs/template
$ npx degit sveltejs/template infinite-scroll

# Change working directory
$ cd infinite-scroll

# Install npm dependencies
$ npm install

# Install XState
$ npm install xstate

# Run the dev server
$ npm run dev
```

## Create components

Create a folder called components inside src one, then create four components: `Character.svelte`, `Footer.svelte`, `Header.svelte`, and `Loader.svelte`, make sure you use Svelte's file extension, which is `.svelte`.

The components are basically the same as the previous post you can find the code [here](https://github.com/gustavocd/infinite-scroll/tree/master/src/components).

## Create utility functions

Create a folter called lib inside _src_ one, then create an `index.js` file, so let's implement some util functions to make our life easier:

```jsx
export const transformCharacter = character => {
  const {
    id,
    name,
    status,
    species,
    created,
    image,
    gender,
    origin,
    location,
  } = character
  return {
    id,
    name,
    image,
    status,
    species,
    gender,
    created,
    origin: origin.name,
    location: location.name,
  }
}

// Format created date.
export const relativeTime = created => {
  const createdYear = new Date(created).getFullYear()
  const currentYear = new Date().getFullYear()
  const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
  return rtf.format(createdYear - currentYear, 'year')
}

// Util function to fetch the list of Characters.
export const fetchCharacters = async (context) =>  {
try {
    const endpoint = context.characters.length > 0 ? context.pageInfo.next : 'https://rickandmortyapi.com/api/character'
    const blob = await fetch(endpoint)
    const { results, info, error } = await blob.json()
    if (!blob.ok) {
      return Promise.reject(error)
    }
    const characters = results.map(result => transformCharacter(result))
    return Promise.resolve({ characters, info })
  } catch (error) {
    return Promise.reject(error.message)
  }
}
```

## Create the State Machine

In _lib_ folder create a file called `scrollMachine.js`, and let's implement our state machine using XState.

```jsx
import { createMachine, assign } from 'xstate'

const scrollMachine = createMachine({
  id: 'scrollMachine',
  context: {
    characters: [],
    pageInfo: {},
    error: '',
  },
  initial: 'idle',
  states: {
    idle: {
      on: {
        FETCH: {
          target: 'loading',
        },
      },
    },
    loading: {
      invoke: {
        id: 'characters',
        src: 'fetchCharacters',
        onDone: {
          target: 'success',
          actions: 'setCharacters',
        },
        onError: {
          target: 'failure',
          actions: 'setError',
        },
      },
    },
    loadMore: {
      invoke: {
        src: 'fetchCharacters',
        onDone: {
          target: 'success',
          actions: 'setMoreCharacters',
        },
        onError: {
          target: 'failure',
          actions: 'setError',
        },
      },
    },
    success: {
      on: {
        FETCH_MORE: {
          target: 'loadMore',
          cond: 'hasMoreCharacters',
        },
      },
    },
    failure: {
      type: 'final',
    },
  },
}, {
  guards: {
    hasMoreCharacters: ({ pageInfo }) => pageInfo.next,
  },
  actions: {
    setCharacters: assign({
      characters: (_, event) => event.data.characters,
      pageInfo: (_, event) => event.data.info,
    }),
    setMoreCharacters: assign({
      characters: ({ characters }, { data }) => [...characters, ...data.characters],
      pageInfo: (_, { data }) => data.info,
    }),
    setError: assign({
      error: (_, event) => event.data,
    }),
  },
})

export default scrollMachine
```

## Update App component

```html
<script>
  import { onMount, onDestroy } from 'svelte'
  import { interpret } from 'xstate'
  import { fetchCharacters } from './lib'
  import scrollMachine from './lib/scrollMachine'
  import Header from './components/Header.svelte'
  import Loader from './components/Loader.svelte'
  import Character from './components/Character.svelte'
  import Footer from './components/Footer.svelte'

  // Let's pass the fetchCharacters function
  // as a service to our state machine.
  const machine = scrollMachine.withConfig({
    services: { fetchCharacters },
  })
  
  // Interpret our machine and start it ("create an instance").
  const service = interpret(machine).start()

  // Create options to our IntersectionObserver instance.
  let options = {
    root: document.getElementById('scrollArea'),
    rootMargin: '0px',
    threshold: 0.5,
  }

  // Handle intersection and send `FETCH_MORE` event to
  // our state machine if there are more characters to load.
  let observer = new IntersectionObserver(event => {
    const [entries] = event
    if (!entries.isIntersecting || !$service.context.pageInfo.next) {
      return
    }
    service.send({ type: 'FETCH_MORE' })
  }, options)

  // Fetch characters on component mounts.
  onMount(() => {
    service.send({ type: 'FETCH' })
    observer.observe(document.querySelector('footer'))
  })

  // Remove observer from our target to avoid potential memory leaks.
  onDestroy(() => {
    observer.unobserve(document.querySelector('footer'))
  })
</script>

<style>
  .container {
    min-height: 50vh;
    background-color: var(--text-color);
  }
  .inner {
    max-width: 80em;
    margin: 0 auto;
    padding: 3rem 0;
    display: grid;
    grid-gap: 20px;
    justify-items: center;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  }
  .loader,
  .error {
    padding-top: var(--padding-lg);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .error {
    color: var(--orange-color);
    font-size: 18px;
  }
</style>

<Header />
<section class="container" id="scrollArea">
  {#if $service.matches('success') || $service.matches('loadMore')}
    <div class="inner">
      {#each $service.context.characters as character (character.id)}
        <Character {character} />
      {/each}
    </div>
  {/if}
  {#if $service.matches('failure')}
    <div class="error"><span>{$service.context.error}</span></div>
  {/if}
  {#if $service.matches('loading') || $service.matches('loadMore')}
    <div class="loader">
      <Loader />
    </div>
  {/if}
  <Footer />
</section>
```

Notes:
Rick and Marty API docs can be found: [here](https://rickandmortyapi.com/documentation/)
GitHub Repository: [here](https://github.com/gustavocd/infinite-scroll)
Youtube video (Spanish): [here](https://www.youtube.com/watch?v=GMXrwrG0cL4)

Happy coding 👋🏽