InertiaJS - make scroll regions preserve their scroll on next requests

Constantin Druc • January 11, 2021

Problem: Preserve and restore the scroll position of a scroll region when it's on the same page with whatever is displayed when clicking on the list elements.

In our example bellow, we have a list with people and a container next to it showing the selected person.

Here's a video showing what I mean:

So... how do I do this with scroll region?

Surprise! You can't. At least not by using scroll-region.

The title has nothing to do with the solution, but that's what I typed into google looking for it, so I assume most people will use the same search terms.

If you're not into reading, you can also watch the screencast I made on InertiaJS scroll management and then come back for the code snippets.

Using Preserve-scroll and Scroll-region

preserve-scroll is useful for form submissions that end up redirecting back to the same page. You don't want your page to jump up when submitting the form, so you use preserve-scroll to maintain the position when redirecting back.

scroll-region can be used to remember and restore the scroll position of scrollable elements (using the overflow CSS property) when browsing back, either from inertia or non-inertia pages.

Adding preserve-state to prevent our component from being fully rerendered, it looks like scroll-region and preserve-scroll might solve our problem:

<template>
  <div>
    <div class="overflow-y-scroll h-96" ref="people" scroll-region>
      <inertia-link v-for="pers in people" :key="pers.id"
                    :href="route('person', {person: pers.id})" preserve-state preserve-scroll 
                    :class="{'active': pers.id === person.id}" class="person-card">
        <img :src="pers.avatar_url" :alt="pers.name" class="avatar">
        <div>
          <h3>{{ pers.name }}</h3>
          <div>{{ pers.phone }}</div>
        </div>
      </inertia-link>
    </div>

    <div>
        <!-- section displaying the current person's page -->
    </div>
  </div>
</template>

<script>
  export default {
    props: {
      people: Array,
      person: Object
    },
  }
</script>

But it does not.

Contrary to what you might think, we do not want to restore the previous scroll position, but the one before that. Getting the previous scroll would have shown us the exact same position, not the one containing the previously selected item. Check the video below:

As you can see, when using a combination of scroll-region and preserve-scroll inertia will preserve the exact old scroll position - but we want the scroll position before that, the one showing our selected person.

Solution

We still need to use preserve-state and preserve-scroll to prevent our page from fully re-rendering and lose the list scroll position when clicking a different person. That will solve our navigating forward problem.

To solve our navigating backward problem, we need to remember the correct list scroll position. Specifically, the scroll position after inertia finishes the request. This will allow us to grab the scroll value from our remembered state and restore the scroll position to the one set by the previous request.

Check out the code below:

<template>
  <div>
    <div class="overflow-y-scroll h-96" ref="people">
      <inertia-link v-for="pers in people" :key="pers.id"
                    :href="route('person', {person: pers.id})" preserve-state preserve-scroll 
                    :class="{'active': pers.id === person.id}" class="person-card">
        <img :src="pers.avatar_url" :alt="pers.name" class="avatar">
        <div>
          <h3>{{ pers.name }}</h3>
          <div>{{ pers.phone }}</div>
        </div>
      </inertia-link>
    </div>

    <div>
        <!-- section displaying the current person's page -->
    </div>
  </div>
</template>

<script>
  export default {
    props: {
      people: Array,
      person: Object
    },

    // tell inertia to remember the `peopleScrollTop` state
    remember: 'peopleScrollTop',

    data() {
      return {
        peopleScrollTop: 0
      }
    },

    mounted() {
      // restore the scroll position from history
      this.$refs.people.scrollTop = window.history.state?.rememberedState?.default?.peopleScrollTop;

      // make sure to remove the listener once the component is destroyed 
      this.$once(
        'hook:destroyed',
        // set the scroll position when the request is finished
        this.$inertia.on('finish', (event) => this.peopleScrollTop = this.$refs.people.scrollTop)
      );
    },
  }
</script>

If you found this post useful and want to learn more about InertiaJS subscribe to my email list bellow!

What's your e-mail?

Get a monthly digest of what I've been up to: screencasts, blogposts, and other goodies.