Build a Simple React Component for a Dynamic Progress Bar

In today’s fast-paced digital world, users expect immediate feedback. Whether it’s uploading a file, processing data, or loading content, a progress bar provides crucial visual cues, letting users know that something is happening and how long it might take. This simple, yet effective, UI element significantly enhances the user experience, reducing frustration and increasing engagement. In this tutorial, we’ll dive into building a dynamic progress bar component using React JS, perfect for beginners and intermediate developers alike.

Why Build a Custom Progress Bar?

While various UI libraries offer pre-built progress bar components, understanding how to build one from scratch offers several advantages:

  • Customization: You have complete control over the appearance, behavior, and functionality.
  • Learning: It’s an excellent way to grasp fundamental React concepts like state management, component composition, and prop drilling.
  • Optimization: You can tailor the component for specific performance needs, avoiding unnecessary overhead from larger libraries.

Prerequisites

Before we begin, ensure you have the following:

  • A basic understanding of HTML, CSS, and JavaScript.
  • Node.js and npm (or yarn) installed on your system.
  • A code editor (e.g., VS Code, Sublime Text).
  • Familiarity with React fundamentals (components, JSX, state, props).

Step-by-Step Guide

Let’s build our dynamic progress bar component. We’ll break it down into manageable steps, explaining each part along the way.

Step 1: Setting Up Your React Project

If you don’t have a React project set up already, create one using Create React App (CRA):

npx create-react-app progress-bar-tutorial
cd progress-bar-tutorial

This command creates a new React application named “progress-bar-tutorial” and navigates you into the project directory.

Step 2: Creating the Progress Bar Component

Create a new file named `ProgressBar.js` inside the `src` directory. This will house our progress bar component. Let’s start with a basic structure:

import React from 'react';

function ProgressBar({
  percentage,
  height = '10px',
  backgroundColor = '#eee',
  barColor = 'blue',
}) {
  const containerStyle = {
    width: '100%',
    height: height,
    backgroundColor: backgroundColor,
    borderRadius: '5px',
    overflow: 'hidden',
  };

  const fillerStyle = {
    width: `${percentage}%`,
    height: '100%',
    backgroundColor: barColor,
    transition: 'width 0.3s ease-in-out',
  };

  return (
    <div style={containerStyle}>
      <div style={fillerStyle}></div>
    </div>
  );
}

export default ProgressBar;

Let’s break down this code:

  • Import React: We import the React library.
  • ProgressBar Component: This is a functional component that accepts props.
  • Props:
    • `percentage`: A number representing the progress (0-100).
    • `height`: The height of the progress bar (defaults to ’10px’).
    • `backgroundColor`: The background color of the container (defaults to ‘#eee’).
    • `barColor`: The color of the progress bar itself (defaults to ‘blue’).
  • containerStyle: Defines the styling for the container div (the background).
  • fillerStyle: Defines the styling for the inner div (the colored progress bar). The width is dynamically set based on the `percentage` prop. The `transition` property adds a smooth animation.
  • Return: Returns the JSX for the progress bar, consisting of a container div and a filler div.

Step 3: Using the Progress Bar Component

Now, let’s use our `ProgressBar` component in `App.js`. Replace the existing content with the following:

import React, { useState, useEffect } from 'react';
import ProgressBar from './ProgressBar';

function App() {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setProgress((prevProgress) => {
        const newProgress = prevProgress + 1;
        return Math.min(newProgress, 100);
      });
    }, 20);

    return () => clearInterval(interval);
  }, []);

  return (
    <div style={{ padding: '20px' }}>
      <h2>Dynamic Progress Bar Example</h2>
      <ProgressBar percentage={progress} barColor="#4CAF50" height="20px" />
      <p>Progress: {progress}%</p>
    </div>
  );
}

export default App;

Here’s what this code does:

  • Import Statements: Imports `useState`, `useEffect` from React, and our `ProgressBar` component.
  • useState: `progress` state variable to hold the current progress value, initialized to 0.
  • useEffect: A side effect hook to update the progress value over time.
    • `setInterval`: Sets up an interval that calls a function every 20 milliseconds.
    • `setProgress`: Updates the `progress` state. It ensures the progress doesn’t exceed 100%.
    • `clearInterval`: Clears the interval when the component unmounts to prevent memory leaks.
  • JSX: Renders the `ProgressBar` component and displays the current progress percentage. We pass the `progress` state as the `percentage` prop, customize the `barColor` and `height`.

Step 4: Running the Application

Start the development server using the command:

npm start

This should open your application in a web browser (usually at `http://localhost:3000`). You should see a progress bar that gradually fills up from 0% to 100%.

Adding More Features and Customization

