Search code examples
reactjs

Using react-countup to count results after fetching from API


I'm trying to use the react-countup library to render counting but I can't get the count-up components to render the metric numbers. However, there are tables below the metric numbers that render fine.

By the way, the listUsers() method references a library function that calls a GET request from the back-end API using Axios which retrieves a list of users.

What I tried doing was making admins and disabledUsers their own state hoping that I could count up directly from their length right after they get their state updated from the useEffect() hook. I also made each hook their own ref. I used to use the id attribute to reference the count-ups but since it's React, I figured that it would be more React-friendly to reference them using a useRef() rather than a standard HTML-friendly id. However, none of these solutions have worked for me.

Here is the source code using react-countup:

export default function LandingPage() {
    const [realData, setRealData] = useState([])
    const [admins, setAdmins] = useState([])
    const [disabledUsers, setDisabledUsers] = useState([])
    const [isLoaded, _] = useState(true)
    const [shouldThrowError, setShouldThrowError] = useState(false)

    useEffect(() => {
        listUsers().then(users => {
            setRealData(users)
        }).catch(err => {
            console.log(JSON.stringify(err))
            setShouldThrowError(true)
        })
        userCountUp.update(realData.length)
    }, [isLoaded])

    useEffect(() => {
        setAdmins(realData.filter(user => user[6]))
        setDisabledUsers(realData.filter(user => !user[7]))
        adminCountUp.update(admins.length)
        disabledCountUp.update(disabledUsers.length)
    }, [realData])

    const userCountUpRef = useRef(null)
    const adminCountUpRef = useRef(null)
    const disabledCountUpRef = useRef(null)

    const userCountUp = useCountUp({
        ref: userCountUpRef,
        end: realData.length
    })

    const adminCountUp = useCountUp({
        ref: adminCountUpRef,
        end: admins.length
    })

    const disabledCountUp = useCountUp({
        ref: disabledCountUpRef,
        end: disabledUsers.length
    })

    const ThrowError = (shouldThrowError) => {
        if (shouldThrowError) {
            //throw new Error('Trying to get around this error by rendering a fallback using an IBM carbon component')
        }
    }

    const ErrorBoundary = ({ trigger, fallback, children }) => {
        if (trigger) {
            console.log('fallback')
            return fallback
        } else {
            return children
        }
    }

    return (
        <>
            <ErrorBoundary trigger={shouldThrowError} fallback={<h1>An error has occurred</h1>}>
                {/*<ThrowError shouldThrowError={throwError} />*/}
                <Grid className="dashboard-page" fullWidth>
                    <Column lg={16} md={8} sm={4} className="dashboard-page__banner">
                        <h1>Admin Dashboard</h1>
                    </Column>
                    <Column className='mt-6 mb-4' lg={16} md={8} sm={4}>
                        <div className='grid grid-cols-3 gap-4 mt-4'>
                            <div>
                                <h2 className='metric'>
                                    <span ref={userCountUpRef} />
                                </h2>
                                <p>Users</p>
                            </div>
                            <div>
                                <h2 className='metric'>
                                    <span ref={adminCountUpRef} />
                                </h2>
                                <p>Administrators</p>
                            </div>
                            <div>
                                <h2 className='metric'>
                                    <span ref={disabledCountUpRef} />
                                </h2>
                                <p>Disabled</p>
                            </div>
                        </div>
                    </Column>
                    <Column className='mt-6 mb-4' lg={16} md={8} sm={4}>
                        <h2 className='mb-2'>Administrators</h2>
                        <Table>
                            <TableHead>
                                <TableRow>
                                    <TableHeader>
                                        Last Name
                                    </TableHeader>
                                    <TableHeader>
                                        First Name
                                    </TableHeader>
                                    <TableHeader>
                                        Username
                                    </TableHeader>
                                </TableRow>
                            </TableHead>
                            <TableBody>
                                {admins.map(user => {
                                    return (
                                        <TableRow key={user[0]}>
                                            <TableCell>{user[5]}</TableCell>
                                            <TableCell>{user[4]}</TableCell>
                                            <TableCell>
                                                {user[1]}
                                            </TableCell>
                                        </TableRow>
                                    )
                                })}
                            </TableBody>
                        </Table>
                    </Column>
                    <Column className='mt-6 mb-4' lg={16} md={8} sm={4}>
                        <h2 className='mb-2'>Disabled</h2>
                        <Table>
                            <TableHead>
                                <TableRow>
                                    <TableHeader>
                                        Last Name
                                    </TableHeader>
                                    <TableHeader>
                                        First Name
                                    </TableHeader>
                                    <TableHeader>
                                        Username
                                    </TableHeader>
                                </TableRow>
                            </TableHead>
                            <TableBody>
                                {disabledUsers.map(user => {
                                    return (
                                        <TableRow key={user[0]}>
                                            <TableCell>{user[5]}</TableCell>
                                            <TableCell>{user[4]}</TableCell>
                                            <TableCell>
                                                {user[1]}
                                            </TableCell>
                                        </TableRow>
                                    )
                                })}
                            </TableBody>
                        </Table>
                    </Column>
                </Grid>
            </ErrorBoundary>
        </>
    );
}

