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>
</>
);
}
Here is what I did to fix the bug:
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).
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;
}
}