Skip to content

Instantly share code, notes, and snippets.

@jkcorrea
Last active January 17, 2025 21:26
Show Gist options
  • Save jkcorrea/9b87713d049bea7c55b690474581f86b to your computer and use it in GitHub Desktop.
Save jkcorrea/9b87713d049bea7c55b690474581f86b to your computer and use it in GitHub Desktop.
AsyncCard
import * as React from 'react'
import { Card, CardContent, CardFooter, CardHeader } from './card'
import { Skeleton } from './skeleton'
type AsyncCardChild<T> = ((data: T) => React.ReactNode) | React.ReactNode
type AsyncCardProps<T> = Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> & {
promise?: Promise<T>
children?: AsyncCardChild<T> | AsyncCardChild<T>[]
}
const AsyncCard = <T,>({ promise, children, ...props }: AsyncCardProps<T>) => {
const [result, setResult] = React.useState<T>()
const [loading, setLoading] = React.useState(false)
React.useEffect(() => {
if (!promise) return
setLoading(true)
promise.then((data) => {
setResult(data)
setLoading(false)
})
}, [promise])
function renderChild(child: AsyncCardChild<T>, index: number) {
if (typeof child === 'function') {
return loading || !result ? (
<Skeleton className="w-full h-4" />
) : (
<React.Fragment key={index}>{child(result)}</React.Fragment>
)
}
return <React.Fragment key={index}>{child}</React.Fragment>
}
return (
<Card {...props}>
{Array.isArray(children)
? children.map((child, index) => renderChild(child, index))
: renderChild(children, 0)}
</Card>
)
}
const AsyncCardContent = <T,>({ promise, children, ...props }: AsyncCardProps<T>) => {
const [result, setResult] = React.useState<T>()
const [loading, setLoading] = React.useState(false)
React.useEffect(() => {
if (!promise) return
setLoading(true)
promise.then((data) => {
setResult(data)
setLoading(false)
})
}, [promise])
function renderChild(child: AsyncCardChild<T>, index: number) {
if (typeof child === 'function') {
return loading || !result ? (
<Skeleton className="w-full h-12" />
) : (
<React.Fragment key={index}>{child(result)}</React.Fragment>
)
}
return <React.Fragment key={index}>{child}</React.Fragment>
}
return (
<CardContent {...props}>
{Array.isArray(children)
? children.map((child, index) => renderChild(child, index))
: renderChild(children, 0)}
</CardContent>
)
}
const AsyncCardHeader = <T,>({ promise, children, ...props }: AsyncCardProps<T>) => {
const [result, setResult] = React.useState<T>()
const [loading, setLoading] = React.useState(false)
React.useEffect(() => {
if (!promise) return
setLoading(true)
promise.then((data) => {
setResult(data)
setLoading(false)
})
}, [promise])
function renderChild(child: AsyncCardChild<T>, index: number) {
if (typeof child === 'function') {
return loading || !result ? (
<Skeleton className="w-full h-4" />
) : (
<React.Fragment key={index}>{child(result)}</React.Fragment>
)
}
return <React.Fragment key={index}>{child}</React.Fragment>
}
return (
<CardHeader {...props}>
{Array.isArray(children)
? children.map((child, index) => renderChild(child, index))
: renderChild(children, 0)}
</CardHeader>
)
}
const AsyncCardFooter = <T,>({ promise, children, ...props }: AsyncCardProps<T>) => {
const [result, setResult] = React.useState<T>()
const [loading, setLoading] = React.useState(false)
React.useEffect(() => {
if (!promise) return
setLoading(true)
promise.then((data) => {
setResult(data)
setLoading(false)
})
}, [promise])
function renderChild(child: AsyncCardChild<T>, index: number) {
if (typeof child === 'function') {
return loading || !result ? (
<Skeleton className="w-full h-4" />
) : (
<React.Fragment key={index}>{child(result)}</React.Fragment>
)
}
return <React.Fragment key={index}>{child}</React.Fragment>
}
return (
<CardFooter {...props}>
{Array.isArray(children)
? children.map((child, index) => renderChild(child, index))
: renderChild(children, 0)}
</CardFooter>
)
}
export { AsyncCard, AsyncCardContent, AsyncCardFooter, AsyncCardHeader }
@jkcorrea
Copy link
Author

jkcorrea commented Jan 17, 2025

renders a Skeleton while promise resolves. data arg inside any function children will be typed as the return type of the promise

usage:

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
            <Icons.DollarSign size={16} />
          </CardHeader>
          <AsyncCardContent promise={revenueQuery.promise}>
            {(data) => {
              return <div className="text-2xl font-bold">{formatCurrency(data?.total)}</div>
            }}
          </AsyncCardContent>
          <AsyncCardFooter promise={revenueQuery.promise}>
            {(data) => {
              const diff = data.netRevenue - data.prevNetRevenue
              const pctChange = (diff / data.prevNetRevenue) * 100
              const sign = diff >= 0 ? '+' : '-'

              return (
                <p className="text-xs text-muted-foreground">{`${sign}${Math.abs(pctChange).toFixed(2)}%`}</p>
              )
            }}
          </AsyncCardFooter>
        </Card>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment