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 { type PropsWithChildren } from 'react';
export default async function Layout({ children }: Readonly<PropsWithChildren>) {
export default function Layout({ children }: Readonly<PropsWithChildren>) {
return <TelegramProvider>{children}</TelegramProvider>;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,16 @@ import { useOrderQuery } from '@/hooks/api/orders';
import { formatDate } from '@repo/utils/datetime-format';
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;

View File

@ -1,18 +1,20 @@
'use client';
import { DataNotFound } from '../shared/alert';
import { ServiceCard } from '../shared/service-card';
import { type OrderComponentProps } from './types';
import { useOrderQuery } from '@/hooks/api/orders';
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
export function OrderServices({ documentId }: Readonly<OrderComponentProps>) {
const { data: { order } = {} } = useOrderQuery({ documentId });
if (!order) return null;
const { data: { order } = {}, isLoading } = useOrderQuery({ documentId });
return (
<div className="flex flex-col space-y-2">
<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} />,
)}
</div>

View File

@ -5,7 +5,14 @@ import { getAlert } from '@/components/shared/status';
import { useOrderQuery } from '@/hooks/api/orders';
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);
}

View File

@ -57,6 +57,8 @@ export function ProfileDataCard() {
<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-5 w-60 rounded bg-muted" />
</div>
</Card>

View File

@ -12,10 +12,27 @@ type 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 } =
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;
return (

View File

@ -3,12 +3,24 @@
import { type SlotComponentProps } from '../types';
import { SlotDate } from './slot-date';
import { SlotTime } from './slot-time';
import { useSlotQuery } from '@/hooks/api/slots';
import { ScheduleStoreProvider } from '@/stores/schedule';
import { withContext } from '@/utils/context';
export const SlotDateTime = withContext(ScheduleStoreProvider)(function (
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 (
<div className="flex flex-col">
<SlotDate {...props} />

View File

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