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
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
- Node.js 8.0 or greater.
- NPM 3.0 or greater.
- Yarn package manager
- Git
- FireLiners Github Repository Code
- An intermediate level of ReactJS knowledge.
- Resource Assets for This Tutorial
- Keen interest and patience.
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/...
- data/
- components/
- Header/
- index.js
- logo.svg
- Header/
- containers/
- App/
- App.test.js
- index.js
- constants.js
- reducer.js
- App/
- 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
- AddLine /
- services/
- DataService/
- index.js
- DataService/
- index.js
- registerServiceWorker.js
- assets/
- 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.
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
}
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 tothis.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 callthis.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 theendMessage
(more on this below). We set this value equal tothis.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.
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
.
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.
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 InfiniteScroll
component and also the newly created Feed
compone