Adding Loading Spinner & Variable Page Size to Our News App ⏳🔢
Right now, when we click Previous or Next, the page number changes instantly —
but in a real app, fetching new articles from an API takes time. Users need visual feedback
that something is happening. On top of that, a fixed pageSize of 6 articles isn't
very flexible — some users may want to see more articles per page, others fewer.
In this tutorial, we will learn:
Why a loading spinner improves user experience
Adding a loading flag to component state
Creating a reusable Spinner component
Simulating network delay with setTimeout
Letting users choose a custom pageSize with a dropdown
Resetting to page 1 whenever the page size changes
---
Why Do We Need a Loading Spinner?
Whenever our app fetches new articles — whether by clicking Next, Previous,
or changing the page size — there's a short delay before the data is ready. Without any
feedback, the screen looks frozen and users might click the button multiple times.
A loading spinner tells the user "hang on, we're getting your articles" and also
lets us disable the buttons while a request is in progress.
---
Step 1: Create a Spinner Component
Let's create a small, reusable Spinner.js component using Bootstrap's spinner classes.
import React from "react";
import loadingGif from "./loading.gif";
const Spinner = () => {
return (
<div className="text-center my-4">
<img src={loadingGif} alt="loading" style={{ width: "80px" }} />
<h5>Loading articles...</h5>
</div>
);
};
export default Spinner;
---
Step 2: Add loading and pageSize to State
Open News.js and update the state to include a loading flag.
We'll keep pageSize as a prop-driven default, but make it changeable via state so the
user can pick their own value.
import React, { Component } from "react";
import NewsItem from "./NewsItem";
import Spinner from "./Spinner";
export class News extends Component {
static defaultProps = {
pageSize: 6,
totalResults: 30,
};
state = {
articles: [],
page: 1,
loading: false,
pageSize: this.props.pageSize,
};
render() {
return (
<div className="container my-4">
<h2 className="text-center mb-4">Welcome to News App 📰</h2>
</div>
);
}
}
export default News;
---
Step 3: Simulate a Delay with setTimeout
Real API calls take time, so let's write a helper method fetchArticles that turns
loading on, waits a moment using setTimeout (standing in for our future API call),
and then turns loading back off.
fetchArticles = () => {
this.setState({ loading: true });
// Simulated network delay — will be replaced by a real API call later
setTimeout(() => {
this.setState({ loading: false });
}, 1000);
};
---
Step 4: Update handlePrevClick and handleNextClick
Now let's call fetchArticles every time the page changes, so the spinner shows up
during the "fetch".
handlePrevClick = () => {
this.setState({ page: this.state.page - 1 }, () => {
this.fetchArticles();
});
};
handleNextClick = () => {
this.setState({ page: this.state.page + 1 }, () => {
this.fetchArticles();
});
};
---
Step 5: Add a Page Size Selector
Let's give users a dropdown to choose how many articles they want per page. Whenever the
page size changes, we reset page back to 1 so the pagination stays consistent.
handlePageSizeChange = (e) => {
const newSize = Number(e.target.value);
this.setState({ pageSize: newSize, page: 1 }, () => {
this.fetchArticles();
});
};
And here's the dropdown JSX we'll place near the top of our articles list:
<div className="d-flex justify-content-end mb-3">
<label className="me-2 align-self-center fw-bold">Articles per page:</label>
<select
className="form-select w-auto"
value={this.state.pageSize}
onChange={this.handlePageSizeChange}
>
<option value="3">3</option>
<option value="6">6</option>
<option value="9">9</option>
<option value="12">12</option>
</select>
</div>
---
Step 6: Render the Spinner Conditionally
Inside render(), we check this.state.loading. If it's true, we show the
Spinner instead of the articles. We also disable the Previous/Next buttons while loading.
render() {
const { totalResults } = this.props;
const { page, pageSize, articles, loading } = this.state;
return (
<div className="container my-4">
<h2 className="text-center mb-4">Welcome to News App 📰</h2>
<div className="d-flex justify-content-end mb-3">
<label className="me-2 align-self-center fw-bold">Articles per page:</label>
<select
className="form-select w-auto"
value={pageSize}
onChange={this.handlePageSizeChange}
>
<option value="3">3</option>
<option value="6">6</option>
<option value="9">9</option>
<option value="12">12</option>
</select>
</div>
{loading ? (
<Spinner />
) : (
articles.map((article, index) => (
<NewsItem
key={index}
title={article.title}
description={article.description}
imageUrl={article.urlToImage}
newsUrl={article.url}
/>
))
)}
{/* Previous & Next Buttons */}
<div className="d-flex justify-content-between my-4">
<button
className="btn btn-warning"
onClick={this.handlePrevClick}
disabled={loading || page <= 1}
>
← Previous
</button>
<span className="align-self-center fw-bold">
Page {page} of {Math.ceil(totalResults / pageSize)}
</span>
<button
className="btn btn-warning"
onClick={this.handleNextClick}
disabled={loading || page >= Math.ceil(totalResults / pageSize)}
>
Next →
</button>
</div>
</div>
);
}
---
Step 7: Full Updated News.js
Here is the complete News.js file with the loading spinner and variable page size wired together.
import React, { Component } from "react";
import NewsItem from "./NewsItem";
import Spinner from "./Spinner";
export class News extends Component {
static defaultProps = {
pageSize: 6,
totalResults: 30,
};
state = {
articles: [
{
title: "React 19 Released",
description: "React 19 brings exciting improvements for developers.",
urlToImage: "",
url: "#",
},
{
title: "GitHub Pages Hosting Made Easy",
description: "Deploy your React app for free using GitHub Pages.",
urlToImage: "",
url: "#",
},
{
title: "JavaScript ES2025 Features",
description: "Explore the newest JavaScript features landing in 2025.",
urlToImage: "",
url: "#",
},
],
page: 1,
loading: false,
pageSize: this.props.pageSize,
};
fetchArticles = () => {
this.setState({ loading: true });
setTimeout(() => {
this.setState({ loading: false });
}, 1000);
};
handlePrevClick = () => {
this.setState({ page: this.state.page - 1 }, () => {
this.fetchArticles();
});
};
handleNextClick = () => {
this.setState({ page: this.state.page + 1 }, () => {
this.fetchArticles();
});
};
handlePageSizeChange = (e) => {
const newSize = Number(e.target.value);
this.setState({ pageSize: newSize, page: 1 }, () => {
this.fetchArticles();
});
};
render() {
const { totalResults } = this.props;
const { page, pageSize, articles, loading } = this.state;
return (
<div className="container my-4">
<h2 className="text-center mb-4">Welcome to News App 📰</h2>
<div className="d-flex justify-content-end mb-3">
<label className="me-2 align-self-center fw-bold">Articles per page:</label>
<select
className="form-select w-auto"
value={pageSize}
onChange={this.handlePageSizeChange}
>
<option value="3">3</option>
<option value="6">6</option>
<option value="9">9</option>
<option value="12">12</option>
</select>
</div>
{loading ? (
<Spinner />
) : (
articles.map((article, index) => (
<NewsItem
key={index}
title={article.title}
description={article.description}
imageUrl={article.urlToImage}
newsUrl={article.url}
/>
))
)}
<div className="d-flex justify-content-between my-4">
<button
className="btn btn-warning"
onClick={this.handlePrevClick}
disabled={loading || page <= 1}
>
← Previous
</button>
<span className="align-self-center fw-bold">
Page {page} of {Math.ceil(totalResults / pageSize)}
</span>
<button
className="btn btn-warning"
onClick={this.handleNextClick}
disabled={loading || page >= Math.ceil(totalResults / pageSize)}
>
Next →
</button>
</div>
</div>
);
}
}
export default News;
---
How the New State Values Work
| State Value |
Purpose |
Changes When |
loading |
Controls whether the Spinner or the article list is shown |
Before/after every simulated fetch |
pageSize |
Determines how many articles are shown per page and how total pages are calculated |
When the user picks a new value from the dropdown |
page |
Tracks which page of results is currently visible |
On Previous/Next clicks, or reset to 1 when pageSize changes |
---
Features and Learnings:-
Understood why loading feedback matters for a smooth user experience.
Created a reusable Spinner component.
Added a loading flag to state and toggled it around a simulated fetch.
Disabled Previous/Next buttons while a "fetch" is in progress.
Made pageSize dynamic using a dropdown selector.
Reset page to 1 whenever the page size changes, keeping pagination accurate.
Prepared the app for the next step: replacing the simulated delay with a real News API call.