Infinite Scroll with Svelte 3 and IntersectionObserver 😍
Tags: frontend, javascript, svelte
Published: 2020-01-15 03:45:03

---
title: Infinite Scroll with Svelte 3 and IntersectionObserver 😍
published: true
description: 
cover_image: https://images.pexels.com/photos/2453136/pexels-photo-2453136.jpeg?cs=srgb&dl=acero-al-aire-libre-binoculares-que-funcionan-con-monedas-2453136.jpg&fm=jpg
tags: Frontend, JavaScript, Svelte
---

### Introduction

Recently I've been learning [Svelte 3](https://svelte.dev/), which is a frontend framework to cybernetically enhanced web apps and I liked it a lot. In this article, I'm going to show you how to build an application with an infinite scroll using IntersectionObserver and Svelte.

In my opinion, the best way of learning a new technology is by doing lots of toy projects, and having fun in the process, so let's get started.

### 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

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

### Create component's files

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

### Implement Footer component

```html
<style>
  .footer {
    display: flex;
    justify-content: center;
    color: #fff;
    padding-top: 3rem;
    padding-bottom: 2rem;
  }

  .footer a {
    color: inherit;
    text-decoration: underline;
  }
</style>

<footer class="footer">
  <span>Built with Svelte 3 original design by &nbsp;</span>
  <a href="https://axelfuhrmann.com/" target="_blank" rel="noopener">
    Axel Fuhrmann
  </a>
</footer>
```

### Implement Header component

```html
<style>
  .header {
    height: 50vh;
    display: flex;
    align-items: center;
    justify-content: center;
    text-transform: uppercase;
  }

  .title {
    font-size: 3.6em;
    display: flex;
    color: var(--text-color);
    align-items: center;
    flex-direction: column;
  }

  .title span {
    display: block;
    background-color: var(--text-color);
    border: medium none;
    font-size: 1.875rem;
    color: var(--orange-color);
    padding: 0 1rem;
    transform: skew(2deg);
  }
</style>

<header class="header">
  <h1 class="title">
    The Rick and Morty APP
    <span>Clone</span>
  </h1>
</header>
```

### Implement Loader component

```html
<style>
  .spinner {
    color: official;
    display: inline-block;
    position: relative;
    width: 40px;
    height: 40px;
  }

  .spinner div {
    transform-origin: 20px 20px;
    animation: spinner 1.2s linear infinite;
  }

  .spinner div:after {
    content: ' ';
    display: block;
    position: absolute;
    top: 1.5px;
    left: 18px;
    width: 1.5px;
    height: 4.5px;
    border-radius: 10%;
    background: #fff;
  }

  .spinner div:nth-child(1) {
    transform: rotate(0deg);
    animation-delay: -1.1s;
  }

  .spinner div:nth-child(2) {
    transform: rotate(30deg);
    animation-delay: -1s;
  }

  .spinner div:nth-child(3) {
    transform: rotate(60deg);
    animation-delay: -0.9s;
  }

  .spinner div:nth-child(4) {
    transform: rotate(90deg);
    animation-delay: -0.8s;
  }

  .spinner div:nth-child(5) {
    transform: rotate(120deg);
    animation-delay: -0.7s;
  }

  .spinner div:nth-child(6) {
    transform: rotate(150deg);
    animation-delay: -0.6s;
  }

  .spinner div:nth-child(7) {
    transform: rotate(180deg);
    animation-delay: -0.5s;
  }

  .spinner div:nth-child(8) {
    transform: rotate(210deg);
    animation-delay: -0.4s;
  }

  .spinner div:nth-child(9) {
    transform: rotate(240deg);
    animation-delay: -0.3s;
  }

  .spinner div:nth-child(10) {
    transform: rotate(270deg);
    animation-delay: -0.2s;
  }

  .spinner div:nth-child(11) {
    transform: rotate(300deg);
    animation-delay: -0.1s;
  }

  .spinner div:nth-child(12) {
    transform: rotate(330deg);
    animation-delay: 0s;
  }

  @keyframes spinner {
    0% {
      opacity: 1;
    }

    100% {
      opacity: 0;
    }
  }
</style>

<div class="spinner">
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
</div>
```

### Implement Character component

