From 8d41295f2cc9c1bcf94ff8da9743fd206c8a9a19 Mon Sep 17 00:00:00 2001 From: shadcn Date: Mon, 6 Apr 2026 16:45:17 +0400 Subject: [PATCH] fix: create page perf --- .../(app)/create/components/customizer.tsx | 9 +++- .../v4/app/(app)/create/hooks/use-history.tsx | 44 ++++++++++++++++--- apps/v4/app/(app)/create/hooks/use-locks.tsx | 9 +++- apps/v4/app/(app)/create/page.tsx | 23 +++++++--- 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/apps/v4/app/(app)/create/components/customizer.tsx b/apps/v4/app/(app)/create/components/customizer.tsx index c462f73c7c..91ba9db7b6 100644 --- a/apps/v4/app/(app)/create/components/customizer.tsx +++ b/apps/v4/app/(app)/create/components/customizer.tsx @@ -1,6 +1,7 @@ "use client" import * as React from "react" +import dynamic from "next/dynamic" import { type RegistryItem } from "shadcn/schema" import { useIsMobile } from "@/hooks/use-mobile" @@ -22,7 +23,6 @@ import { FontPicker } from "@/app/(app)/create/components/font-picker" import { IconLibraryPicker } from "@/app/(app)/create/components/icon-library-picker" import { MainMenu } from "@/app/(app)/create/components/main-menu" import { MenuColorPicker } from "@/app/(app)/create/components/menu-picker" -import { ProjectForm } from "@/app/(app)/create/components/project-form" import { RadiusPicker } from "@/app/(app)/create/components/radius-picker" import { RandomButton } from "@/app/(app)/create/components/random-button" import { ResetDialog } from "@/app/(app)/create/components/reset-button" @@ -32,6 +32,13 @@ import { V0Button } from "@/app/(app)/create/components/v0-button" import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/create/lib/fonts" import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params" +// Only visible when user clicks "Create Project". +const ProjectForm = dynamic(() => + import("@/app/(app)/create/components/project-form").then( + (m) => m.ProjectForm + ) +) + export function Customizer({ itemsByBase, }: { diff --git a/apps/v4/app/(app)/create/hooks/use-history.tsx b/apps/v4/app/(app)/create/hooks/use-history.tsx index 8612005bf0..ad7f341431 100644 --- a/apps/v4/app/(app)/create/hooks/use-history.tsx +++ b/apps/v4/app/(app)/create/hooks/use-history.tsx @@ -1,7 +1,8 @@ "use client" import * as React from "react" -import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { Suspense } from "react" +import { useRouter, useSearchParams } from "next/navigation" type HistoryContextValue = { canGoBack: boolean @@ -12,12 +13,28 @@ type HistoryContextValue = { const HistoryContext = React.createContext(null) -export function HistoryProvider({ children }: { children: React.ReactNode }) { - const router = useRouter() - const pathname = usePathname() +// Reads useSearchParams() in its own Suspense boundary so the +// provider never blanks out children while search params resolve. +function PresetSync({ + onPresetChange, +}: { + onPresetChange: (preset: string) => void +}) { const searchParams = useSearchParams() const preset = searchParams.get("preset") ?? "" + React.useEffect(() => { + onPresetChange(preset) + }, [preset, onPresetChange]) + + return null +} + +export function HistoryProvider({ children }: { children: React.ReactNode }) { + const router = useRouter() + + const [preset, setPreset] = React.useState("") + const entriesRef = React.useRef([preset]) const indexRef = React.useRef(0) const maxIndexRef = React.useRef(0) @@ -26,6 +43,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) { const [index, setIndex] = React.useState(0) const [maxIndex, setMaxIndex] = React.useState(0) + const onPresetChange = React.useCallback((nextPreset: string) => { + setPreset(nextPreset) + }, []) + React.useEffect(() => { if (isNavigatingRef.current) { isNavigatingRef.current = false @@ -67,9 +88,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) { } else { params.delete("preset") } + const pathname = window.location.pathname const query = params.toString() router.replace(query ? `${pathname}?${query}` : pathname) - }, [pathname, router]) + }, [router]) const goForward = React.useCallback(() => { if (indexRef.current >= maxIndexRef.current) { @@ -88,9 +110,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) { } else { params.delete("preset") } + const pathname = window.location.pathname const query = params.toString() router.replace(query ? `${pathname}?${query}` : pathname) - }, [pathname, router]) + }, [router]) React.useEffect(() => { const down = (e: KeyboardEvent) => { @@ -133,7 +156,14 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) { [canGoBack, canGoForward, goBack, goForward] ) - return {children} + return ( + + + + + {children} + + ) } export function useHistory() { diff --git a/apps/v4/app/(app)/create/hooks/use-locks.tsx b/apps/v4/app/(app)/create/hooks/use-locks.tsx index 3949df7324..2cc1597aba 100644 --- a/apps/v4/app/(app)/create/hooks/use-locks.tsx +++ b/apps/v4/app/(app)/create/hooks/use-locks.tsx @@ -24,10 +24,15 @@ const LocksContext = React.createContext(null) export function LocksProvider({ children }: { children: React.ReactNode }) { const [locks, setLocks] = React.useState>(new Set()) + const locksRef = React.useRef(locks) + React.useEffect(() => { + locksRef.current = locks + }, [locks]) + // Stable callback — reads from ref so it doesn't change on every lock toggle. const isLocked = React.useCallback( - (param: LockableParam) => locks.has(param), - [locks] + (param: LockableParam) => locksRef.current.has(param), + [] ) const toggleLock = React.useCallback((param: LockableParam) => { diff --git a/apps/v4/app/(app)/create/page.tsx b/apps/v4/app/(app)/create/page.tsx index 40f6053168..82c5bee09e 100644 --- a/apps/v4/app/(app)/create/page.tsx +++ b/apps/v4/app/(app)/create/page.tsx @@ -1,13 +1,21 @@ +import { Suspense } from "react" import { type Metadata } from "next" +import dynamic from "next/dynamic" import { siteConfig } from "@/lib/config" import { absoluteUrl } from "@/lib/utils" import { Customizer } from "@/app/(app)/create/components/customizer" import { PresetHandler } from "@/app/(app)/create/components/preset-handler" import { Preview } from "@/app/(app)/create/components/preview" -import { WelcomeDialog } from "@/app/(app)/create/components/welcome-dialog" import { getAllItems } from "@/app/(app)/create/lib/api" +// Only shown on first visit (checks localStorage). +const WelcomeDialog = dynamic(() => + import("@/app/(app)/create/components/welcome-dialog").then( + (m) => m.WelcomeDialog + ) +) + export const metadata: Metadata = { title: "New Project", description: @@ -37,9 +45,7 @@ export const metadata: Metadata = { }, } -export default async function CreatePage() { - const itemsByBase = await getAllItems() - +export default function CreatePage() { return (
- + + +
) } + +async function CustomizerLoader() { + const itemsByBase = await getAllItems() + return +}