Loading Page Guide
Loading pages are Next.js files (loading.tsx) that show skeleton placeholders while the real page loads. They mirror the structure of their corresponding page — same layout components, same spacing, same hierarchy — but replace dynamic content with skeleton components.
Workflow
- Read the corresponding
page.tsxto understand its structure - Identify the page type using the decision tree
- Generate the loading file(s) using the matching pattern from loading-patterns.md
Decision Tree
Analyze what the page imports and renders:
- List page? (imports a list component, uses
Suspense, hasforce-dynamic) → List Loading - Form sub-page? (imports a Form component or uses
FormLayout) → Form Sub-Page Loading - Detail overview? (imports
TaskGroup/TaskPanel, shows grouped tasks) → Detail Overview Loading - Description list page? (imports
DescriptionsList) → Description List Loading - Cards grid page? (imports
CardsGridLayoutor renders card panels) → Cards Grid Loading - Custom content page? (documents, contracts, timeline, etc.) → Custom Content Loading
File Structure
Loading pages follow a two-file pattern:
1. Page-level loading.tsx (in src/app/...)
Thin wrapper that imports and renders the feature-level loading component.
import { FeatureLoading } from "@/features/{feature}/components/FeatureLoading";
export default function PageNameLoading() {
return <FeatureLoading />;
}
2. Feature-level loading component (in src/features/...)
Contains the actual skeleton layout. Lives alongside the feature's other components.
Exception — List pages: Only need the page-level loading.tsx since they delegate directly to the shared ListLoading component. No feature-level component needed.
Available Skeleton Components
| Component | Import | Purpose |
|-----------|--------|---------|
| BoxSkeleton | @finstreet/ui/components/base/Skeletons/BoxSkeleton | Rectangular placeholder for content blocks |
| TextSkeleton | @finstreet/ui/components/base/Skeletons/TextSkeleton | Animated text lines (lines, fontSize props) |
| AvatarSkeleton | @finstreet/ui/components/base/Skeletons/AvatarSkeleton | Circular placeholder (size prop) |
Shared Loading Components
| Component | Import | Use for |
|-----------|--------|---------|
| ListLoading | @/shared/components/ListLoading | List pages — accepts title prop |
| FormSkeleton | @/shared/components/FormSkeleton | Form pages — renders form field placeholders, no props |
| SubPageHeaderSkeleton | @/shared/components/SubPageHeaderSkeleton | Sub-pages — accepts title prop, renders back button + title + text skeleton |
Server vs Client Components
- Server (async): Use
getTranslations()fromnext-intl/server. Export asasync function. - Client: Add
"use client"directive. UseuseTranslations()fromnext-intl.
Choose based on what the component needs:
- Only needs translations → prefer async server component with
getTranslations() - Needs hooks (
useParams, etc.) → client component withuseTranslations()
Patterns
See loading-patterns.md for detailed code templates and examples of each loading type.
Rules
- The loading page must mirror the structure of its corresponding page — same layout hierarchy, same spacing
- Use shared components (
ListLoading,FormSkeleton,SubPageHeaderSkeleton) whenever the pattern matches — do not reinvent them - Translation namespaces must match the actual page's translation namespace
- Page-level
loading.tsxfunction name:{RouteSegments}Loading(PascalCase from route path) - Feature-level component name:
{FeatureName}Loading - Do NOT import or call data-fetching functions — loading pages have no data
- Do NOT search the project for existing loading pages or dependencies
- Match gap sizes, grid column counts, and layout areas from the actual page
- Skeleton dimensions should approximate the content they replace (e.g., a card that's ~200px tall →
BoxSkeleton height="200") - When the page has a description
Typographywith a translation key, the loading page should render that same translated text — descriptions are static and known at load time