Our basic progress bar is functional, but let’s explore ways to enhance it.

Adding Labels

To display a label showing the percentage, modify the `ProgressBar.js` component:

import React from 'react';

function ProgressBar({
  percentage,
  height = '10px',
  backgroundColor = '#eee',
  barColor = 'blue',
  showLabel = true,
}) {
  const containerStyle = {
    width: '100%',
    height: height,
    backgroundColor: backgroundColor,
    borderRadius: '5px',
    overflow: 'hidden',
    position: 'relative', // Add this
  };

  const fillerStyle = {
    width: `${percentage}%`,
    height: '100%',
    backgroundColor: barColor,
    transition: 'width 0.3s ease-in-out',
  };

  const labelStyle = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    color: 'white',
    fontSize: '12px',
    fontWeight: 'bold',
  };

  return (
    <div style={containerStyle}>
      <div style={fillerStyle}></div>
      {showLabel && <span style={labelStyle}>{percentage}%</span>}
    </div>
  );
}

export default ProgressBar;

Changes:

  • Added a new prop `showLabel` which defaults to `true`.
  • Added `position: ‘relative’` to the `containerStyle` to enable absolute positioning of the label.
  • Added `labelStyle` for styling the label.
  • Conditionally render the label using `showLabel && <span>`.

Modify `App.js` to enable the label:

<ProgressBar percentage={progress} barColor="#4CAF50" height="20px" showLabel={true} />

Adding Different Styles

Create a few more styles to make the component more reusable.

function ProgressBar({
  percentage,
  height = '10px',
  backgroundColor = '#eee',
  barColor = 'blue',
  showLabel = true,
  borderRadius = '5px',
  styleType = 'default', // Add this
}) {
  const containerStyle = {
    width: '100%',
    height: height,
    backgroundColor: backgroundColor,
    borderRadius: borderRadius,
    overflow: 'hidden',
    position: 'relative',
  };

  const fillerStyle = {
    width: `${percentage}%`,
    height: '100%',
    backgroundColor: barColor,
    transition: 'width 0.3s ease-in-out',
  };

  const labelStyle = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    color: 'white',
    fontSize: '12px',
    fontWeight: 'bold',
  };

  // Add style variations
  if (styleType === 'striped') {
    fillerStyle.backgroundImage = 'repeating-linear-gradient(45deg, #606dbc, #606dbc 10px, #465298 10px, #465298 20px)';
  }

  if (styleType === 'rounded') {
    containerStyle.borderRadius = '20px';
  }

  return (
    <div style={containerStyle}>
      <div style={fillerStyle}></div>
      {showLabel && <span style={labelStyle}>{percentage}%</span>}
    </div>
  );
}

export default ProgressBar;

Changes:

  • Added a new prop `styleType` with default value ‘default’.
  • Added `borderRadius` prop.
  • Added an `if` statement to add a striped background image.
  • Added an `if` statement to add rounded corners.

Modify `App.js` to use the new styles:

<ProgressBar percentage={progress} barColor="#4CAF50" height="20px" showLabel={true} styleType="striped" />
<ProgressBar percentage={progress} barColor="orange" height="20px" showLabel={true} styleType="rounded" />

Adding Animation Control

To control the animation, you can add a prop that determines whether the animation is running or paused. Modify the `ProgressBar.js` component:

function ProgressBar({
  percentage,
  height = '10px',
  backgroundColor = '#eee',
  barColor = 'blue',
  showLabel = true,
  borderRadius = '5px',
  styleType = 'default',
  isPaused = false, // Add this
}) {
  const containerStyle = {
    width: '100%',
    height: height,
    backgroundColor: backgroundColor,
    borderRadius: borderRadius,
    overflow: 'hidden',
    position: 'relative',
  };

  const fillerStyle = {
    width: `${percentage}%`,
    height: '100%',
    backgroundColor: barColor,
    transition: isPaused ? 'none' : 'width 0.3s ease-in-out',
  };

  const labelStyle = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    color: 'white',
    fontSize: '12px',
    fontWeight: 'bold',
  };

  // Add style variations
  if (styleType === 'striped') {
    fillerStyle.backgroundImage = 'repeating-linear-gradient(45deg, #606dbc, #606dbc 10px, #465298 10px, #465298 20px)';
  }

  if (styleType === 'rounded') {
    containerStyle.borderRadius = '20px';
  }

  return (
    <div style={containerStyle}>
      <div style={fillerStyle}></div>
      {showLabel && <span style={labelStyle}>{percentage}%</span>}
    </div>
  );
}

export default ProgressBar;

Changes:

  • Added a new prop `isPaused` with a default value of `false`.
  • Modified the `transition` property in `fillerStyle` to use the `isPaused` prop.

