Part 5: Build a CSS-in-JS React App with Styled Components and Priceline Design System

@creatrixity · 2018-06-06 17:41 · utopian-io

GIF animation of the Infinite Scroll in action

Currently, we live in the era of dwindling patience. We expect our meals available quickly, so we visit the local fast food drive through for takeouts. We schedule automated order purchases with tools like Alexa and Google Home and sometimes, even skip the bus to visit our destinations with on-demand services like Uber and Lyft.

As a result, we've adopted a set of behavioral patterns that shape our expectations when using applications. Studies have shown that users are willing to abandon a site or app within 6 seconds of perceived non-responsiveness.

What does this mean for us as application developers? It means we either get our sites or apps faster or we lose users. It's really important to note that users tend to be more interested in apps that seem fast even if they may not be orders of magnitude faster than the next guy's app.

Today, we'll explore a few techniques for improving perceived responsiveness for apps. We'll be looking at a few such techniques today (even though there are much more sophisticated and code intensive techniques out there). We'll use a technique that is employed by some of the most popular apps available today. Stay with us to find out more.

Disclaimer:

This tutorial may be pretty challenging for a first time reader of this series. I sincerely recommend that visitors get at least a cursory gaze at the previous articles in this series for easier comprehension. Links to the previous articles are available at the end of this tutorial.

Repository

React Github Repository

Tutorial Repository

Difficulty

  • Advanced

What Will I Learn?

By the time we get to the end of this tutorial, you should be able to apply these techniques to your React apps:

  • Implementing route based content filtering using React Router.
  • Use JSON data as an in-app data store. We'll be working extensively with JSON.
  • Improving content loading times by delegating to Infinite Scroll.
  • Hooking Infinite Scroll to our Redux store using sagas.

Requirements

Brief Introduction.

In the overview above, we briefly described our problem space and we also clearly outlined the techniques we hope to use to address said problems. Let's address the first technique.

Infinite Scroll:

Infinite scroll is a very popular performance technique for when you have a really long list of items to display to the user. It's nearly ubiquitous these days. It's currently employed in the most popular web and mobile apps available today.

How does infinite scroll work? Well, its principle is really simple. You simply detect when you're scrolling closer to the bottom of the screen and then you issue a request for additional information. It's main drawback usually, is the loss of awareness upon destruction of the app instance.

You can determine the position of the scroll by running calculations in the window.onScroll handler. You may also use the shiny new IntersectionObserver API that makes handling this way easier than the way it was done in the past.

To save on time, we'll be using the wonderful react-infinite-scroll-component package. You can get it installed by running

npm install --save react-infinite-scroll-component

With this React component now available, we should give ourselves a brief refreshing of the current app structure we're employing.

  • fire-liners/

    • config/...
    • node_modules/...
    • public/...
    • scripts/...
    • src/
      • assets/
        • data/
          • authors.json
          • liners.json
        • img/...
      • components/
        • Header/
          • index.js
          • logo.svg
      • containers/
        • App/
          • App.test.js
          • index.js
          • constants.js
          • reducer.js
      • redux/
        • reducerInjector.js
        • reducers.js
        • sagas.js
        • store.js
      • screens/
        • AddLine /
          • index.js
          • actions.js
        • Home /
          • index.js
          • constants.js
          • actions.js
        • Loading /
          • index.js
      • services/
        • DataService/
          • index.js
      • index.js
      • registerServiceWorker.js
    • package.json

Important: Before we proceed, please remember to download the fireliners-resources.zip file (listed under requirements) above that contains some resources we'll be using for our tutorial. If you've done that, extract the contents of the assets folder within the fire-liners.zip to the src/assets directory.

We'll be refactoring our Data Service to get it ready for infinite scrolling.

Refactoring the Data Service.

Currently, our data service methods retrieve either all the data within our liners.json or just a particular item within the liners.json file. Our infinite scrolling should only load five items at any particular point in time.

getLinersData method shot

Our new getLinersData method accepts a config object. If we specify an id property within the config object, we return only one item. We need to keep track of the data we'll be selecting. For instance, we might need to request the first set of items within the range (1-5), then from ranges (5-10), you know stuff like that. We keep track using the linersSetIndex variable. The values within this variable would usually be 1, 2, 3...

