Infinite scrolling using redux and sagas, Part II.
October 16, 2019 - 8 min readMore on this series: Part I ⋮ Part III
For this part we’ll be focusing on the component setup, and the srolling part and fetching data when the scroll is at the bottom.
Our component structure will be the following:
-
Provider (React-redux wrapper.)
-
PokemonList (Component with scrolling logic and api calls)
- PokemonListItem (Stateless component just for displaying the pokemon)
-
We will add the following dependencies as well, one is for having a content loader for the first time load, second it bootstrap for its awesome grid system, lodash is for the ease of validating if the redux array is empty and node sass to have the bootstrap core import in our scss file.
yarn add react-content-loader bootstrap lodash node-sass
We will rename our app.css
to app.scss
and we will add this import at the beginning, now with this require we will be able to use the bootstrap grid and core components.
@import "~bootstrap/scss/bootstrap";
When we have this ready lets create a new file for the PokemonList component
touch src/components/PokemonList.js
First we’ll start connecting redux with the component, the component will dispatch two redux actions creators loadPokemonList
and loadMorePokemon
also we will set an state for our component which is going to keep count of the pagination, to send the params to our endpoint
import _ from "lodash";
import React, { Component } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { loadPokemonList, loadMorePokemon } from "../redux/modules/pokemonList";
class PokemonList extends Component {
constructor(props) {
super(props);
this.state = {
currentCount: 20,
};
}
}
const mapStateToProps = state => ({
isLoading: state.pokemonListReducer.isLoading,
error: state.pokemonListReducer.error,
pokemonList: state.pokemonListReducer.pokemonList,
});
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
fetchActionCreator: loadPokemonList,
loadMoreActionCreator: loadMorePokemon,
},
dispatch,
);
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(PokemonList);
Notice how we are adding the redux state, to handle all the use cases of our API, for example the loading and error attributes. Also we use bindActionCreators function to define the name of our action creators in the component, those will be available in the component props.
Now we are going to add the first fetch on the componentDidMount
because we want to perform the fetch when our component mounts.
componentDidMount() {
const { fetchActionCreator } = this.props;
fetchActionCreator();
}
As I mentioned, I rename the loadMorePokemon to fetchActionCreator
and it is available in the props so we’re just calling that function inside the componentDidMount
. This will trigger all the redux flow that will either bring a success response, or return an error message.
So to handle the initial load, I’m going to create a new component that is going to use the library react-content-loader
so the user we’ll see a content loader on screen
touch src/components/ListItemLoader.js
Please check the docs if you have trouble reading this component
import React from "react";
import ContentLoader from "react-content-loader";
const ListItemLoader = () => {
return (
<ContentLoader
height={507}
width={900}
speed={2}
primaryColor="#f3f3f3"
secondaryColor="#ecebeb"
>
<rect x="30" y="20" rx="0" ry="0" width="130" height="23" />
<rect x="30" y="60" rx="0" ry="0" width="200" height="120" />
<rect x="30" y="189" rx="0" ry="0" width="200" height="15" />
<rect x="30" y="211" rx="0" ry="0" width="140" height="15" />
<rect x="243" y="60" rx="0" ry="0" width="200" height="120" />
<rect x="243" y="189" rx="0" ry="0" width="200" height="15" />
<rect x="243" y="211" rx="0" ry="0" width="140" height="15" />
<rect x="455" y="60" rx="0" ry="0" width="200" height="120" />
<rect x="455" y="189" rx="0" ry="0" width="200" height="15" />
<rect x="455" y="211" rx="0" ry="0" width="140" height="15" />
<rect x="667" y="60" rx="0" ry="0" width="200" height="120" />
<rect x="667" y="188" rx="0" ry="0" width="200" height="15" />
<rect x="667" y="209" rx="0" ry="0" width="140" height="15" />
<rect x="30" y="280" rx="0" ry="0" width="130" height="23" />
<rect x="30" y="320" rx="0" ry="0" width="200" height="120" />
<rect x="30" y="450" rx="0" ry="0" width="200" height="15" />
<rect x="30" y="474" rx="0" ry="0" width="140" height="15" />
<rect x="243" y="320" rx="0" ry="0" width="200" height="120" />
<rect x="455" y="320" rx="0" ry="0" width="200" height="120" />
<rect x="667" y="320" rx="0" ry="0" width="200" height="120" />
<rect x="243" y="450" rx="0" ry="0" width="200" height="15" />
<rect x="455" y="450" rx="0" ry="0" width="200" height="15" />
<rect x="667" y="450" rx="0" ry="0" width="200" height="15" />
<rect x="243" y="474" rx="0" ry="0" width="140" height="15" />
<rect x="455" y="474" rx="0" ry="0" width="140" height="15" />
<rect x="667" y="474" rx="0" ry="0" width="140" height="15" />
</ContentLoader>
);
};
export default ListItemLoader;
Now we will modify our PokemonList
component to display this new component when we have do our initial load.
render() {
const { isLoading, error, pokemonList } = this.props;
if (_.isEmpty(pokemonList) && isLoading) return <ListItemLoader />;
if (error) return <p>Error</p>;
return (
<div>
{pokemonList.length}
</div>
)
}
Here we’re using the props from redux, notice that we are going to show the ListItemLoader just on the first load, when we implement the scrolling we will use something else, we also have an error if just in case something happens and we’re just returning the array length if we get the correct response.
Now we’ll modify the App.js
component to add the Provider wrapper and our newly created component.
import React from 'react';
import { Provider } from "react-redux"
import configureStore from "./redux/configureStore";
import './App.scss';
import PokemonList from './components/PokemonList';
const store = configureStore();
function App() {
return (
<Provider store={store}>
<div className="container">
<PokemonList />
</div>
</Provider>
);
}
export default App;
Here we’re just wrapping our code in the Provider and using the store function that we just created.
Now we should see something like this on the initial load and our count afterwards should be 20 because that’s what we defined in the endpoint:
Pretty cool right, now let’s do the logic for our scrolling, this was taken from an example from this post a condition that checks if our scroll has reach the end of the container where it belongs.
handleScroll = event => {
const { loadMoreActionCreator } = this.props;
const { currentCount } = this.state;
const element = event.target;
if (element.scrollHeight - element.scrollTop === element.clientHeight) {
loadMoreActionCreator(currentCount);
this.setState({
currentCount: currentCount + 20,
});
}
};
If we are at the end the condition is met and we will trigger the loadMoreActionCreator
that will request for more pokemon and we will increment the currenCount by 20, so if we go to the bottom of the container again we will fetch for more pokemon. now that we have everything, our render method should look like this.
render() {
const { isLoading, error, pokemonList } = this.props;
if (_.isEmpty(pokemonList) && isLoading) return <ListItemLoader />;
if (error) return <p>Error</p>;
return (
<div className="border m-5">
<div
className="row"
onScroll={this.handleScroll}
style={{ height: "500px, overflow: "auto" }}
>
{pokemonList.map(pokemon => {
const { url, name } = pokemon;
const id = getId(url);
return (
<div key={pokemon.url} className="col-sm-3">
<PokemonListItem id={id} name={name} />
</div>
);
})}
</div>
{isLoading && (
<div className="text-center">
<div
className="spinner-border"
style={{ width: "4rem", height: "4rem" }}
role="status"
>
<span className="sr-only">Loading...</span>
</div>
</div>
)}
<p className="text-muted ml-3">Displaying {pokemonList.length} pokemon of 807</p>
</div>
)
}
There are a few thing happening, we created a main div
that has two div
one is the one that contains the <PokemonListItem>
which we will add later and the other one is to display a loading icon if the loading changes, which is the expected behavior if we scroll to the bottom of the div
since a new request will be triggered. get id is a helper that we will add as well. Lets do that touch src/helpers/pokemonUtils.js
export const getId = url => {
return url
.split("/")
.filter(el => !!el)
.pop();
};
This just takes the url attribute from the response data and it returns the id that is associated to it. Now the PokemonListItem
is a fairly easy component, it looks like this:
import _ from "lodash";
import React from "react";
const PokemonListItem = ({ id, name }) => {
return (
<>
<div>
<img
className="d-block mx-auto"
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
alt={name}
/>
</div>
<div className="text-center">
<p>{_.capitalize(name)}</p>
</div>
</>
);
};
export default PokemonListItem;
That’s why the getId method comes in handy is important because we will show the pokemon image which is available in github.
If you follow everything step by step you should see something like this:
So here it is, this is the way I figure out for fetching large datasets, 9gag uses a similar way to fetch its content and I think is a pretty awesome way to do it if you don’t want to add a paginator. This is the repo if you want to see all the implementation.
- ← Infinite scrolling using redux and sagas, Part I.
- Infinite scrolling using redux and sagas, Part III. →
Built and mantained by Jean Aguilar
Nobody likes you when you're twenty three