Solution

  • Here is what I did to fix the bug:

    1. I defined the ErrorBoundary component outside the LandingPage component. Nesting component definitions cause the state of the nested component to reset on every render. You can find a working example here(under the "Pitfall" section).

    2. I stopped using the state variables in the *CountUp.update calls because they held stale data.

      useEffect(() => {
        listUsers()
          .then((users) => {
            setRealData(users);
            userCountUp.update(users.length);
          })
          .catch((err) => {
            console.log(JSON.stringify(err));
            setShouldThrowError(true);
          });
      }, [isLoaded]);
    

    Here is the code I used to reproduce and fix the bug:

    "use client";
    import { useEffect, useRef, useState } from "react";
    import { useCountUp } from "react-countup";
    
    export default function LandingPage() {
      const [realData, setRealData] = useState([]);
      const [admins, setAdmins] = useState([]);
      const [disabledUsers, setDisabledUsers] = useState([]);
      const [isLoaded, _] = useState(true);
      const [shouldThrowError, setShouldThrowError] = useState(false);
    
      useEffect(() => {
        listUsers()
          .then((users) => {
            setRealData(users as never[]);
            userCountUp.update(users.length);
          })
          .catch((err) => {
            console.log(JSON.stringify(err));
            setShouldThrowError(true);
          });
      }, [isLoaded]);
    
      useEffect(() => {
        const admins = realData.filter((user, i) => i % 2 == 0);
        const disabledUsers = realData.filter((user, i) => i % 2 != 0);
        setAdmins(admins);
        setDisabledUsers(disabledUsers);
        adminCountUp.update(admins.length);
        disabledCountUp.update(disabledUsers.length);
      }, [realData]);
    
      const userCountUpRef = useRef(null);
      const adminCountUpRef = useRef(null);
      const disabledCountUpRef = useRef(null);
    
      const userCountUp = useCountUp({
        ref: userCountUpRef,
        end: realData.length,
      });
    
      const adminCountUp = useCountUp({
        ref: adminCountUpRef,
        end: admins.length,
      });
    
      const disabledCountUp = useCountUp({
        ref: disabledCountUpRef,
        end: disabledUsers.length,
      });
    
      return (
        <ErrorBoundary trigger={shouldThrowError} fallback={<h1>An error has occurred</h1>}>
          <div>
            <span ref={userCountUpRef} />
          </div>
          <div>
            <span ref={adminCountUpRef} />
          </div>
          <div>
            <span ref={disabledCountUpRef} />
          </div>
        </ErrorBoundary>
      );
    }
    
    async function listUsers(): Promise<any[]> {
      return new Promise((resolve, _) => {
        setTimeout(() => {
         resolve("a".repeat(20).split(""));
        }, 1000);
      });
    }
    
    function ErrorBoundary({ trigger, fallback, children }: any) {
      if (trigger) {
        console.log("fallback");
        return fallback;
      } else {
        return children;
      }
    }