Refactor async components to synchronous functions for improved performance

- Converted several async components to synchronous functions, including `Layout`, `AddOrdersPage`, `ProfilePage`, `SlotPage`, and `ServicePage`, enhancing rendering efficiency.
- Removed unnecessary prefetching logic and hydration boundaries, simplifying component structure and improving maintainability.
- Updated the `TelegramProvider` to return null during the initial mount instead of a loading message, streamlining the loading state handling.
- Enhanced loading state management in order-related components by adding loading spinners and data not found alerts, improving user experience during data fetching.
This commit is contained in:
vchikalkin 2025-10-07 13:28:40 +03:00
parent c7648e8bf9
commit 458a06a620
13 changed files with 73 additions and 48 deletions

View File

@ -1,6 +1,6 @@
import { TelegramProvider } from '@/providers/telegram'; import { TelegramProvider } from '@/providers/telegram';
import { type PropsWithChildren } from 'react'; import { type PropsWithChildren } from 'react';
export default async function Layout({ children }: Readonly<PropsWithChildren>) { export default function Layout({ children }: Readonly<PropsWithChildren>) {
return <TelegramProvider>{children}</TelegramProvider>; return <TelegramProvider>{children}</TelegramProvider>;
} }

View File

@ -1,4 +1,3 @@
import { getOrder } from '@/actions/api/orders';
import { Container } from '@/components/layout'; import { Container } from '@/components/layout';
import { PageHeader } from '@/components/navigation'; import { PageHeader } from '@/components/navigation';
import { import {
@ -9,23 +8,14 @@ import {
OrderStatus, OrderStatus,
} from '@/components/orders'; } from '@/components/orders';
import { type OrderPageParameters } from '@/components/orders/types'; import { type OrderPageParameters } from '@/components/orders/types';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
type Props = { params: Promise<OrderPageParameters> }; type Props = { params: Promise<OrderPageParameters> };
export default async function ProfilePage(props: Readonly<Props>) { export default async function ProfilePage(props: Readonly<Props>) {
const parameters = await props.params; const parameters = await props.params;
const documentId = parameters.documentId;
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryFn: () => getOrder({ documentId }),
queryKey: ['order', documentId],
});
return ( return (
<HydrationBoundary state={dehydrate(queryClient)}> <>
<PageHeader title="Запись" /> <PageHeader title="Запись" />
<Container> <Container>
<OrderDateTime {...parameters} /> <OrderDateTime {...parameters} />
@ -35,6 +25,6 @@ export default async function ProfilePage(props: Readonly<Props>) {
<div className="pb-24" /> <div className="pb-24" />
<OrderButtons {...parameters} /> <OrderButtons {...parameters} />
</Container> </Container>
</HydrationBoundary> </>
); );
} }

View File

