mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Improve loading state feedback (#202)
* Improve loading states with graceful transitions and enhanced UX Co-authored-by: matt <matt@roocode.com> * DRY it up * Visual cleanup * Cleanup * More cleanup * More cleanup --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
parent
7e2cd4b7b0
commit
9c59a4e278
12 changed files with 392 additions and 68 deletions
|
|
@ -10,7 +10,7 @@ import {
|
|||
formatNumber,
|
||||
formatTimestamp,
|
||||
} from '@/lib/formatters';
|
||||
import { Button, Skeleton } from '@/components/ui';
|
||||
import { Button } from '@/components/ui';
|
||||
import { DataTable } from '@/components/layout';
|
||||
|
||||
import type { Filter } from './types';
|
||||
|
|
@ -24,13 +24,17 @@ export const Developers = ({
|
|||
}) => {
|
||||
const { orgId } = useAuth();
|
||||
|
||||
const { data = [], isPending } = useQuery({
|
||||
const {
|
||||
data = [],
|
||||
isPending,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ['getDeveloperUsage', orgId, filters],
|
||||
queryFn: () => getDeveloperUsage({ orgId, filters }),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
const cols: ColumnDef<DeveloperUsage>[] = useMemo(
|
||||
const columns: ColumnDef<DeveloperUsage>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Developer',
|
||||
|
|
@ -93,16 +97,17 @@ export const Developers = ({
|
|||
[onFilter],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
isPending
|
||||
? cols.map((col) => ({
|
||||
...col,
|
||||
cell: () => <Skeleton className="h-9 w-full" />,
|
||||
}))
|
||||
: cols,
|
||||
[isPending, cols],
|
||||
return (
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
isPending={isPending}
|
||||
isError={isError}
|
||||
filters={filters}
|
||||
loadingMessage="Loading developers..."
|
||||
errorTitle="Failed to load developers"
|
||||
emptyTitle="No developers found"
|
||||
emptyDescription="Developer activity will appear here once team members start using the system"
|
||||
/>
|
||||
);
|
||||
|
||||
return <DataTable columns={columns} data={data} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useAuth } from '@clerk/nextjs';
|
|||
|
||||
import { type ModelUsage, getModelUsage } from '@/actions/analytics';
|
||||
import { formatCurrency, formatNumber } from '@/lib/formatters';
|
||||
import { Button, Skeleton } from '@/components/ui';
|
||||
import { Button } from '@/components/ui';
|
||||
import { DataTable } from '@/components/layout';
|
||||
|
||||
import type { Filter } from './types';
|
||||
|
|
@ -19,13 +19,17 @@ export const Models = ({
|
|||
}) => {
|
||||
const { orgId } = useAuth();
|
||||
|
||||
const { data = [], isPending } = useQuery({
|
||||
const {
|
||||
data = [],
|
||||
isPending,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ['getModelUsage', orgId, filters],
|
||||
queryFn: () => getModelUsage({ orgId, filters }),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
const cols: ColumnDef<ModelUsage>[] = useMemo(
|
||||
const columns: ColumnDef<ModelUsage>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Model',
|
||||
|
|
@ -65,16 +69,17 @@ export const Models = ({
|
|||
[onFilter],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
isPending
|
||||
? cols.map((col) => ({
|
||||
...col,
|
||||
cell: () => <Skeleton className="h-9 w-full" />,
|
||||
}))
|
||||
: cols,
|
||||
[isPending, cols],
|
||||
return (
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
isPending={isPending}
|
||||
isError={isError}
|
||||
filters={filters}
|
||||
loadingMessage="Loading models..."
|
||||
errorTitle="Failed to load models"
|
||||
emptyTitle="No models found"
|
||||
emptyDescription="Model usage data will appear here once AI models are used in tasks"
|
||||
/>
|
||||
);
|
||||
|
||||
return <DataTable columns={columns} data={data} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
formatNumber,
|
||||
formatTimestamp,
|
||||
} from '@/lib/formatters';
|
||||
import { Button, Skeleton } from '@/components/ui';
|
||||
import { Button } from '@/components/ui';
|
||||
import { DataTable } from '@/components/layout';
|
||||
|
||||
import type { Filter } from './types';
|
||||
|
|
@ -25,13 +25,17 @@ export const Repositories = ({
|
|||
}) => {
|
||||
const { orgId } = useAuth();
|
||||
|
||||
const { data = [], isPending } = useQuery({
|
||||
const {
|
||||
data = [],
|
||||
isPending,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ['getRepositoryUsage', orgId, filters],
|
||||
queryFn: () => getRepositoryUsage({ orgId, filters }),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
const cols: ColumnDef<RepositoryUsage>[] = useMemo(
|
||||
const columns: ColumnDef<RepositoryUsage>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Repository',
|
||||
|
|
@ -103,25 +107,17 @@ export const Repositories = ({
|
|||
[onFilter],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
isPending
|
||||
? cols.map((col) => ({
|
||||
...col,
|
||||
cell: () => <Skeleton className="h-9 w-full" />,
|
||||
}))
|
||||
: cols,
|
||||
[isPending, cols],
|
||||
return (
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
isPending={isPending}
|
||||
isError={isError}
|
||||
filters={filters}
|
||||
loadingMessage="Loading repositories..."
|
||||
errorTitle="Failed to load repositories"
|
||||
emptyTitle="No repositories found"
|
||||
emptyDescription="Repository data will appear here once tasks with Git context are created"
|
||||
/>
|
||||
);
|
||||
|
||||
if (data.length === 0 && !isPending) {
|
||||
return (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No Git repository data available. Repository information will appear
|
||||
here once tasks with Git repository context are created.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <DataTable columns={columns} data={data} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import type { TaskWithUser } from '@/actions/analytics';
|
||||
|
||||
import { useRealtimePolling } from '@/hooks/useRealtimePolling';
|
||||
import { useGracefulLoading } from '@/hooks/useGracefulLoading';
|
||||
import { getTasks } from '@/actions/analytics';
|
||||
import { Skeleton, CursorPaginationControls } from '@/components/ui';
|
||||
import { CursorPaginationControls } from '@/components/ui';
|
||||
import { LoadingState, ErrorState, EmptyState } from '@/components/ui/states';
|
||||
import { TaskCard } from '@/components/usage';
|
||||
import { useCursorPagination } from '@/hooks/usePagination';
|
||||
|
||||
|
|
@ -32,7 +34,7 @@ export const Tasks = ({
|
|||
// Initialize cursor-based pagination
|
||||
const pagination = useCursorPagination(100);
|
||||
|
||||
const { data, isPending } = useQuery({
|
||||
const { data, isPending, isError } = useQuery({
|
||||
queryKey: [
|
||||
'getTasksPaginated',
|
||||
orgId,
|
||||
|
|
@ -54,6 +56,13 @@ export const Tasks = ({
|
|||
...polling,
|
||||
});
|
||||
|
||||
// Use graceful loading hook
|
||||
const { showContent, isTransitioning } = useGracefulLoading({
|
||||
isPending,
|
||||
data: data?.tasks || [],
|
||||
dependencies: [filters],
|
||||
});
|
||||
|
||||
// Update cursor when we get new data
|
||||
useEffect(() => {
|
||||
if (data?.nextCursor) {
|
||||
|
|
@ -81,24 +90,26 @@ export const Tasks = ({
|
|||
|
||||
const tasks = data?.tasks || [];
|
||||
|
||||
if (isPending) {
|
||||
// Show loading state while pending or during transition period
|
||||
if (isPending || !showContent) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{Array(8)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<Skeleton key={i} className="h-14 w-full" />
|
||||
))}
|
||||
</div>
|
||||
<LoadingState
|
||||
message={isPending ? 'Loading tasks...' : 'Preparing results...'}
|
||||
isTransitioning={isTransitioning}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <ErrorState title="Failed to load tasks" />;
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No tasks have been synced yet. Check back after creating or sharing a
|
||||
task.
|
||||
</div>
|
||||
<EmptyState
|
||||
title="No tasks found"
|
||||
description="Tasks will appear here after being created or shared"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,22 +15,79 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui';
|
||||
import { useGracefulLoading } from '@/hooks/useGracefulLoading';
|
||||
import { LoadingState, ErrorState, EmptyState } from '@/components/ui/states';
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
isLoading?: boolean;
|
||||
emptyMessage?: string;
|
||||
emptyDescription?: string;
|
||||
// New props for enhanced state management
|
||||
isPending?: boolean;
|
||||
isError?: boolean;
|
||||
filters?: unknown[];
|
||||
loadingMessage?: string;
|
||||
errorTitle?: string;
|
||||
emptyTitle?: string;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
isLoading = false,
|
||||
emptyMessage = 'No results found',
|
||||
emptyDescription = 'Try adjusting your search or filter criteria',
|
||||
// New props
|
||||
isPending,
|
||||
isError,
|
||||
filters = [],
|
||||
loadingMessage = 'Loading...',
|
||||
errorTitle = 'Failed to load',
|
||||
emptyTitle,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
// Always call hooks at the top level to avoid conditional hook calls
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
// Use graceful loading if isPending is provided, otherwise fall back to isLoading
|
||||
const shouldUseGracefulLoading = isPending !== undefined;
|
||||
const { showContent, isTransitioning } = useGracefulLoading({
|
||||
isPending: isPending || false,
|
||||
data,
|
||||
dependencies: [filters],
|
||||
});
|
||||
|
||||
// Enhanced loading state management
|
||||
if (shouldUseGracefulLoading) {
|
||||
// Show loading state while pending or during transition period
|
||||
if (isPending || !showContent) {
|
||||
return (
|
||||
<LoadingState
|
||||
message={loadingMessage}
|
||||
isTransitioning={isTransitioning}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <ErrorState title={errorTitle} />;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title={emptyTitle || emptyMessage}
|
||||
description={emptyDescription}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-card border shadow rounded">
|
||||
<Table>
|
||||
|
|
@ -53,7 +110,18 @@ export function DataTable<TData, TValue>({
|
|||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
{isLoading ? (
|
||||
// Show loading rows while data is being fetched
|
||||
Array.from({ length: 5 }, (_, index) => (
|
||||
<TableRow key={`loading-${index}`}>
|
||||
{columns.map((_, colIndex) => (
|
||||
<TableCell key={`loading-cell-${colIndex}`}>
|
||||
<div className="h-4 bg-muted/20 rounded animate-pulse" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
|
|
@ -68,8 +136,32 @@ export function DataTable<TData, TValue>({
|
|||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
<TableCell colSpan={columns.length} className="h-32">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="w-10 h-10 mx-auto rounded-full bg-muted/20 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-5 h-5 text-muted-foreground/50"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{emptyMessage}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{emptyDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
|
|
|||
39
apps/web/src/components/ui/icons/StateIcons.tsx
Normal file
39
apps/web/src/components/ui/icons/StateIcons.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Collection of SVG icons used in loading, error, and empty states
|
||||
*/
|
||||
|
||||
export const ErrorIcon = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EmptyIcon = () => (
|
||||
<svg
|
||||
className="w-6 h-6 text-muted-foreground/50"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Spinner = () => (
|
||||
<div className="w-4 h-4 border-2 border-muted-foreground/30 border-t-muted-foreground rounded-full animate-spin" />
|
||||
);
|
||||
24
apps/web/src/components/ui/states/EmptyState.tsx
Normal file
24
apps/web/src/components/ui/states/EmptyState.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { StateContainer } from './StateContainer';
|
||||
import { EmptyIcon } from '../icons/StateIcons';
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable empty state component for when no data is available.
|
||||
* Uses a consistent default table icon for all empty states.
|
||||
*/
|
||||
export function EmptyState({ title, description }: EmptyStateProps) {
|
||||
return (
|
||||
<StateContainer
|
||||
icon={<EmptyIcon />}
|
||||
iconClassName="bg-muted/20"
|
||||
iconColor="text-muted-foreground/50"
|
||||
title={title}
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
apps/web/src/components/ui/states/ErrorState.tsx
Normal file
27
apps/web/src/components/ui/states/ErrorState.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { StateContainer } from './StateContainer';
|
||||
import { ErrorIcon } from '../icons/StateIcons';
|
||||
|
||||
interface ErrorStateProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable error state component with consistent styling and messaging.
|
||||
* Used when data fetching fails or encounters an error.
|
||||
*/
|
||||
export function ErrorState({
|
||||
title = 'Failed to load',
|
||||
description = 'Please try again or check your connection',
|
||||
}: ErrorStateProps) {
|
||||
return (
|
||||
<StateContainer
|
||||
icon={<ErrorIcon />}
|
||||
iconClassName="bg-destructive/10"
|
||||
iconColor="text-destructive"
|
||||
title={title}
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
apps/web/src/components/ui/states/LoadingState.tsx
Normal file
27
apps/web/src/components/ui/states/LoadingState.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Spinner } from '../icons/StateIcons';
|
||||
|
||||
interface LoadingStateProps {
|
||||
message?: string;
|
||||
transitionMessage?: string;
|
||||
isTransitioning?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable loading state component with spinner and customizable messages.
|
||||
* Shows different messages for initial loading vs transition states.
|
||||
*/
|
||||
export function LoadingState({
|
||||
message = 'Loading...',
|
||||
transitionMessage = 'Preparing data...',
|
||||
isTransitioning = false,
|
||||
}: LoadingStateProps) {
|
||||
return (
|
||||
<div className="text-center mt-8 mb-4">
|
||||
<div className="inline-flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Spinner />
|
||||
{isTransitioning ? transitionMessage : message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
apps/web/src/components/ui/states/StateContainer.tsx
Normal file
37
apps/web/src/components/ui/states/StateContainer.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
|
||||
interface StateContainerProps {
|
||||
icon: React.ReactNode;
|
||||
iconClassName?: string;
|
||||
iconColor?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared container component for loading, error, and empty states.
|
||||
* Provides consistent styling and layout across all state components.
|
||||
*/
|
||||
export function StateContainer({
|
||||
icon,
|
||||
iconClassName = 'bg-muted/20',
|
||||
iconColor = 'text-muted-foreground/50',
|
||||
title,
|
||||
description,
|
||||
}: StateContainerProps) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-center space-y-3">
|
||||
<div
|
||||
className={`w-12 h-12 mx-auto rounded-full ${iconClassName} flex items-center justify-center`}
|
||||
>
|
||||
<div className={iconColor}>{icon}</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{title}</p>
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
apps/web/src/components/ui/states/index.ts
Normal file
4
apps/web/src/components/ui/states/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { StateContainer } from './StateContainer';
|
||||
export { LoadingState } from './LoadingState';
|
||||
export { ErrorState } from './ErrorState';
|
||||
export { EmptyState } from './EmptyState';
|
||||
57
apps/web/src/hooks/useGracefulLoading.ts
Normal file
57
apps/web/src/hooks/useGracefulLoading.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface GracefulLoadingOptions {
|
||||
isPending: boolean;
|
||||
data: unknown;
|
||||
dependencies?: unknown[];
|
||||
}
|
||||
|
||||
interface GracefulLoadingReturn {
|
||||
showContent: boolean;
|
||||
isTransitioning: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to manage graceful loading transitions that prevent jarring "no data" flashes.
|
||||
* Ensures a minimum loading time before showing content and resets state when dependencies change.
|
||||
*/
|
||||
export function useGracefulLoading({
|
||||
isPending,
|
||||
data,
|
||||
dependencies = [],
|
||||
}: GracefulLoadingOptions): GracefulLoadingReturn {
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
|
||||
// Prevent flashing by ensuring minimum loading time
|
||||
useEffect(() => {
|
||||
let timeoutId: number;
|
||||
|
||||
if (!isPending && data !== undefined) {
|
||||
// Add minimum delay before showing content to prevent flash
|
||||
timeoutId = window.setTimeout(() => {
|
||||
setShowContent(true);
|
||||
}, 400);
|
||||
} else {
|
||||
setShowContent(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [isPending, data]);
|
||||
|
||||
// Reset show content when dependencies change (e.g., filters)
|
||||
const dependenciesKey = JSON.stringify(dependencies);
|
||||
useEffect(() => {
|
||||
setShowContent(false);
|
||||
}, [dependenciesKey]);
|
||||
|
||||
return {
|
||||
showContent,
|
||||
isTransitioning: !isPending && !showContent,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue