lazy load charts and disable chart animations

This commit is contained in:
Henry Dollman
2024-08-04 20:14:13 -04:00
parent e3ed07a999
commit 8ef30e0733
12 changed files with 189 additions and 105 deletions

Binary file not shown.

View File

@@ -37,6 +37,7 @@
"recharts": "^2.13.0-alpha.4",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"use-is-in-viewport": "^1.0.9",
"valibot": "^0.36.0"
},
"devDependencies": {

View File

@@ -1,12 +1,12 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner'
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types'
import { useRef } from 'react'
import { useMemo, useRef } from 'react'
export default function BandwidthChart({
ticks,
@@ -19,13 +19,17 @@ export default function BandwidthChart({
const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
if (!systemData.length || !ticks.length) {
return <Spinner />
}
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
return (
<div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto">
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart
accessibilityLayer
data={systemData}
@@ -80,7 +84,8 @@ export default function BandwidthChart({
fill="hsl(var(--chart-5))"
fillOpacity={0.4}
stroke="hsl(var(--chart-5))"
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
/>
<Area
dataKey="stats.nr"
@@ -89,7 +94,8 @@ export default function BandwidthChart({
fill="hsl(var(--chart-2))"
fillOpacity={0.4}
stroke="hsl(var(--chart-2))"
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>

View File

@@ -6,8 +6,8 @@ import {
ChartTooltipContent,
} from '@/components/ui/chart'
import { useMemo, useRef } from 'react'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner'
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
@@ -22,6 +22,8 @@ export default function ContainerCpuChart({
const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const chartConfig = useMemo(() => {
let config = {} as Record<
string,
@@ -57,13 +59,19 @@ export default function ContainerCpuChart({
return config satisfies ChartConfig
}, [chartData])
if (!chartData.length || !ticks.length) {
return <Spinner />
}
// if (!chartData.length || !ticks.length) {
// return <Spinner />
// }
return (
<div ref={chartRef}>
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart
accessibilityLayer
data={chartData}
@@ -105,8 +113,9 @@ export default function ContainerCpuChart({
<Area
key={key}
// isAnimationActive={chartData.length < 20}
animateNewValues={false}
animationDuration={1200}
isAnimationActive={false}
// animateNewValues={false}
// animationDuration={1200}
dataKey={key}
type="monotoneX"
fill={chartConfig[key].color}

View File

@@ -6,8 +6,8 @@ import {
ChartTooltipContent,
} from '@/components/ui/chart'
import { useMemo, useRef } from 'react'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner'
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
@@ -18,9 +18,11 @@ export default function ContainerMemChart({
chartData: Record<string, number | string>[]
ticks: number[]
}) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const chartConfig = useMemo(() => {
let config = {} as Record<
@@ -57,13 +59,19 @@ export default function ContainerMemChart({
return config satisfies ChartConfig
}, [chartData])
if (!chartData.length || !ticks.length) {
return <Spinner />
}
// if (!chartData.length || !ticks.length) {
// return <Spinner />
// }
return (
<div ref={chartRef}>
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart
accessibilityLayer
data={chartData}
@@ -71,8 +79,6 @@ export default function ContainerMemChart({
margin={{
top: 10,
}}
// reverseStackOrder={true}
>
<CartesianGrid vertical={false} />
<YAxis
@@ -109,9 +115,8 @@ export default function ContainerMemChart({
{Object.keys(chartConfig).map((key) => (
<Area
key={key}
isAnimationActive={chartData.length < 20}
animateNewValues={false}
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
dataKey={key}
type="monotoneX"
fill={chartConfig[key].color}

View File

@@ -6,8 +6,8 @@ import {
ChartTooltipContent,
} from '@/components/ui/chart'
import { useMemo, useRef } from 'react'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner'
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { Separator } from '@/components/ui/separator'
@@ -19,9 +19,11 @@ export default function ContainerCpuChart({
chartData: Record<string, number | number[]>[]
ticks: number[]
}) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const chartConfig = useMemo(() => {
let config = {} as Record<
@@ -57,13 +59,19 @@ export default function ContainerCpuChart({
return config satisfies ChartConfig
}, [chartData])
if (!chartData.length || !ticks.length) {
return <Spinner />
}
// if (!chartData.length || !ticks.length) {
// return <Spinner />
// }
return (
<div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto">
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart
accessibilityLayer
data={chartData}
@@ -134,9 +142,8 @@ export default function ContainerCpuChart({
<Area
key={key}
name={key}
// isAnimationActive={chartData.length < 20}
animateNewValues={false}
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
dataKey={(data) => data?.[key]?.[2] ?? 0}
type="monotoneX"
fill={chartConfig[key].color}

View File

@@ -1,12 +1,12 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner'
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types'
import { useRef } from 'react'
import { useMemo, useRef } from 'react'
export default function CpuChart({
ticks,
@@ -15,17 +15,24 @@ export default function CpuChart({
ticks: number[]
systemData: SystemStatsRecord[]
}) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
if (!systemData.length || !ticks.length) {
return <Spinner />
}
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
// if (!systemData.length || !ticks.length) {
// return <Spinner />
// }
return (
<div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto">
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart accessibilityLayer data={systemData} margin={{ top: 10 }}>
<CartesianGrid vertical={false} />
<YAxis
@@ -64,9 +71,10 @@ export default function CpuChart({
fill="hsl(var(--chart-1))"
fillOpacity={0.4}
stroke="hsl(var(--chart-1))"
animationDuration={1200}
isAnimationActive={false}
// animationEasing="ease-out"
// animateNewValues={false}
animationDuration={700}
animateNewValues={true}
/>
</AreaChart>
</ChartContainer>

View File

@@ -1,9 +1,9 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils'
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import { useMemo, useRef } from 'react'
import Spinner from '../spinner'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types'
@@ -15,9 +15,11 @@ export default function DiskChart({
ticks: number[]
systemData: SystemStatsRecord[]
}) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const diskSize = useMemo(() => {
return Math.round(systemData[0]?.stats.d)
@@ -32,13 +34,19 @@ export default function DiskChart({
// return ticks
// }, [diskSize])
if (!systemData.length || !ticks.length) {
return <Spinner />
}
// if (!systemData.length || !ticks.length) {
// return <Spinner />
// }
return (
<div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto">
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart
accessibilityLayer
data={systemData}
@@ -88,7 +96,8 @@ export default function DiskChart({
fill="hsl(var(--chart-4))"
fillOpacity={0.4}
stroke="hsl(var(--chart-4))"
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>

View File

@@ -1,12 +1,12 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner'
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types'
import { useRef } from 'react'
import { useMemo, useRef } from 'react'
export default function DiskIoChart({
ticks,
@@ -15,17 +15,25 @@ export default function DiskIoChart({
ticks: number[]
systemData: SystemStatsRecord[]
}) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
if (!systemData.length || !ticks.length) {
return <Spinner />
}
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
// if (!systemData.length || !ticks.length) {
// return <Spinner />
// }
return (
<div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto">
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart
accessibilityLayer
data={systemData}
@@ -80,7 +88,8 @@ export default function DiskIoChart({
fill="hsl(var(--chart-3))"
fillOpacity={0.4}
stroke="hsl(var(--chart-3))"
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
/>
<Area
dataKey="stats.dr"
@@ -89,7 +98,8 @@ export default function DiskIoChart({
fill="hsl(var(--chart-1))"
fillOpacity={0.4}
stroke="hsl(var(--chart-1))"
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>

View File

@@ -1,9 +1,9 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils'
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import { useMemo, useRef } from 'react'
import Spinner from '../spinner'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types'
@@ -19,18 +19,26 @@ export default function MemChart({
const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const totalMem = useMemo(() => {
const maxMem = Math.ceil(systemData[0]?.stats.m)
return maxMem > 2 && maxMem % 2 !== 0 ? maxMem + 1 : maxMem
}, [systemData])
if (!systemData.length || !ticks.length) {
return <Spinner />
}
// if (!systemData.length || !ticks.length) {
// return <Spinner />
// }
return (
<div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto">
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart
accessibilityLayer
data={systemData}
@@ -80,7 +88,8 @@ export default function MemChart({
fillOpacity={0.4}
stroke="hsl(var(--chart-2))"
stackId="a"
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
/>
<Area
dataKey="stats.mb"
@@ -91,7 +100,8 @@ export default function MemChart({
strokeOpacity={0.3}
stroke="hsl(var(--chart-2))"
stackId="a"
animationDuration={1200}
// animationDuration={1200}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>

View File

@@ -1,12 +1,12 @@
import { $updatedSystem, $systems, pb, $chartTime } from '@/lib/stores'
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from 'react'
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
import { useStore } from '@nanostores/react'
import Spinner from '../spinner'
import { ClockArrowUp, CpuIcon, GlobeIcon } from 'lucide-react'
import ChartTimeSelect from '../charts/chart-time-select'
import { chartTimeData, cn, getPbTimestamp } from '@/lib/utils'
import { chartTimeData, cn, getPbTimestamp, useClampedIsInViewport } from '@/lib/utils'
import { Separator } from '../ui/separator'
import { scaleTime } from 'd3-scale'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
@@ -27,15 +27,10 @@ export default function ServerDetail({ name }: { name: string }) {
const [ticks, setTicks] = useState([] as number[])
const [server, setServer] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [dockerCpuChartData, setDockerCpuChartData] = useState(
[] as Record<string, number | string>[]
)
const [dockerMemChartData, setDockerMemChartData] = useState(
[] as Record<string, number | string>[]
)
const [dockerNetChartData, setDockerNetChartData] = useState(
[] as Record<string, number | number[]>[]
)
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>()
const [dockerMemChartData, setDockerMemChartData] = useState<Record<string, number | string>[]>()
const [dockerNetChartData, setDockerNetChartData] =
useState<Record<string, number | number[]>[]>()
useEffect(() => {
document.title = `${name} / Beszel`
@@ -47,9 +42,9 @@ export default function ServerDetail({ name }: { name: string }) {
const resetCharts = useCallback(() => {
setSystemStats([])
setDockerCpuChartData([])
setDockerMemChartData([])
setDockerNetChartData([])
setDockerCpuChartData(undefined)
setDockerMemChartData(undefined)
setDockerNetChartData(undefined)
}, [])
useEffect(resetCharts, [chartTime])
@@ -121,7 +116,9 @@ export default function ServerDetail({ name }: { name: string }) {
sort: 'created',
})
.then((records) => {
makeContainerData(records)
if (records.length) {
makeContainerData(records)
}
// setContainers(records)
})
}, [server, chartTime])
@@ -129,15 +126,14 @@ export default function ServerDetail({ name }: { name: string }) {
// container stats for charts
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
// console.log('containers', containers)
const dockerCpuData = [] as typeof dockerCpuChartData
const dockerMemData = [] as typeof dockerMemChartData
const dockerNetData = [] as typeof dockerNetChartData
const dockerCpuData = []
const dockerMemData = []
const dockerNetData = []
for (let { created, stats } of containers) {
const time = new Date(created).getTime()
let cpuData = { time } as (typeof dockerCpuChartData)[0]
let memData = { time } as (typeof dockerMemChartData)[0]
let netData = { time } as (typeof dockerNetChartData)[0]
let cpuData = { time } as Record<string, number | string>
let memData = { time } as Record<string, number | string>
let netData = { time } as Record<string, number | number[]>
for (let container of stats) {
cpuData[container.n] = container.c
memData[container.n] = container.m
@@ -225,7 +221,7 @@ export default function ServerDetail({ name }: { name: string }) {
<CpuChart ticks={ticks} systemData={systemStats} />
</ChartCard>
{dockerCpuChartData.length > 0 && (
{dockerCpuChartData && (
<ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers">
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
</ChartCard>
@@ -235,7 +231,7 @@ export default function ServerDetail({ name }: { name: string }) {
<MemChart ticks={ticks} systemData={systemStats} />
</ChartCard>
{dockerMemChartData.length > 0 && (
{dockerMemChartData && (
<ChartCard title="Docker Memory Usage" description="Memory usage of docker containers">
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
</ChartCard>
@@ -256,7 +252,7 @@ export default function ServerDetail({ name }: { name: string }) {
<BandwidthChart ticks={ticks} systemData={systemStats} />
</ChartCard>
{dockerNetChartData.length > 0 && (
{dockerNetChartData && (
<ChartCard
title="Docker Network I/O"
description="Includes traffic between internal services"
@@ -277,8 +273,10 @@ function ChartCard({
description: string
children: React.ReactNode
}) {
const target = useRef<HTMLDivElement>(null)
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
return (
<Card className="pb-2 sm:pb-4 col-span-full">
<Card className="pb-2 sm:pb-4 col-span-full" ref={wrappedTargetRef}>
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
@@ -287,7 +285,8 @@ function ChartCard({
</div>
</CardHeader>
<CardContent className="pl-1 w-[calc(100%-1.6em)] h-52 relative">
<Suspense fallback={<Spinner />}>{children}</Suspense>
{<Spinner />}
{isInViewport && <Suspense>{children}</Suspense>}
</CardContent>
</Card>
)

View File

@@ -7,6 +7,7 @@ import { RecordModel, RecordSubscription } from 'pocketbase'
import { WritableAtom } from 'nanostores'
import { timeDay, timeHour } from 'd3-time'
import { useEffect, useState } from 'react'
import useIsInViewport, { CallbackRef, HookOptions } from 'use-is-in-viewport'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -183,17 +184,36 @@ export const chartTimeData: ChartTimeData = {
/** Hacky solution to set the correct width of the yAxis in recharts */
export function useYaxisWidth(chartRef: React.RefObject<HTMLDivElement>) {
const [yAxisWidth, setYAxisWidth] = useState(90)
const [yAxisWidth, setYAxisWidth] = useState(180)
useEffect(() => {
let interval = setInterval(() => {
// console.log('chartRef', chartRef.current)
const yAxisElement = chartRef?.current?.querySelector('.yAxis')
if (yAxisElement) {
console.log('yAxisElement', yAxisElement)
// console.log('yAxisElement', yAxisElement)
setYAxisWidth(yAxisElement.getBoundingClientRect().width + 22)
clearInterval(interval)
}
}, 16)
}, 0)
return () => clearInterval(interval)
}, [])
return yAxisWidth
}
export function useClampedIsInViewport(options: HookOptions): [boolean | null, CallbackRef] {
const [isInViewport, wrappedTargetRef] = useIsInViewport(options)
const [wasInViewportAtleastOnce, setWasInViewportAtleastOnce] = useState(isInViewport)
useEffect(() => {
setWasInViewportAtleastOnce((prev) => {
// this will clamp it to the first true
// received from useIsInViewport
if (!prev) {
return isInViewport
}
return prev
})
}, [isInViewport])
return [wasInViewportAtleastOnce, wrappedTargetRef]
}