Infinite Scrolling in React with Redux and Redux-Saga (Part III: Performance & Architecture Improvements)

January 16, 2020 · 3 min read

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

More on this series:
Part IPart II

Introduction

This part focuses on improving the original implementation.

The main issue: the app was making multiple API calls to PokeAPI. The goal is to reduce that to a single request and handle the rest on the frontend.

Moving State to Redux

Previously, pagination state lived inside the component. Now it is moved to Redux to support:

  • Filtering
  • Sorting
  • Reusability across the app
const INCREMENT = "pokemon-frontend/filters/INCREMENT";

const filtersReducerDefaultState = {
  count: 20,
};

export default (state = filtersReducerDefaultState, action) => {
  switch (action.type) {
    case INCREMENT:
      return {
        ...state,
        count: state.count + 20,
      };
    default:
      return state;
  }
};

export const increment = () => ({
  type: INCREMENT,
});

Selectors

Selectors allow computing derived data from Redux state.

const pokemonListSelector = (state) =>
  state.pokemonListReducer.pokemonList;

const filterSelector = (state) => state.filterReducer;

export const pokemonListFilterSelector = createSelector(
  [pokemonListSelector, filterSelector],
  (pokemonList, { count }) => {
    return pokemonList.filter((pokemon) => pokemon.id <= count);
  },
);

This enables filtering without modifying the original state.

Improving the Reducer

case FETCH_POKEMON_LIST_SUCCESS:
  const { results } = action.payload.data;
  const pokemonResultsList = results.map((pokemon) => {
    const id = parseInt(getId(pokemon.url), 10);
    return { id, ...pokemon };
  });

  return {
    ...state,
    pokemonList: pokemonResultsList,
    isLoading: false,
  };

Now each Pokémon includes an id, making filtering easier and avoiding repeated parsing.

New Display Flow

Instead of fetching more data, we simulate loading:

function* displayMorePokemonSaga() {
  yield delay(400);
  yield put(displayMorePokemonEnd());
  yield put(increment());
}

This removes extra API calls and improves performance.

Updating the Component

const mapStateToProps = (state) => ({
  isLoading: state.pokemonListReducer.isLoading,
  error: state.pokemonListReducer.error,
  pokemonList: pokemonListFilterSelector(state),
});

We can now simplify the component using hooks:

useEffect(() => {
  fetchActionCreator();
}, [fetchActionCreator]);

const handleScroll = (event) => {
  const element = event.target;
  if (element.scrollHeight - element.scrollTop === element.clientHeight) {
    displayMore();
  }
};

Fetching All Data Once

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

This enables frontend-driven pagination.

Total Count Selector

export const pokemonListCount = createSelector(
  [pokemonListSelector],
  (pokemonList) => pokemonList.length
);

Usage:

<p>
  Displaying {pokemonList.length} pokemon of {totalPokemonCount}
</p>

Prevent Over-fetching

if (
  element.scrollHeight - element.scrollTop === element.clientHeight &&
  totalPokemonCount > pokemonList.length
) {
  displayMore();
}

Wrap-up

Key improvements:

  • Only one API call
  • Better state architecture
  • Cleaner UI logic
  • Scalable filtering system

This approach shifts complexity to the frontend, reducing backend dependency and improving performance.