Modify `App.js` to control the animation:

import React, { useState, useEffect } from 'react';
import ProgressBar from './ProgressBar';

function App() {
  const [progress, setProgress] = useState(0);
  const [isPaused, setIsPaused] = useState(false);

  useEffect(() => {
    if (!isPaused) {
      const interval = setInterval(() => {
        setProgress((prevProgress) => {
          const newProgress = prevProgress + 1;
          return Math.min(newProgress, 100);
        });
      }, 20);

      return () => clearInterval(interval);
    }
  }, [isPaused]);

  const togglePause = () => {
    setIsPaused(!isPaused);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Dynamic Progress Bar Example</h2>
      <ProgressBar percentage={progress} barColor="#4CAF50" height="20px" showLabel={true} styleType="striped" isPaused={isPaused} />
      <ProgressBar percentage={progress} barColor="orange" height="20px" showLabel={true} styleType="rounded" isPaused={isPaused} />
      <p>Progress: {progress}%</p>
      <button onClick={togglePause}>{isPaused ? 'Resume' : 'Pause'}</button>
    </div>
  );
}

export default App;

Changes:

  • Added `isPaused` state.
  • Modified the `useEffect` to only run the interval if `isPaused` is false.
  • Added a `togglePause` function.
  • Added a button to pause and resume the animation.

Common Mistakes and How to Fix Them

Here are some common pitfalls and how to avoid them:

1. Incorrect State Updates

Mistake: Directly modifying the state variable instead of using the setter function.

// Incorrect
progress = progress + 1; // Wrong

// Correct
setProgress(progress + 1); // Correct

Fix: Always use the state setter function (`setProgress` in our example) to update the state. This ensures React re-renders the component with the updated values.

2. Forgetting to Clean Up Intervals

Mistake: Not clearing the `setInterval` when the component unmounts.

useEffect(() => {
  const interval = setInterval(() => {
    setProgress((prevProgress) => prevProgress + 1);
  }, 20);
  // Missing clearInterval
}, []);

Fix: Return a cleanup function from the `useEffect` hook to clear the interval:

useEffect(() => {
  const interval = setInterval(() => {
    setProgress((prevProgress) => prevProgress + 1);
  }, 20);

  return () => clearInterval(interval);
}, []);

This prevents memory leaks and unexpected behavior.

3. Incorrect Prop Types (TypeScript)

Mistake: Not defining prop types.

Fix: While this tutorial does not use TypeScript, in a TypeScript project, always define prop types using `interface` or `type` to ensure the correct data types are being passed to the component.

interface ProgressBarProps {
  percentage: number;
  height?: string;
  backgroundColor?: string;
  barColor?: string;
  showLabel?: boolean;
  styleType?: 'default' | 'striped' | 'rounded';
  isPaused?: boolean;
}

Summary / Key Takeaways

In this tutorial, we’ve built a dynamic progress bar component using React. We’ve covered the basics of creating a reusable component, managing state, and adding custom styling and features. The key takeaways are:

  • Component Reusability: Components should be designed to be reusable in different parts of your application.
  • State Management: Use the `useState` hook to manage the progress value.
  • Props for Customization: Use props to control the appearance and behavior of the progress bar.
  • Side Effects with `useEffect`: Use the `useEffect` hook for side effects like setting up and clearing the interval.
  • Clean Up: Always clean up side effects to prevent memory leaks.

FAQ

Here are some frequently asked questions about building React progress bars:

  1. How can I make the progress bar responsive? You can use relative units (e.g., percentages, `em`, `rem`) for the width and height of the progress bar and its container. You can also use media queries in your CSS to adjust the appearance based on screen size.
  2. How do I animate the progress bar smoothly? Use CSS transitions on the `width` property of the filler element. We’ve already done this in `fillerStyle` with `transition: width 0.3s ease-in-out;`
  3. Can I use a library instead? Yes, there are many excellent React UI libraries (e.g., Material UI, Ant Design) that include pre-built progress bar components. Using a library can save you time and effort, but building your own component gives you more control and helps you understand the underlying concepts.
  4. How can I add different animation styles? You can use CSS animations or a library like `react-spring` or `framer-motion` for more advanced animation effects.
  5. How do I handle errors or failures in the progress? You can add additional states (e.g., `isError`, `errorMessage`) and conditionally render different UI elements based on the progress status. You could also add a visual indicator (e.g., a red color) if an error occurs.

Building a dynamic progress bar is an excellent exercise for understanding React fundamentals. By creating this component from scratch, you’ve gained valuable experience in state management, component composition, and prop handling. You now have a solid foundation for building more complex UI elements and enhancing the user experience in your React applications.