Infinite Scrolling in React with Redux and Redux-Saga (Part I: Setup)

October 14, 2019 · 4 min read

Update — 2026-04-04: This post has been updated to improve clarity and structure. Key changes include clearer explanations of Redux flow, improved formatting, and better code readability.

More on this series:
Part IIPart III

Introduction

This tutorial walks through building the foundation for infinite scrolling using Redux and Redux-Saga.

The example uses a Pokédex-style app to demonstrate how to handle large datasets (700+ Pokémon) using paginated API requests.

Setup

Create a new React app and install dependencies:

yarn add redux react-redux redux-logger redux-saga

Redux Module Structure

mkdir src/redux
mkdir src/redux/modules
touch src/redux/modules/pokemonList.js

Actions and Initial State

const FETCH_POKEMON_LIST = "pokemon-frontend/pokemon/FETCH_POKEMON_LIST";
const FETCH_POKEMON_LIST_SUCCESS =
  "pokemon-frontend/pokemon/FETCH_POKEMON_LIST_SUCCESS";
const FETCH_POKEMON_LIST_FAILURE =
  "pokemon-frontend/pokemon/FETCH_POKEMON_LIST_FAILURE";

const LOAD_MORE_POKEMON = "pokemon-frontend/pokemon/LOAD_MORE_POKEMON";
const LOAD_MORE_POKEMON_SUCCEED =
  "pokemon-frontend/pokemon/LOAD_MORE_POKEMON_SUCCEED";
const LOAD_MORE_POKEMON_FAILED =
  "pokemon-frontend/pokemon/LOAD_MORE_POKEMON_FAILED";

const initialState = {
  pokemonList: [],
  isLoading: false,
  error: "",
};

We define two groups of actions:

  • Initial fetch
  • Load more (triggered on scroll)

Reducer

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case FETCH_POKEMON_LIST:
    case LOAD_MORE_POKEMON:
      return {
        ...state,
        isLoading: true,
      };

    case FETCH_POKEMON_LIST_SUCCESS:
      return {
        ...state,
        pokemonList: action.payload.data.results,
        isLoading: false,
      };

    case LOAD_MORE_POKEMON_SUCCEED:
      return {
        ...state,
        pokemonList: [...state.pokemonList, ...action.payload.data.results],
        isLoading: false,
      };

    case FETCH_POKEMON_LIST_FAILURE:
    case LOAD_MORE_POKEMON_FAILED:
      return {
        ...state,
        error: action.payload,
        isLoading: false,
      };

    default:
      return state;
  }
}

The key detail: new results are appended, not replaced.

Action Creators

export function loadPokemonList() {
  return { type: FETCH_POKEMON_LIST };
}

export function loadPokemonListSucceed(payload) {
  return { type: FETCH_POKEMON_LIST_SUCCESS, payload };
}

export function loadPokemonListFailed(payload) {
  return { type: FETCH_POKEMON_LIST_FAILURE, payload };
}

export function loadMorePokemon(payload) {
  return { type: LOAD_MORE_POKEMON, payload };
}

export function loadMorePokemonSucceed(payload) {
  return { type: LOAD_MORE_POKEMON_SUCCEED, payload };
}

export function loadMorePokemonFailed(payload) {
  return { type: LOAD_MORE_POKEMON_FAILED, payload };
}

Sagas

import { call, delay, put, takeEvery, takeLatest } from "redux-saga/effects";

export function* fetchPokemonListSaga() {
  try {
    const response = yield call(getPokemonList);
    yield put(loadPokemonListSucceed(response));
  } catch (error) {
    yield put(loadPokemonListFailed(error.message));
  }
}

export function* loadMorePokemonListSaga(action) {
  try {
    const response = yield call(loadMorePokemonList, action.payload);
    yield delay(1000);
    yield put(loadMorePokemonSucceed(response));
  } catch (error) {
    yield put(loadMorePokemonFailed(error.message));
  }
}

export function* pokemonListWatcherSaga() {
  yield takeLatest(FETCH_POKEMON_LIST, fetchPokemonListSaga);
  yield takeEvery(LOAD_MORE_POKEMON, loadMorePokemonListSaga);
}

Key Concepts

  • call: executes async requests
  • put: dispatches actions
  • takeLatest: cancels previous requests
  • takeEvery: runs all requests (important for scroll behavior)

Root Setup

Root Saga

import { all } from "redux-saga/effects";
import { pokemonListWatcherSaga } from "./modules/pokemonList";

export default function* rootSaga() {
  yield all([pokemonListWatcherSaga()]);
}

Root Reducer

import { combineReducers } from "redux";
import pokemonListReducer from "./modules/pokemonList";

export default combineReducers({
  pokemonListReducer,
});

Store Configuration

import { createStore, applyMiddleware } from "redux";
import { createLogger } from "redux-logger";
import createSagaMiddleware from "redux-saga";

import rootReducer from "./rootReducer";
import rootSaga from "./rootSaga";

export default function configureStore(initialState = {}) {
  const middlewares = [];

  if (process.env.NODE_ENV === "development") {
    middlewares.push(createLogger({ collapsed: true }));
  }

  const sagaMiddleware = createSagaMiddleware();
  middlewares.push(sagaMiddleware);

  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(...middlewares),
  );

  sagaMiddleware.run(rootSaga);

  return store;
}

API Layer

yarn add axios humps
mkdir src/api
touch src/api/axiosInstance.js
touch src/api/pokemonEndpoints.js

Axios Instance

import axios from "axios";
import humps from "humps";

const axiosInstance = axios.create({
  baseURL: "https://pokeapi.co/api/v2/",
  transformResponse: [
    ...axios.defaults.transformResponse,
    (data) => humps.camelizeKeys(data),
  ],
  transformRequest: [
    (data) => humps.decamelizeKeys(data),
    ...axios.defaults.transformRequest,
  ],
});

export default function api(method, url, data = {}, options = {}) {
  const httpMethod = method.toLowerCase();
  const hasData = ["post", "put", "patch"].includes(httpMethod);

  return hasData
    ? axiosInstance[httpMethod](url, data, options)
    : axiosInstance[httpMethod](url, data);
}

Endpoints

import API from "./axiosInstance";

export const getPokemonList = () => {
  return API("get", "/pokemon/?offset=0&limit=20");
};

export const loadMorePokemonList = (offset) => {
  return API("get", `/pokemon/?offset=${offset}&limit=20`);
};

Wrap-up

This setup gives a solid foundation for:

  • Handling paginated APIs
  • Managing async flows with Redux-Saga
  • Supporting infinite scrolling

In Part II, the focus will shift to building the UI and connecting everything together.