One place for hosting & domains

      Implementing

      Implementing an Infinite Scroll list in React Native

      Introduction

      While implementing pagination in mobile devices, one has to take a different approach since space is minimal, unlike the web, due to this factor, infinite scrolling has always been the go-to solution, giving your users a smooth and desirable experience.

      In this tutorial, we will be building an infinite scroll list using the FlatList component in React Native, we will be consuming Punk API which is a free beer catalog API.

      Here’s a small demo video of what the end result will look like:

      We will be using create-react-native-app to bootstrap our React Native app, run the following command to install it globally:

      1. npm install -g create-react-native-app

      Next, we need to bootstrap the app in your preferred directory:

      1. react-native init react_native_infinite_scroll_tutorial

      I’ll be using an android emulator for this tutorial but the code works for both iOS and Android platforms. In case you don’t have an android emulator setup follow the instructions provided in the Android documentation.

      Make sure your emulator is up and running then navigate to your project directory and run the following command:

      1. react-native run-android

      This should download all required dependencies and install the app on your emulator and then launch it automatically, You should have a screen with the default text showing as follows:

      Now that we have our sample app up and running, we will now install the required dependencies for the project, we will be using the Axios for making requests to the server and Glamorous Native for styling our components, run the following command to install them:

      1. npm install -S axios glamorous-native

      Directory structure is always crucial in an application, since this is a simple demo app, we’ll keep this as minimal as possible:

      src
      ├── App.js
      ├── components
      │   ├── BeerPreviewCard
      │   │   ├── BeerPreviewCard.js
      │   │   └── index.js
      │   ├── ContainedImage
      │   │   └── index.js
      │   └── Title
      │       └── index.js
      ├── config
      │   └── theme.js
      └── utils
          └── lib
              └── axiosService.js
      

      In order to make our Axios usage easy, we will create a singleton instance of the Axios service that we can import across our components:

      import axios from 'axios';
      
      const axiosService = axios.create({
        baseURL: 'https://api.punkapi.com/v2/',
        timeout: 10000,
        headers: {
          'Content-Type': 'application/json'
        }
      });
      
      
      export default axiosService;
      

      Next, we will create cards to display our beer data and add some designs to it.

      theme.js

      This file contains the app color palette which we will use across the app.

      export const colors = {
        french_blue: '#3f51b5',
        deep_sky_blue: '#007aff',
        white: '#ffffff',
        black: '#000000',
        veryLightPink: '#f2f2f2'
      };
      

      Title.js

      This file contains the card text component that we will use to display the beer name in the card.

      import glamorous from 'glamorous-native';
      import { colors } from '../../config/theme';
      
      const Title = glamorous.text((props, theme) => ({
        fontFamily: 'robotoRegular',
        fontSize: 16,
        color: props.color || colors.black,
        lineHeight: 24,
        textAlign: props.align || 'left',
        alignSelf: props.alignSelf || 'center'
      }));
      
      export default Title;
      

      ContainedImage.js

      This file contains our image component which will have a resizeMode of contained in order to have the image fit within its containing component.

      import React from 'react';
      import glamorous from 'glamorous-native';
      
      const CardImageContainer = glamorous.view((props, theme) => ({
        flex: 1,
        alignItems: 'stretch'
      }));
      
      const StyledImage = glamorous.image((props, theme) => ({
        position: 'absolute',
        top: 0,
        left: 0,
        bottom: 0,
        right: 0
      }));
      
      const ContainedImage = props => {
        return (
          <CardImageContainer>
            <StyledImage resizeMode="contain" {...props} />
          </CardImageContainer>
        );
      };
      
      export default ContainedImage;
      

      BeerPreviewCard.js

      This file contains the main card container, this is where we combine the title component and the image component to form a card that displays the beer name and image.

      import React from 'react';
      import glamorous from 'glamorous-native';
      
      
      import { colors } from '../../config/theme';
      
      
      import Title from '../Title';
      import ContainedImage from '../ContainedImage';
      
      const CardContainer = glamorous.view((props, theme) => ({
        height: 160,
        width: '85%',
        left: '7.5%',
        justifyContent: 'space-around'
      }));
      
      const CardImageContainer = glamorous.view((props, theme) => ({
        flex: 1,
        alignItems: 'stretch'
      }));
      
      const BeerNameContainer = glamorous.view((props, theme) => ({
        height: '30%',
        backgroundColor: colors.deep_sky_blue,
        justifyContent: 'center'
      }));
      
      const BeerPreviewCard = ({ name, imageUrl }) => {
        return (
          <CardContainer>
            <CardImageContainer>
              <ContainedImage source={{ uri: imageUrl }} />
            </CardImageContainer>
            <BeerNameContainer>
              <Title align="center" color={colors.white}>
                {name}
              </Title>
            </BeerNameContainer>
          </CardContainer>
        );
      };
      
      export default BeerPreviewCard;
      

      Fetching Beers

      The logic for fetching beers will be in App.js which is the main component of the app, we need to consume the API by making a GET request to fetch a list of paginated beers:

      import React, { Component } from 'react';
      
      
      import axiosService from './utils/lib/axiosService';
      
      export default class AllBeersScreen extends Component {
        state = {
          data: [],
          page: 1,
          loading: true,
          error: null
        };
      
        componentDidMount() {
          this._fetchAllBeers();
        }
      
        _fetchAllBeers = () => {
          const { page } = this.state;
          const URL = `/beers?page=${page}&per_page=10`;
      
          axiosService
            .request({
              url: URL,
              method: 'GET'
            })
            .then(response => {
              this.setState((prevState, nextProps) => ({
                data:
                  page === 1
                    ? Array.from(response.data)
                    : [...this.state.data, ...response.data],
                loading: false
              }));
            })
            .catch(error => {
              this.setState({ error, loading: false });
            });
        };
      
        render() {
          return (
            
          );
        }
      }
      

      So what is a FlatList component? I’ll quote the React Native docs which describes it as a performant interface for rendering simple, flat lists, supporting the following features:

      • Fully cross-platform.
      • Optional horizontal mode.
      • Configurable viewability callbacks.
      • Header support.
      • Footer support.
      • Separator support.
      • Pull to Refresh.
      • Scroll loading.
      • ScrollToIndex support

      We will be using a few features from the above list for our app namely footer, pull to refresh, and scroll loading.

      Basic Usage

      To use the FlatList component, you have to pass two main props which are RenderItem and data

      We can now pass the data we fetched earlier on to the FlatList component and use the BeerPreviewCard component to render a basic FlatList as follows:

      export default class AllBeersScreen extends Component {
       
       render() {
          return (
               <FlatList
                contentContainerStyle={{
                  flex: 1,
                  flexDirection: 'column',
                  height: '100%',
                  width: '100%'
                }}
                data={this.state.data}
                keyExtractor={item => item.id.toString()}
                renderItem={({ item }) => (
                  <View
                    style={{
                      marginTop: 25,
                      width: '50%'
                    }}
                  >
                    <BeerPreviewCard name={item.name} imageUrl={item.image_url} />
                  </View>
                )}
              />
          );
      }
      

      Reload your app and you should a view similar to this:

      Scroll Loading

      The main feature of infinite scrolling is loading content on-demand as the user scrolls through the app, to achieve this, the FlatList component requires two props namely onEndReached and onEndReachedThreshold.

      onEndReached is the callback called when the users scroll position is close to the onEndReachedThreshold of the rendered content, onEndReachedThreshold is basically a number that indicates the user’s scroll position in relation to how far it is from the end of the visible content when the user reaches the specified position, the onEndReached callback is triggered.

      A value of 0.5 will trigger onEndReached when the end of the content is within half the visible length of the list, which is what we need for this use case.

      export default class AllBeersScreen extends Component {
        state = {
          data: [],
          page: 1,
          loading: true,
          loadingMore: false,
          error: null
        }; 
      
        
      
        _handleLoadMore = () => {
          this.setState(
            (prevState, nextProps) => ({
              page: prevState.page + 1,
              loadingMore: true
            }),
            () => {
              this._fetchAllBeers();
            }
          );
        };
      
        render() {
          return (
            <FlatList
              contentContainerStyle={{
                flex: 1,
                flexDirection: 'column',
                height: '100%',
                width: '100%'
              }}
              data={this.state.data}
              renderItem={({ item }) => (
                <View
                  style={{
                    marginTop: 25,
                    width: '50%'
                  }}
                >
                  <BeerPreviewCard name={item.name} imageUrl={item.image_url} />
                </View>
              )}
              onEndReached={this._handleLoadMore}
              onEndReachedThreshold={0.5}
              initialNumToRender={10}
            />
          );
        }
      }
      

      If you go back to the app and scroll down, you’ll notice the beer list has been automatically loaded as you scroll down (see the demo at the start of the tutorial).

      The footer is basically the bottom part of our FlatList component, when the user scrolls down we want to show a loader when the content is been fetched, we can achieve this using the ListFooterComponent
      prop where we will pass a function that returns an ActivityIndicator component wrapped in a View component:

        _renderFooter = () => {
          if (!this.state.loadingMore) return null;
      
          return (
            <View
              style={{
                position: 'relative',
                width: width,
                height: height,
                paddingVertical: 20,
                borderTopWidth: 1,
                marginTop: 10,
                marginBottom: 10,
                borderColor: colors.veryLightPink
              }}
            >
              <ActivityIndicator animating size="large" />
            </View>
          );
        };
      
       render() {
          return (
               <FlatList
                
                ListFooterComponent={this._renderFooter}
              />
          );
        }
      

      Now when scrolling a loader will show on the screen while the content is loading (see the demo at the start of the tutorial)

      Pull To Refresh

      Pull to refresh functionality is widely used in almost every modern application that uses network activity to fetch data, to achieve this in the FlatList, we need to pass the onRefresh prop which triggers a callback when the user carries a pull-down gesture at the top of the screen:

        _handleRefresh = () => {
          this.setState(
            {
              page: 1,
              refreshing: true
            },
            () => {
              this._fetchAllBeers();
            }
          );
        };
      
        render() {
          return (
            <FlatList
              
              onRefresh={this._handleRefresh}
              refreshing={this.state.refreshing}
            />
          );
        }
      

      Now when you try pulling down from the top part of the screen a loader will appear from the top and the content will be refetched.

      initialNumToRender – This is the number of items we want to render when the app loads the data.

      keyExtractor – Used to extract a unique key for a given item at the specified index.

      Infinite scrolling grants your users a smooth experience while using your app and is an easy way for you to deliver presentable and well-ordered content for your users.

      You can access the code here.

      Implementing a Scroll Based Animation with JavaScript


      Introduction

      There is a kind of animation that has not stopped increasing its presence in the most modern and original websites: the animations based on the scroll event of JavaScript. This trend literally exploded when the parallax effects appeared, and since then its use has become more frequent.

      But the truth is that you must be very careful when implementing an animation based on scroll, since it can seriously affect the performance of the website, especially on mobile devices.

      That’s why we invite you to continue reading the tutorial, where we will implement from scratch and with vanilla JavaScript, this beautiful animation based on scroll, also keeping good performance even on mobile devices:

      Let’s start!

      Prerequisites

      This tutorial uses Sass to loop through 10 images with @for and reference parent selectors with Parent Selector (&). It is possible to accomplish the same result with CSS, but it will require additional work.

      HTML Structure

      We will use a simple HTML structure, where each image in the design will actually be a div element in the HTML code, and the images will be defined and positioned with CSS, which will facilitate this task:

      <!-- The `.container` element will contain all the images -->
      <!-- It will be used also to perform the custom scroll behavior -->
      <div class="container">
        <!-- Each following `div` correspond to one image -->
        <!-- The images will be set using CSS backgrounds -->
        <div class="image"></div>
        <div class="image"></div>
        <div class="image"></div>
        <div class="image"></div>
        <div class="image"></div>
        <div class="image"></div>
        <div class="image"></div>
        <div class="image"></div>
        <div class="image"></div>
        <div class="image"></div>
      </div>
      

      Now let’s look at the CSS styles needed to achieve the desired design.

      Applying CSS styles

      First, let’s start by creating the layout.

      This time we will use a CSS Grid, taking advantage of the fact that this technology is already supported in all modern browsers.

      // The container for all images
      .container {
        // 2 columns grid
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-gap: 0 10%;
        justify-items: end; // This will align all items (images) to the right
      
        // Fixed positioned, so it won't be affected by default scroll
        // It will be moved using `transform`, to achieve a custom scroll behavior
        position: fixed;
        top: 0;
        left: 0;
      
        width: 100%;
      }
      

      It is important to note that, in addition to the CSS grid, we are giving the .container element a fixed position. This will make this element not affected by the default scroll behavior, allowing to perform custom transforms with JavaScript.

      Now let’s see how to define the styles associated with the images. See the comments for a brief explanation of each part:

      // Styles for image elements
      // Mainly positioning and background styles
      .image {
        position: relative;
        width: 300px;
        height: 100vh;
        background-repeat: no-repeat;
        background-position: center;
      
        // This will align all even images to the left
        // For getting centered positioned images, respect to the viewport
        &:nth-child(2n) {
          justify-self: start;
        }
      
        // Set each `background-image` using a SCSS `for` loop
        @for $i from 1 through 10 {
          &:nth-child(#{$i}) {
            background-image: url('../img/image#{$i}.jpg');
          }
        }
      }
      

      Now let’s make some adjustments for small screens since there we should have a column instead of two.

      // Adjusting layout for small screens
      @media screen and (max-width: 760px) {
        .container {
          // 1 column grid
          grid-template-columns: 1fr;
          // Fix image centering
          justify-items: center;
        }
      
        // Fix image centering
        .image:nth-child(2n) {
          justify-self: center;
        }
      }
      

      And this way we have our design almost ready, we just need to add the background to the body, which we will not explain so as not to extend the tutorial with trivial details.

      Note also that for now, you will not be able to scroll, since we have given a fixed position to the container element. Next, we will solve this problem and bring our design to life.

      Implementing Animations with JavaScript

      Now let’s see how to implement, from scratch and using vanilla JavaScript, a custom scroll movement, smoother and suitable for the animations planned. All this we will achieve without trying to reimplement all the work associated with the scroll that the web browser does. Instead, we will keep the native scroll functionality, at the same time that we will have a custom scroll behavior.
      Sounds good, huh? Let’s see how to do it!

      Useful Functions and Variables

      First let’s look at some useful functions that we will be using. Lean on the comments for a better understanding:

      // Easing function used for `translateX` animation
      // From: https://gist.github.com/gre/1650294
      function easeOutQuad (t) {
        return t * (2 - t)
      }
      
      // Returns a random number (integer) between `min` and `max`
      function random (min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min
      }
      
      // Returns a random number as well, but it could be negative also
      function randomPositiveOrNegative (min, max) {
        return random(min, max) * (Math.random() > 0.5 ? 1 : -1)
      }
      
      // Set CSS `tranform` property for an element
      function setTransform (el, transform) {
        el.style.transform = transform
        el.style.WebkitTransform = transform
      }
      

      And these are the variables that we will be using also, described briefly to understand much better the code that we will present below:

      // Current scroll position
      var current = 0
      // Target scroll position
      var target = 0
      // Ease or speed for moving from `current` to `target`
      var ease = 0.075
      // Utility variables for `requestAnimationFrame`
      var rafId = undefined
      var rafActive = false
      // Container element
      var container = document.querySelector('.container')
      // Array with `.image` elements
      var images = Array.prototype.slice.call(document.querySelectorAll('.image'))
      // Variables for storing dimmensions
      var windowWidth, containerHeight, imageHeight
      
      // Variables for specifying transform parameters (max limits)
      var rotateXMaxList = []
      var rotateYMaxList = []
      var translateXMax = -200
      
      // Populating the `rotateXMaxList` and `rotateYMaxList` with random values
      images.forEach(function () {
        rotateXMaxList.push(randomPositiveOrNegative(20, 40))
        rotateYMaxList.push(randomPositiveOrNegative(20, 60))
      })
      

      With all this ready, let’s see how to implement our custom scroll behavior.

      Implementing the Custom Scroll Behavior

      To make our webpage scrollable, we will add a new div element to the body dynamically, to which we will set the same height of our container element, in such a way that the scrollable area will be the same.

      // The `fakeScroll` is an element to make the page scrollable
      // Here we are creating it and appending it to the `body`
      var fakeScroll = document.createElement('div')
      fakeScroll.className="fake-scroll"
      document.body.appendChild(fakeScroll)
      // In the `setupAnimation` function (below) we will set the `height` properly
      

      We also need a bit of CSS styles so that our .fake-scroll element makes the page scrollable, without interfering with the layout and the other elements:

      // The styles for a `div` element (inserted with JavaScript)
      // Used to make the page scrollable
      // Will be setted a proper `height` value using JavaScript
      .fake-scroll {
        position: absolute;
        top: 0;
        width: 1px;
      }
      

      Now let’s see the function responsible for calculating all the necessary dimensions, and preparing the ground for the animations:

      // Geeting dimmensions and setting up all for animation
      function setupAnimation () {
        // Updating dimmensions
        windowWidth = window.innerWidth
        containerHeight = container.getBoundingClientRect().height
        imageHeight = containerHeight / (windowWidth > 760 ? images.length / 2 : images.length)
        // Set `height` for the fake scroll element
        fakeScroll.style.height = containerHeight + 'px'
        // Start the animation, if it is not running already
        startAnimation()
      }
      

      Once the setupAnimation function is called, the page will be scrollable, and everything will be ready to start listening to the scroll event and run the animation.

      So let’s see what we will do when the scroll event is triggered:

      // Update scroll `target`, and start the animation if it is not running already
      function updateScroll () {
        target = window.scrollY || window.pageYOffset
        startAnimation()
      }
      
      // Listen for `scroll` event to update `target` scroll position
      window.addEventListener('scroll', updateScroll)
      

      Each time the scroll event is triggered, you simply update the target variable with the new position, and call the startAnimation function, which does nothing but start the animation if it is not active yet. Here is the code:

      // Start the animation, if it is not running already
      function startAnimation () {
        if (!rafActive) {
          rafActive = true
          rafId = requestAnimationFrame(updateAnimation)
        }
      }
      

      Now let’s see the internal behavior for the updateAnimation function, which is the one that actually performs all calculations and transformations in each frame, to achieve the desired animation.
      Please follow the comments for a better understanding of the code:

      // Do calculations and apply CSS `transform`s accordingly
      function updateAnimation () {
        // Difference between `target` and `current` scroll position
        var diff = target - current
        // `delta` is the value for adding to the `current` scroll position
        // If `diff < 0.1`, make `delta = 0`, so the animation would not be endless
        var delta = Math.abs(diff) < 0.1 ? 0 : diff * ease
      
        if (delta) { // If `delta !== 0`
          // Update `current` scroll position
          current += delta
          // Round value for better performance
          current = parseFloat(current.toFixed(2))
          // Call `update` again, using `requestAnimationFrame`
          rafId = requestAnimationFrame(updateAnimation)
        } else { // If `delta === 0`
          // Update `current`, and finish the animation loop
          current = target
          rafActive = false
          cancelAnimationFrame(rafId)
        }
      
        // Update images (explained below)
        updateAnimationImages()
      
        // Set the CSS `transform` corresponding to the custom scroll effect
        setTransform(container, 'translateY('+ -current +'px)')
      }
      

      And our custom scroll behavior is ready!

      After calling the function setupAnimation, you could scroll as you normally would, and the .container element would be moved in correspondence, but with a very smooth and pleasant effect.

      Then we only have to animate the images in correspondence with the position in which they are with respect to the viewport. Let’s see how to do it!

      Animating Images While Scrolling

      To animate the images we will use the current position of the fake scroll (current), and we will calculate the intersectionRatio (similar to the value from the IntersectionObserver API) between each image and the viewport. Then, we just have to apply the transformations that we want depending on that ratio, and we will obtain the desired animation.

      The idea is to show the image without any transformation when it is in the center of the screen (intersectionRatio = 1), and to increase the transformations as the image moves towards the ends of the screen (intersectionRatio = 0).

      Pay close attention to the code shown below, especially the part where the intersectionRatio for each image is calculated. This value is essential to then apply the appropriate CSS transformations. Please follow the comments for a better understanding:

      // Calculate the CSS `transform` values for each `image`, given the `current` scroll position
      function updateAnimationImages () {
        // This value is the `ratio` between `current` scroll position and images `height`
        var ratio = current / imageHeight
        // Some variables for using in the loop
        var intersectionRatioIndex, intersectionRatioValue, intersectionRatio
        var rotateX, rotateXMax, rotateY, rotateYMax, translateX
      
        // For each `image` element, make calculations and set CSS `transform` accordingly
        images.forEach(function (image, index) {
          // Calculating the `intersectionRatio`, similar to the value provided by
          // the IntersectionObserver API
          intersectionRatioIndex = windowWidth > 760 ? parseInt(index / 2) : index
          intersectionRatioValue = ratio - intersectionRatioIndex
          intersectionRatio = Math.max(0, 1 - Math.abs(intersectionRatioValue))
          // Calculate the `rotateX` value for the current `image`
          rotateXMax = rotateXMaxList[index]
          rotateX = rotateXMax - (rotateXMax * intersectionRatio)
          rotateX = rotateX.toFixed(2)
          // Calculate the `rotateY` value for the current `image`
          rotateYMax = rotateYMaxList[index]
          rotateY = rotateYMax - (rotateYMax * intersectionRatio)
          rotateY = rotateY.toFixed(2)
          // Calculate the `translateX` value for the current `image`
          if (windowWidth > 760) {
            translateX = translateXMax - (translateXMax * easeOutQuad(intersectionRatio))
            translateX = translateX.toFixed(2)
          } else {
            translateX = 0
          }
          // Invert `rotateX` and `rotateY` values in case the image is below the center of the viewport
          // Also update `translateX` value, to achieve an alternating effect
          if (intersectionRatioValue < 0) {
            rotateX = -rotateX
            rotateY = -rotateY
            translateX = index % 2 ? -translateX : 0
          } else {
            translateX = index % 2 ? 0 : translateX
          }
          // Set the CSS `transform`, using calculated values
          setTransform(image, 'perspective(500px) translateX('+ translateX +'px) rotateX('+ rotateX +'deg) rotateY('+ rotateY +'deg)')
        })
      }
      

      Init Animation

      And we are almost ready to enjoy our animation. We only need to make the initial call to the setupAnimation function, in addition to updating the dimensions in case the resize event is triggered:

      // Listen for `resize` event to recalculate dimensions
      window.addEventListener('resize', setupAnimation)
      
      // Initial setup
      setupAnimation()
      

      Scroll! Scroll! Scroll!

      Fixing the Jump Issue for Mobile Devices

      So far, everything should work perfectly on the desktop, but the story is very different if we try the animation on mobile devices.

      The problem occurs when the address bar (and the navigation bar in the footer of some browsers) hides when it is scrolled down and is shown again when scrolling upwards. This is not a bug, but a feature. The problem appears when we use the CSS unit vh, since in this process that unit is recalculated, resulting in an unwanted jump in our animation.

      The workaround that we have implemented is to use the small library vh-fix, which defines, for each element with the class .vh-fix, a static height based on their vh value and the viewport height.

      In this way, we should no longer have unwanted jumps.

      Conclusions

      And we have finished implementing this beautiful scroll-based animation.

      You can check the live demo, play with the code in Codepen, or check the full code in the repository in Github.

      Please keep in mind that the objective of this tutorial is essentially academic and to be used as inspiration. To use this demo in production other aspects must be taken into account, such as accessibility, browser support, the use of some debounce function for the scroll and resize events, etc.

      Without further ado, we hope you enjoyed it and it has been useful!

      Credits



      Source link