Finally, we use the linersData.slice method to 'select' out the items we're interested in.

export const getLinersData = (config) => {
    // If we only need one item from our liners data.
    if (config.id) {
        let liner = linersData.filter(liner => liner.id === config.id);
        return new Promise(resolve => resolve(liner));
    }

    // We track the current set of liners we'd like to load through this variable.
    let linersSetIndex = config.linersSetIndex ? config.linersSetIndex : 0;

    // Since we're only loading 5 items at a time.
    let resultsIndex = (linersSetIndex * 5);

    // We select the range of items we're interested in.
    // Could be all items for index 5 to index 10
    let data = linersData.slice(resultsIndex, resultsIndex + 5)

    return new Promise(resolve => resolve(data));
};

Also, we should create a method that returns the total number of items in our liners.json file.

export const getLinersTotal = () => linersData.length;

Adding Infinite Scroll Functionality.

We'll need to modify our HomeScreen component to support infinite scrolling. We'll add two dependencies to the top of the src/screens/Home class. We're simply importing the InfiniteScroll component and the getLinersTotal method from the data service.

import InfiniteScroll from 'react-infinite-scroll-component';
import { getLinersTotal } from '../../services/DataService';

Next, we set up our constructor method. We define some local state for this component. We set the previously mentioned linersSetIndex property to default to 0. We also need a means of checking to see if we've still got more items that we're yet to load. We can track that by using the boolean hasMore property that we'll set to true by default.

class Home extends Component {
    constructor(props) {
            // ...Previous code here.
            this.state = {
                linersSetIndex: 0,
                hasMoreItems: true,
                linersTotal: 0
        }
    }
    //... More code here
}

Home ComponentDidMount code shot

We also need to run some tasks when the component mounts. We first check to see if we've got any liners in the Redux store. If we do, then we don't need to run any other tasks. If that's not the case, we get the total number of liners available to us (in this case, 30 liners). We then keep the value returned by the getLinersTotal method in the local state.

Next, we need to check if we already have the maximum number of liners available so we can skip making an unnecessary request. If that's not the case we proceed to call the this.props.fetchLiners method to get a fresh set of liners.

    componentDidMount() {
        if (this.props.liners.length) return;

        let linersTotal = getLinersTotal();

        this.setState({
            linersTotal
        })

        if (this.props.liners.length >= linersTotal) return;

        this.props.fetchLiners({
            linersSetIndex: this.state.linersSetIndex
        })
    }

In our render method, we'll use the InfiniteScroll component. We'll supply some attributes to this component. Let's go through them.

  • dataLength: We'll use this prop to tell our Infinite Scroll component the number of items we have available. In this case we set its value equal to this.props.liners.length

  • next: We can use this prop to specify the method we'd like the component to call when it needs to load our next set of items. In this case, we'd like it to call this.fetchMoreData method that we'll define shortly.

  • hasMore: We use this prop to tell the component we have more items available. If this value is set to false. The component displays the endMessage (more on this below). We set this value equal to this.state.hasMoreItems (defined above).

  • loader: This is set to some markup that displays before new content is loaded.

  • endMessage: This is set to some markup that displays whenever there are no more items to load. In our case, we simply show a bold centered paragraph. You could display a loading animation if you wish.

    render() {
        return (
            

              
                  Recent Quotes

                  Loading...}
                    endMessage={
                      

Homie, you done seen all the liners we got.

}> {/* Our Liners will show here */}
) }

The rest of the render method is unchanged. All we simply need to do is wrap the previously existing markup in the component.

                  {
                      this.props.liners.length > 0 &&
                      this.props.liners.sort((a, b) => a.id < b.id).map((liner, index) => (
                      
                          
                              
                                  
                                    {this.getLinerAvatar(liner) &&
                                    
                                    }
                                  
                              
                              
                                  
                                      {liner.body}
                                  
                                  
                                      {liner.author}
                                  
                              

                          
                      
                  ))}

