skip to Main Content

I am having a progress bar which on click starts its progress from 60 to all the way upto 100. I am able to achieve this using settinterval and setting the corresponding state.

import React, { useState, useRef } from "react";
import ProgressBar from "./ProgressBar";

export default function App() {
  const [file, setFile] = useState({ status: 0, error: "" });
  const mockRef = useRef(60);

  const initiate = () => {
    const intervalID = setInterval(() => {
      if (mockRef.current >= 100) {
        clearInterval(intervalID);
        setFile((prevState) => ({
          ...prevState,
          status: 100
        }));
      } else {
        setFile((prevState) => ({
          ...prevState,
          status: mockRef.current
        }));
        mockRef.current = mockRef.current + 10;
      }
    }, 200);
  };

  return (
    <div className="App" style={appStyles}>
      <button type="button" onClick={initiate}>
        Click Me!
      </button>
      <ProgressBar bgColor={"#DF8100"} progress={file.status} />
      {file.status === 100 && <span>Upload complete</span>}
    </div>
  );
}

I am using the ref to dymanically increment the progress by 10 and when its 100, I clear it and bring the message as Upload Complete. Code works just fine.

Sandbox: https://codesandbox.io/s/simple-react-progress-bar-forked-yfz9xb?file=/src/useProgress.jsx:0-436

Now I want the content inside of initiateto move it to a custom hooks that should take care of the setinterval functionality and set the object so that the functionality remains the same. May be this cutsom hook should take initial percentage as number and may be the state setter.

Any idea how I can write this custom hook.This is what my attempt looks like

import { useRef } from "react";

export const useProgress = (state, timer) => {
  const mockRef = useRef(timer);

  const intervalID = setInterval(() => {
    if (mockRef.current >= 100) {
      clearInterval(intervalID);
      return {
        ...state,
        status: 100
      };
    } else {
      mockRef.current = mockRef.current + 10;
      return {
        ...state,
        status: mockRef.current
      };
    }
  }, 200);
};

2

Answers


  1. When you have something working within a function component that you want to turn into a hook, it’s often easier than one might think. Usually, you can copy the functioning code you have directly into the hook; make the hook accept anything it needs from outside that code as parameters; and return whatever the calling component will need from the hook as a return value (if there are multiple values, wrap them in an array [tuple] or object).

    In your case, just copy everything prior to the return from your component into a hook, and have the hook return file and initiate:

    export const useProgress = () => {
        const [file, setFile] = useState({ status: 0, error: "" });
        const mockRef = useRef(60);
    
        const initiate = () => {
            const intervalID = setInterval(() => {
                if (mockRef.current >= 100) {
                    clearInterval(intervalID);
                    setFile((prevState) => ({
                        ...prevState,
                        status: 100,
                    }));
                } else {
                    setFile((prevState) => ({
                        ...prevState,
                        status: mockRef.current,
                    }));
                    mockRef.current = mockRef.current + 10;
                }
            }, 200);
        };
    
        return [file, initiate];
    };
    

    Then App uses it like this:

    export default function App() {
        const [file, initiate] = useProgress();
    
        return (
            <div className="App" style={appStyles}>
                <button type="button" onClick={initiate}>
                    Click Me!
                </button>
                <ProgressBar bgColor={"#DF8100"} progress={file.status} />
                {file.status === 100 && <span>Upload complete</span>}
            </div>
        );
    }
    

    Updated sandbox

    Login or Signup to reply.
  2. You can provide updater handler function and the timer start value as arguments which can change depending on where you use your custom hook is used.

    This would also work:

    import { useRef } from "react";
    
    const useProgress = (updater, timer) => {
      const mockRef = useRef(timer);
    
      const initiate = () => {
        const intervalID = setInterval(() => {
          if (mockRef.current >= 100) {
            clearInterval(intervalID);
            updater((prevState) => ({
              ...prevState,
              status: 100
            }));
          } else {
            mockRef.current = mockRef.current + 10;
            updater((prevState) => ({
              ...prevState,
              status: mockRef.current
            }));
          }
        }, 200);
      };
    
      return initiate;
    };
    
    export default useProgress;
    

    In your App.jsx

    ...
    ...
    export default function App() {
      const [file, setFile] = useState({ status: 0, error: "" });
      const initiate = useProgress(setFile, 60);
    
      return (
        <div className="App" style={appStyles}>
          <button type="button" onClick={initiate}>
            Click Me!
          </button>
          <ProgressBar bgColor={"#DF8100"} progress={file.status} />
          {file.status === 100 && <span>Upload complete</span>}
        </div>
      );
    }
    ...
    

    Codesandbox Demo:

    Edit simple react progress bar (forked)

    If the state is an array then fileIndex can be passed into the custom hook:

    Edit simple react progress bar (forked)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search