@ -2,7 +2,7 @@ import { Container } from '@/components/layout';
import { PageHeader } from '@/components/navigation'; import { PageHeader } from '@/components/navigation';
import { OrderForm } from '@/components/orders'; import { OrderForm } from '@/components/orders';
export default async function AddOrdersPage() { export default function AddOrdersPage() {
return ( return (
<> <>
<PageHeader title="Новая запись" /> <PageHeader title="Новая запись" />

View File

@ -1,26 +1,16 @@
import { getSlot } from '@/actions/api/slots';
import { Container } from '@/components/layout'; import { Container } from '@/components/layout';
import { PageHeader } from '@/components/navigation'; import { PageHeader } from '@/components/navigation';
import { SlotButtons, SlotDateTime, SlotOrdersList } from '@/components/schedule'; import { SlotButtons, SlotDateTime, SlotOrdersList } from '@/components/schedule';
import { type SlotPageParameters } from '@/components/schedule/types'; import { type SlotPageParameters } from '@/components/schedule/types';
import { BookButton } from '@/components/shared/book-button'; import { BookButton } from '@/components/shared/book-button';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
type Props = { params: Promise<SlotPageParameters> }; type Props = { params: Promise<SlotPageParameters> };
export default async function SlotPage(props: Readonly<Props>) { export default async function SlotPage(props: Readonly<Props>) {
const parameters = await props.params; const parameters = await props.params;
const documentId = parameters.documentId;
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryFn: () => getSlot({ documentId }),
queryKey: ['slot', documentId],
});
return ( return (
<HydrationBoundary state={dehydrate(queryClient)}> <>
<PageHeader title="Слот" /> <PageHeader title="Слот" />
<Container> <Container>
<SlotDateTime {...parameters} /> <SlotDateTime {...parameters} />
@ -29,6 +19,6 @@ export default async function SlotPage(props: Readonly<Props>) {
<div className="pb-24" /> <div className="pb-24" />
<SlotButtons {...parameters} /> <SlotButtons {...parameters} />
</Container> </Container>
</HydrationBoundary> </>
); );
} }

View File

@ -1,28 +1,20 @@
import { getService } from '@/actions/api/services';
import { Container } from '@/components/layout'; import { Container } from '@/components/layout';
import { PageHeader } from '@/components/navigation'; import { PageHeader } from '@/components/navigation';
import { ServiceButtons, ServiceDataCard } from '@/components/profile/services'; import { ServiceButtons, ServiceDataCard } from '@/components/profile/services';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
// Тип параметров страницы // Тип параметров страницы
type Props = { params: Promise<{ serviceId: string }> }; type Props = { params: Promise<{ serviceId: string }> };
export default async function ProfilePage(props: Readonly<Props>) { export default async function ProfilePage(props: Readonly<Props>) {
const { serviceId } = await props.params; const { serviceId } = await props.params;
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryFn: () => getService({ documentId: serviceId }),
queryKey: ['service', serviceId],
});
return ( return (
<HydrationBoundary state={dehydrate(queryClient)}> <>
<PageHeader title="Услуга" /> <PageHeader title="Услуга" />
<Container className="px-0"> <Container className="px-0">
<ServiceDataCard serviceId={serviceId} /> <ServiceDataCard serviceId={serviceId} />
<ServiceButtons serviceId={serviceId} /> <ServiceButtons serviceId={serviceId} />
</Container> </Container>
</HydrationBoundary> </>
); );
} }

View File

@ -1,25 +1,29 @@
'use client'; 'use client';
import { DataNotFound } from '../shared/alert';
import { ContactRow } from '../shared/contact-row'; import { ContactRow } from '../shared/contact-row';
import { type OrderComponentProps } from './types'; import { type OrderComponentProps } from './types';
import { useOrderQuery } from '@/hooks/api/orders'; import { useOrderQuery } from '@/hooks/api/orders';
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
export function OrderContacts({ documentId }: Readonly<OrderComponentProps>) { export function OrderContacts({ documentId }: Readonly<OrderComponentProps>) {
const { data: { order } = {} } = useOrderQuery({ documentId }); const { data: { order } = {}, isLoading } = useOrderQuery({ documentId });
if (!order) return null; const noContacts = !order?.slot?.master && !order?.client;
return ( return (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<h1 className="font-bold">Участники</h1> <h1 className="font-bold">Участники</h1>
<div className="space-y-2"> <div className="space-y-2">
{order.slot?.master && ( {isLoading && <LoadingSpinner />}
{!isLoading && noContacts ? <DataNotFound title="Пользователи не найдены" /> : null}
{order?.slot?.master && (
<ContactRow <ContactRow
className="rounded-2xl bg-background p-2 px-4 dark:bg-primary/5" className="rounded-2xl bg-background p-2 px-4 dark:bg-primary/5"
description="Мастер" description="Мастер"
{...order.slot?.master} {...order.slot?.master}
/> />
)} )}
{order.client && ( {order?.client && (
<ContactRow <ContactRow
className="rounded-2xl bg-background p-2 px-4 dark:bg-primary/5" className="rounded-2xl bg-background p-2 px-4 dark:bg-primary/5"
description="Клиент" description="Клиент"

View File

@ -6,7 +6,16 @@ import { useOrderQuery } from '@/hooks/api/orders';
import { formatDate } from '@repo/utils/datetime-format'; import { formatDate } from '@repo/utils/datetime-format';
export function OrderDateTime({ documentId }: Readonly<OrderComponentProps>) { export function OrderDateTime({ documentId }: Readonly<OrderComponentProps>) {
const { data: { order } = {} } = useOrderQuery({ documentId }); const { data: { order } = {}, isLoading } = useOrderQuery({ documentId });
if (isLoading) {
return (
<div className="flex animate-pulse flex-col space-y-1">
<div className="h-5 w-28 rounded bg-muted" />
<div className="h-9 w-48 rounded bg-muted" />
</div>
);
}
if (!order) return null; if (!order) return null;

View File

@ -1,18 +1,20 @@
'use client'; 'use client';
import { DataNotFound } from '../shared/alert';
import { ServiceCard } from '../shared/service-card'; import { ServiceCard } from '../shared/service-card';
import { type OrderComponentProps } from './types'; import { type OrderComponentProps } from './types';
import { useOrderQuery } from '@/hooks/api/orders'; import { useOrderQuery } from '@/hooks/api/orders';
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
export function OrderServices({ documentId }: Readonly<OrderComponentProps>) { export function OrderServices({ documentId }: Readonly<OrderComponentProps>) {
const { data: { order } = {} } = useOrderQuery({ documentId }); const { data: { order } = {}, isLoading } = useOrderQuery({ documentId });
if (!order) return null;
return ( return (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<h1 className="font-bold">Услуги</h1> <h1 className="font-bold">Услуги</h1>
{order.services?.map( {isLoading && <LoadingSpinner />}
{!isLoading && !order?.services?.length ? <DataNotFound title="Услуги не найдены" /> : null}
{order?.services?.map(
(service) => service && <ServiceCard key={service.documentId} {...service} />, (service) => service && <ServiceCard key={service.documentId} {...service} />,
)} )}
</div> </div>

View File

@ -5,7 +5,14 @@ import { getAlert } from '@/components/shared/status';
import { useOrderQuery } from '@/hooks/api/orders'; import { useOrderQuery } from '@/hooks/api/orders';
export function OrderStatus({ documentId }: Readonly<OrderComponentProps>) { export function OrderStatus({ documentId }: Readonly<OrderComponentProps>) {
const { data: { order } = {} } = useOrderQuery({ documentId }); const { data: { order } = {}, isLoading } = useOrderQuery({ documentId });
if (isLoading)
return (
<div className="flex animate-pulse flex-col space-y-1">
<div className="h-10 w-full rounded bg-muted" />
</div>
);
return order?.state && getAlert(order.state); return order?.state && getAlert(order.state);
} }

View File

@ -57,6 +57,8 @@ export function ProfileDataCard() {
<div className="h-10 w-full rounded bg-muted" /> <div className="h-10 w-full rounded bg-muted" />
<div className="h-4 w-16 rounded bg-muted" /> <div className="h-4 w-16 rounded bg-muted" />
<div className="h-10 w-full rounded bg-muted" /> <div className="h-10 w-full rounded bg-muted" />
<div className="h-4 w-16 rounded bg-muted" />
<div className="h-10 w-full rounded bg-muted" />
<div className="h-5 w-60 rounded bg-muted" /> <div className="h-5 w-60 rounded bg-muted" />
</div> </div>
</Card> </Card>

View File

@ -12,10 +12,27 @@ type Props = {
}; };
export function ServiceDataCard({ serviceId }: Readonly<Props>) { export function ServiceDataCard({ serviceId }: Readonly<Props>) {
const { data: { service } = {} } = useServiceQuery({ documentId: serviceId }); const { data: { service } = {}, isLoading } = useServiceQuery({ documentId: serviceId });
const { cancelChanges, hasChanges, isPending, resetTrigger, saveChanges, updateField } = const { cancelChanges, hasChanges, isPending, resetTrigger, saveChanges, updateField } =
useServiceEdit(serviceId); useServiceEdit(serviceId);
if (isLoading) {
return (
<Card className="p-4">
<div className="flex animate-pulse flex-col gap-4">
<div className="h-4 w-16 rounded bg-muted" />
<div className="h-10 w-full rounded bg-muted" />
<div className="h-4 w-16 rounded bg-muted" />
<div className="h-10 w-full rounded bg-muted" />
<div className="h-4 w-16 rounded bg-muted" />
<div className="h-10 w-full rounded bg-muted" />
<div className="h-4 w-16 rounded bg-muted" />
<div className="h-28 w-full rounded bg-muted" />
</div>
</Card>
);
}
if (!service) return null; if (!service) return null;
return ( return (

View File

@ -3,12 +3,24 @@
import { type SlotComponentProps } from '../types'; import { type SlotComponentProps } from '../types';
import { SlotDate } from './slot-date'; import { SlotDate } from './slot-date';
import { SlotTime } from './slot-time'; import { SlotTime } from './slot-time';
import { useSlotQuery } from '@/hooks/api/slots';
import { ScheduleStoreProvider } from '@/stores/schedule'; import { ScheduleStoreProvider } from '@/stores/schedule';
import { withContext } from '@/utils/context'; import { withContext } from '@/utils/context';
export const SlotDateTime = withContext(ScheduleStoreProvider)(function ( export const SlotDateTime = withContext(ScheduleStoreProvider)(function (
props: Readonly<SlotComponentProps>, props: Readonly<SlotComponentProps>,
) { ) {
const { isLoading } = useSlotQuery(props);
if (isLoading) {
return (
<div className="flex animate-pulse flex-col space-y-1">
<div className="h-5 w-28 rounded bg-muted" />
<div className="h-9 w-48 rounded bg-muted" />
</div>
);
}
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<SlotDate {...props} /> <SlotDate {...props} />

View File

@ -13,7 +13,7 @@ export function TelegramProvider(props: Readonly<PropsWithChildren>) {
// side. // side.
const didMount = useDidMount(); const didMount = useDidMount();
if (!didMount) return <div>Loading</div>; if (!didMount) return null;
return <RootInner {...props} />; return <RootInner {...props} />;
} }