We now need to define the fetchMoreData method. We'll simulate production app conditions. Usually, a roundtrip to the server could take as much as 1500 milliseconds (I'm being hopeful here). We'll set a timeout of 1500 milliseconds then we'll carry out some tasks.

Firstly, we'll increment the state property linersSetIndex by 1. We'll also check if the total number of liners loaded is less than the total number of all liners available and we update the hasMoreItems property to true if our condition is true and false otherwise.

Last of all, we run check to see if they're more items available to load. If that's the case, we call this.props.fetchLiners to get more items from the data service.

    fetchMoreData = () => {
      // a fake async api call like which sends
      // 20 more records in 1.5 secs
      setTimeout(() => {

          this.setState({
            linersSetIndex: this.state.linersSetIndex + 1,
            hasMoreItems: this.props.liners.length < this.state.linersTotal
          });

          if (this.state.hasMoreItems) {
              this.props.fetchLiners({
                  linersSetIndex: this.state.linersSetIndex
              })
          }
      }, 1500);
    };

Setting up Our Reducers

We'll need to modify our reducer to accommodate infinite scrolling. Previously, we simply set the liners properties to the array returned by the data whenever the SET_LINERS action was detected. We have to change its reducer to one that adds a new set of items instead of just replacing it.

Let's do just that. Let's open up src/containers/app/reducer.js and take a look at it.

A look at the App's reducer

We'll need to modify it to look like this. We're simply setting the liners property to an array that comprises of the previous liners and the new set of liners. We use the fromJS method to make this an ImmutableJS record.

    case SET_LINERS_DATA:
        return state.set('liners', fromJS([...state.get('liners'), ...action.payload.data]))

Congrats! We've completed our Infinite Scroll feature. We can test it out by running npm start and visit http://localhost: 3000.

GIF animation of the Infinite Scroll in action

Implementing route based content filtering using React Router.

Usually, in most applications, we have multiple routes to serve different content types. These routes are usually passed parameters that influence how its going to work. We'd love to be able to filter liners by authors. For instance, if we hit /authors/Eminem we should see only liners by Eminem. We'll need a new screen component, the Author screen to help us display liners for an author.

We'll also need to refactor our code so we can reuse the infinite scroll feature in our Author screen.

Refactoring our Code for Maximum Reuse

We'll need to extract our code into a new component so we can share it between different component. We'll be extracting our code that displays our liners into the Feed component we'll be creating soon. Create src/components/Feed/index.js and we'll get to work. We'll import our dependencies similar to what exists at the code for the Home screen. We'll also be defining the Circle component here.

GetLinerAuthor method shot

We need a way to be able to get information about the creator of a liner. We'll use the utility function getLinerAuthor to get the author information for a liner. It's a really simple method, we just go through every author and we only return an author if that author is the author of the liner.

import React from 'react';
import {
    Box,
    Image,
    Flex,
    Link,
    Text
} from 'pcln-design-system';
import styled from 'styled-components';

const Circle = styled(Flex)`
    border-radius: 50px;
    width: 45px;
    height: 45px;
`;

const getLinerAuthor = (liner, authors) => authors.filter(author => author.name === liner.author)[0]

We then define the stateless Feed component that is in reality, nothing but a wrapper around our previous code at the Home screen for displaying our liners. Our Feed component is in reality a "pure" function that accepts props and returns some JSX markup. Our JSX markup simply displays the name & photo of the liner's author alongside the liner.

export const Feed = (props) => {
    return props.liners.length > 0 &&
            props
            .liners
            .sort()
            .map((liner, index) => (
                    
                        
                            
                                
                                  {getLinerAuthor(liner, props.authors).photo &&
                                  
                                  }
                                
                            
                            
                                
                                    {liner.body}
                                
                                
                                    {liner.author}
                                
                            

                        

                    
                )
            )
}

export default Feed;

Building the Author Screen.

Let's create src/screens/author/index.js and add some code. We'll reuse some of our code for the Home screen. We'll import the InfiniteScrollcomponent and also the newly created Feed compone

#utopian-io #tutorials #programming #technology #coding
Payout: 0.000 HBD
Votes: 164
More interactions (upvote, reblog, reply) coming soon.