```html
<script>
  import { relativeTime } from '../lib'

  export let character = {
    id: 0,
    image: '',
    name: '',
    created: '',
    status: '',
    species: '',
    gender: '',
    origin: '',
    location: '',
  }
</script>

<style>
  .character {
    min-width: 300px;
    border-radius: 0.625rem;
    overflow: hidden;
    margin-bottom: 0.625rem;
    box-shadow: rgba(0, 0, 0, 0.16) 0px 2px 2px 0px,
      rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
  }

  .image-container {
    position: relative;
    width: 100%;
    background: rgb(32, 35, 41) none repeat scroll 0% 0%;
  }

  .image-container img {
    margin: 0;
    opacity: 1;
    transition: opacity 0.5s ease 0s;
  }

  .name-container {
    width: 100%;
    background: rgb(32, 35, 41) none repeat scroll 0% 0%;
    opacity: 0.8;
    position: absolute;
    bottom: 0px;
    padding: 0.625rem;
  }

  .name {
    color: rgb(245, 245, 245);
    font-size: 1.625rem;
    font-weight: 400;
    margin-bottom: 0.5rem;
    font-stretch: expanded;
  }

  .id {
    color: rgb(187, 187, 187);
    margin: 0;
    font-size: 0.875rem;
    letter-spacing: 0.5px;
  }

  .info {
    padding: 1.25rem;
    height: 100%;
    background: rgb(51, 51, 51) none repeat scroll 0% 0%;
  }

  .info > div {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: nowrap;
    padding: 0.75rem 0px 0.375rem;
    border-bottom: 1px solid var(--boder-bottom-color);
  }

  .info > div span {
    font-size: 0.7rem;
    font-weight: 400;
    color: rgb(158, 158, 158);
  }

  .info > div span:nth-child(2) {
    color: var(--orange-color);
  }
</style>

<article class="character">
  <div class="image-container">
    <div>
      <img src={character.image} alt={character.name} />
    </div>
    <div class="name-container">
      <h2 class="name">{character.name}</h2>
      <p class="id">
        id: {character.id} - created {relativeTime(character.created)}
      </p>
    </div>
  </div>
  <div class="info">
    <div>
      <span>STATUS</span>
      <span>{character.status}</span>
    </div>
    <div>
      <span>SPECIES</span>
      <span>{character.species}</span>
    </div>
    <div>
      <span>GENDER</span>
      <span>{character.gender}</span>
    </div>
    <div>
      <span>ORIGIN</span>
      <span>{character.origin}</span>
    </div>
    <div>
      <span>LAST LOCATION</span>
      <span>{character.location}</span>
    </div>
  </div>
</article>
```

### Create utility functions

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

```javascript
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,
  }
}

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')
}
```

### Update App component

```html
<script>
  import { onMount } from 'svelte'
  import { transformCharacter } from './lib'
  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 loading = true
  let characters = []
  let pageInfo = {}
  let options = {
    root: document.getElementById('scrollArea'),
    rootMargin: '0px',
    threshold: 0.5,
  }
  let observer = new IntersectionObserver(handleIntersection, options)

  async function handleIntersection(event) {
    const [entries] = event
    loading = true
    try {
      if (!entries.isIntersecting || !pageInfo.next) {
        loading = false
        return
      }
      const blob = await fetch(pageInfo.next)
      const { results, info } = await blob.json()
      characters = [
        ...characters,
        ...results.map(result => trasformCharacter(result)),
      ]
      pageInfo = info
      loading = false
    } catch (error) {
      loading = false
      console.log(error)
    }
  }

  onMount(async () => {
    try {
      const blob = await fetch('https://rickandmortyapi.com/api/character')
      const { results, info } = await blob.json()
      characters = results.map(result => trasformCharacter(result))
      pageInfo = info
      loading = false
      observer.observe(document.querySelector('footer'))
    } catch (error) {
      loading = false
      console.log(error)
    }
  })
</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 {
    display: flex;
    align-items: center;
    justify-content: center;
  }
</style>

<Header />
<section class="container" id="scrollArea">
  <div class="inner">
    {#each characters as character (character.id)}
      <Character {character} />
    {/each}
  </div>
  <div class="loader">
    {#if loading}
      <Loader />
    {/if}
  </div>
  <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)

Happy coding 👋🏽