diff --git a/package.json b/package.json index fb4c290be..f5deece08 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@tanstack/react-router-devtools": "1.167.0", "@tanstack/react-router-ssr-query": "1.167.0", "@tanstack/react-start": "1.168.10", + "@tanstack/react-start-client": "1.168.2", "@tanstack/react-table": "^8.21.3", "@types/d3": "^7.4.3", "@uploadthing/react": "^7.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf8c99f53..3e53a68eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,9 @@ importers: '@tanstack/react-start': specifier: 1.168.10 version: 1.168.10(@vitejs/plugin-rsc@0.5.24(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.13(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.13(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3)) + '@tanstack/react-start-client': + specifier: 1.168.2 + version: 1.168.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) diff --git a/src/components/DeferredApplicationStarter.tsx b/src/components/DeferredApplicationStarter.tsx index ae4686ecd..a1422dce9 100644 --- a/src/components/DeferredApplicationStarter.tsx +++ b/src/components/DeferredApplicationStarter.tsx @@ -1,93 +1,20 @@ -import * as React from 'react' +import { Hydrate } from '@tanstack/react-start' +import { idle, visible } from '@tanstack/react-start/hydration' -import type { ApplicationStarterProps } from '~/components/ApplicationStarter' - -const LazyApplicationStarter = React.lazy(() => - import('~/components/ApplicationStarter').then((m) => ({ - default: m.ApplicationStarter, - })), -) +import { + ApplicationStarter, + type ApplicationStarterProps, +} from '~/components/ApplicationStarter' export function DeferredApplicationStarter(props: ApplicationStarterProps) { - const wrapperRef = React.useRef(null) - const [shouldLoad, setShouldLoad] = React.useState(false) - - React.useEffect(() => { - if (shouldLoad) { - return - } - - const element = wrapperRef.current - - if (!element || typeof IntersectionObserver === 'undefined') { - setShouldLoad(true) - return - } - - const observer = new IntersectionObserver( - (entries) => { - if (!entries.some((entry) => entry.isIntersecting)) { - return - } - - setShouldLoad(true) - observer.disconnect() - }, - { rootMargin: '320px 0px' }, - ) - - observer.observe(element) - - return () => { - observer.disconnect() - } - }, [shouldLoad]) - - React.useEffect(() => { - if (shouldLoad || typeof window === 'undefined') { - return - } - - const requestIdleCallback = window.requestIdleCallback - const cancelIdleCallback = window.cancelIdleCallback - - if ( - typeof requestIdleCallback === 'function' && - typeof cancelIdleCallback === 'function' - ) { - const idleId = requestIdleCallback( - () => { - setShouldLoad(true) - }, - { timeout: 2500 }, - ) - - return () => { - cancelIdleCallback(idleId) - } - } - - const timeoutId = window.setTimeout(() => { - setShouldLoad(true) - }, 1500) - - return () => { - window.clearTimeout(timeoutId) - } - }, [shouldLoad]) - return ( -
- {shouldLoad ? ( - } - > - - - ) : ( - - )} -
+ } + > + + ) } diff --git a/src/components/LazyLandingCommunitySection.tsx b/src/components/LazyLandingCommunitySection.tsx index 09e654b8f..60904c714 100644 --- a/src/components/LazyLandingCommunitySection.tsx +++ b/src/components/LazyLandingCommunitySection.tsx @@ -1,18 +1,9 @@ -import * as React from 'react' -import type { LibraryId } from '~/libraries' -import { useIntersectionObserver } from '~/hooks/useIntersectionObserver' - -const LazyMaintainersSection = React.lazy(async () => { - const mod = await import('./MaintainersSection') - - return { default: mod.MaintainersSection } -}) +import { Hydrate } from '@tanstack/react-start' +import { visible } from '@tanstack/react-start/hydration' -const LazyPartnersSection = React.lazy(async () => { - const mod = await import('./PartnersSection') - - return { default: mod.PartnersSection } -}) +import type { LibraryId } from '~/libraries' +import { MaintainersSection } from './MaintainersSection' +import { PartnersSection } from './PartnersSection' interface LazyLandingCommunitySectionProps { libraryId: LibraryId @@ -41,31 +32,20 @@ function SectionSkeleton({ title }: { title: string }) { export function LazyLandingCommunitySection({ libraryId, }: LazyLandingCommunitySectionProps) { - const { ref, isIntersecting } = useIntersectionObserver({ - rootMargin: '25%', - triggerOnce: true, - }) - return ( -
- {!isIntersecting ? ( - <> + - - ) : ( - - - - - } - > - - - - )} -
+ + } + > +
+ + +
+ ) } diff --git a/src/components/LazySponsorSection.tsx b/src/components/LazySponsorSection.tsx index 51df19691..6bcaae497 100644 --- a/src/components/LazySponsorSection.tsx +++ b/src/components/LazySponsorSection.tsx @@ -1,17 +1,17 @@ -import React from 'react' +import type { CSSProperties, ReactNode } from 'react' import { useQuery } from '@tanstack/react-query' +import { Hydrate } from '@tanstack/react-start' +import { visible } from '@tanstack/react-start/hydration' import { ArrowRight } from 'lucide-react' -import { useIntersectionObserver } from '~/hooks/useIntersectionObserver' import { getSponsorsForSponsorPack } from '~/utils/sponsors.functions' import { Button } from '~/ui' import PlaceholderSponsorPack from './PlaceholderSponsorPack' - -const LazySponsorPack = React.lazy(() => import('./SponsorPack')) +import SponsorPack from './SponsorPack' type LazySponsorSectionProps = { - title?: React.ReactNode + title?: ReactNode aspectRatio?: string - packMaxWidth?: React.CSSProperties['maxWidth'] + packMaxWidth?: CSSProperties['maxWidth'] showCTA?: boolean } @@ -26,7 +26,7 @@ function SponsorPackWithQuery() { return } - return + return } export function LazySponsorSection({ @@ -35,29 +35,20 @@ export function LazySponsorSection({ packMaxWidth, showCTA = true, }: LazySponsorSectionProps) { - const { ref, isIntersecting } = useIntersectionObserver({ - rootMargin: '50%', // Half viewport height - triggers when about half a page away - triggerOnce: true, - }) - return ( -
+

{title}

- {!isIntersecting ? ( - - ) : ( - }> - - - )} + } + > + +
{showCTA ? (
diff --git a/src/components/home/HomeApplicationStarter.tsx b/src/components/home/HomeApplicationStarter.tsx index 5cc525460..3ee247e6f 100644 --- a/src/components/home/HomeApplicationStarter.tsx +++ b/src/components/home/HomeApplicationStarter.tsx @@ -1,95 +1,22 @@ -import * as React from 'react' +import { Hydrate } from '@tanstack/react-start' +import { idle, visible } from '@tanstack/react-start/hydration' +import { ApplicationStarter } from '~/components/ApplicationStarter' import { HomeApplicationStarterFallback } from './HomeSectionFallbacks' -const LazyApplicationStarter = React.lazy(() => - import('~/components/ApplicationStarter').then((m) => ({ - default: m.ApplicationStarter, - })), -) - export function HomeApplicationStarter() { - const wrapperRef = React.useRef(null) - const [shouldLoad, setShouldLoad] = React.useState(false) - - React.useEffect(() => { - if (shouldLoad) { - return - } - - const element = wrapperRef.current - - if (!element || typeof IntersectionObserver === 'undefined') { - setShouldLoad(true) - return - } - - const observer = new IntersectionObserver( - (entries) => { - if (!entries.some((entry) => entry.isIntersecting)) { - return - } - - React.startTransition(() => { - setShouldLoad(true) - }) - observer.disconnect() - }, - { rootMargin: '180px 0px' }, - ) - - observer.observe(element) - - return () => { - observer.disconnect() - } - }, [shouldLoad]) - - React.useEffect(() => { - if (shouldLoad || typeof window === 'undefined') { - return - } - - if (typeof window.requestIdleCallback === 'function') { - const idleId = window.requestIdleCallback( - () => { - React.startTransition(() => { - setShouldLoad(true) - }) - }, - { timeout: 3500 }, - ) - - return () => { - window.cancelIdleCallback(idleId) - } - } - - const timeoutId = window.setTimeout(() => { - React.startTransition(() => { - setShouldLoad(true) - }) - }, 2500) - - return () => { - window.clearTimeout(timeoutId) - } - }, [shouldLoad]) - return ( -
- {shouldLoad ? ( - }> - - - ) : ( - - )} -
+ } + > + + ) } diff --git a/src/components/home/HomeDeferredSection.tsx b/src/components/home/HomeDeferredSection.tsx deleted file mode 100644 index a14b6ab5e..000000000 --- a/src/components/home/HomeDeferredSection.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import * as React from 'react' -import { useIntersectionObserver } from '~/hooks/useIntersectionObserver' - -export function HomeDeferredSection({ - children, - fallback, - forceLoad = false, - preload, - rootMargin = '20%', - timeoutMs = 4000, -}: { - children: React.ReactNode - fallback: React.ReactNode - forceLoad?: boolean - preload?: () => Promise - rootMargin?: string - timeoutMs?: number -}) { - const { ref, isIntersecting } = useIntersectionObserver({ - rootMargin, - triggerOnce: true, - }) - const [shouldLoad, setShouldLoad] = React.useState(false) - - React.useLayoutEffect(() => { - if (!forceLoad || shouldLoad) { - return - } - - void preload?.() - setShouldLoad(true) - }, [forceLoad, preload, shouldLoad]) - - React.useEffect(() => { - if (forceLoad || shouldLoad || !isIntersecting) { - return - } - - void preload?.() - - React.startTransition(() => { - setShouldLoad(true) - }) - }, [forceLoad, isIntersecting, preload, shouldLoad]) - - React.useEffect(() => { - if (forceLoad || shouldLoad || typeof window === 'undefined') { - return - } - - if (typeof window.requestIdleCallback === 'function') { - const idleId = window.requestIdleCallback( - () => { - void preload?.() - - React.startTransition(() => { - setShouldLoad(true) - }) - }, - { timeout: timeoutMs }, - ) - - return () => { - window.cancelIdleCallback(idleId) - } - } - - const timeoutId = window.setTimeout(() => { - void preload?.() - - React.startTransition(() => { - setShouldLoad(true) - }) - }, timeoutMs) - - return () => { - window.clearTimeout(timeoutId) - } - }, [forceLoad, preload, shouldLoad, timeoutMs]) - - return ( -
- {shouldLoad ? ( - {children} - ) : ( - fallback - )} -
- ) -} diff --git a/src/hooks/useIntersectionObserver.ts b/src/hooks/useIntersectionObserver.ts deleted file mode 100644 index 295741ca0..000000000 --- a/src/hooks/useIntersectionObserver.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect, useRef, useState } from 'react' - -export interface UseIntersectionObserverOptions { - root?: Element | null - rootMargin?: string - threshold?: number | number[] - triggerOnce?: boolean -} - -export function useIntersectionObserver( - options: UseIntersectionObserverOptions = {}, -) { - const { - root = null, - rootMargin = '50%', // Half viewport height - triggers when about half a page away - threshold = 0, - triggerOnce = true, - } = options - - const [isIntersecting, setIsIntersecting] = useState(false) - const [hasTriggered, setHasTriggered] = useState(false) - const targetRef = useRef(null) - - useEffect(() => { - const target = targetRef.current - if (!target) return - - // If already triggered and triggerOnce is true, don't create observer - if (triggerOnce && hasTriggered) return - - const observer = new IntersectionObserver( - ([entry]) => { - const isCurrentlyIntersecting = entry.isIntersecting - setIsIntersecting(isCurrentlyIntersecting) - - if (isCurrentlyIntersecting && triggerOnce) { - setHasTriggered(true) - } - }, - { - root, - rootMargin, - threshold, - }, - ) - - observer.observe(target) - - return () => { - observer.disconnect() - } - }, [root, rootMargin, threshold, triggerOnce, hasTriggered]) - - return { - ref: targetRef, - isIntersecting: triggerOnce - ? hasTriggered || isIntersecting - : isIntersecting, - hasTriggered, - } -} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 90ab39d4f..7df054a54 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -5,6 +5,8 @@ import { createFileRoute, useRouterState, } from '@tanstack/react-router' +import { Hydrate } from '@tanstack/react-start' +import { idle, load, visible } from '@tanstack/react-start/hydration' import discordImage from '~/images/discord-logo-white.svg' import { librariesByGroup, librariesGroupNamesMap, Library } from '~/libraries' @@ -16,13 +18,16 @@ import { ArrowRight, Code2, Layers, Shield, Zap, Play } from 'lucide-react' import { YouTubeIcon } from '~/components/icons/YouTubeIcon' import { Card } from '~/components/Card' import { HomeApplicationStarter } from '~/components/home/HomeApplicationStarter' -import { HomeDeferredSection } from '~/components/home/HomeDeferredSection' import { HomeCommunityFallback, HomeNewsletterFallback, HomeSocialProofFallback, HomeStatsFallback, } from '~/components/home/HomeSectionFallbacks' +import { HomeCommunitySection } from '~/components/home/HomeCommunitySection' +import { HomeNewsletterSection } from '~/components/home/HomeNewsletterSection' +import { HomeSocialProofSection } from '~/components/home/HomeSocialProofSection' +import { HomeStatsSection } from '~/components/home/HomeStatsSection' import { Button } from '~/ui' import { seo } from '~/utils/seo' @@ -32,41 +37,6 @@ const LazyBrandContextMenu = React.lazy(() => })), ) -const loadHomeSocialProofSection = () => - import('~/components/home/HomeSocialProofSection') - -const LazyHomeSocialProofSection = React.lazy(() => - loadHomeSocialProofSection().then((m) => ({ - default: m.HomeSocialProofSection, - })), -) - -const loadHomeCommunitySection = () => - import('~/components/home/HomeCommunitySection') - -const LazyHomeCommunitySection = React.lazy(() => - loadHomeCommunitySection().then((m) => ({ - default: m.HomeCommunitySection, - })), -) - -const loadHomeNewsletterSection = () => - import('~/components/home/HomeNewsletterSection') - -const LazyHomeNewsletterSection = React.lazy(() => - loadHomeNewsletterSection().then((m) => ({ - default: m.HomeNewsletterSection, - })), -) - -const loadHomeStatsSection = () => import('~/components/home/HomeStatsSection') - -const LazyHomeStatsSection = React.lazy(() => - loadHomeStatsSection().then((m) => ({ - default: m.HomeStatsSection, - })), -) - function getDeferredSectionStage(hash: string) { const normalizedHash = hash.replace(/^#/, '') @@ -231,15 +201,13 @@ function Index() {
- } - preload={loadHomeStatsSection} - rootMargin="10%" - timeoutMs={6000} > - - + +
@@ -339,21 +307,25 @@ function Index() {
- = 1} + + deferredSectionStage >= 1 ? load() : visible({ rootMargin: '20%' }) + } + prefetch={idle({ timeout: 4000 })} fallback={} - preload={loadHomeSocialProofSection} > - - - - = 2} + + + + + deferredSectionStage >= 2 ? load() : visible({ rootMargin: '20%' }) + } + prefetch={idle({ timeout: 4000 })} fallback={} - preload={loadHomeCommunitySection} > - - + +
- = 3} + + deferredSectionStage >= 3 ? load() : visible({ rootMargin: '10%' }) + } + prefetch={idle({ timeout: 4000 })} fallback={} - preload={loadHomeNewsletterSection} - rootMargin="10%" > - - + +
)