Skip to content
Back to skills

Widget Slot Architecture

ASecurity

Architecture guide using Next.js App Router's Parallel Routes for Widget-Slot pattern. Separates static layouts from dynamic widgets to achieve separation of concerns, fault isolation, and plug-and-play development.

  • 31 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added February 7, 2026
developmentgoshellreactnextjsnodetestingapidatabasefrontendperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add JeremyDev87/codingbuddy --skill widget-slot-architecture --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Widget Slot Architecture?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Widget Slot Architecture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jeremydev87-widget-slot-architecture/badge)](https://www.skillsdirectory.com/skills/jeremydev87-widget-slot-architecture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: widget-slot-architecture
description: Architecture guide using Next.js App Router's Parallel Routes for Widget-Slot pattern. Separates static layouts from dynamic widgets to achieve separation of concerns, fault isolation, and plug-and-play development.
---

# Widget-Slot Architecture (WSA)

## Overview

Widget-Slot Architecture (WSA) is a frontend architecture pattern that leverages Next.js App Router's Parallel Routes feature to **strictly separate interaction logic from static layouts** and manage each feature as an independent widget unit.

**Core Principle:** Layouts handle structure only; logic is isolated in widgets.

**Requirements:** Next.js 13.4+ (App Router with Parallel Routes support)

## When to Use

**Use this skill:**
- Designing large-scale Next.js App Router projects
- When multiple independent dynamic regions are needed within a page
- Projects requiring team-based parallel development
- Services where feature-level fault isolation is critical
- When widget reusability and portability are required

**Not needed for:**
- Simple static pages
- Small projects (over-abstraction)
- Pages Router based projects (Parallel Routes not supported)

---

## Philosophy (5 Core Principles)

### 1. Separation of Concerns and Static Shell Maintenance
Layouts provide only the structural framework and contain no complex interactions. All dynamic elements are isolated in widgets.

### 2. Independent Self-Containment
Widgets minimize external dependencies and handle their own data. A widget should work immediately when moved to a different page's slot.

### 3. Standardized Fault Isolation
All dynamic regions (Slots) have independent Error Boundaries and Suspense. A feature's error should not cause the entire page to collapse.

### 4. Server-Centric Data Flow
Widget communication prioritizes server data cache invalidation (`revalidateTag`) or URL state over direct state sharing.

### 5. Plug & Play Development
Widgets are designed like Lego blocks. Developers have extreme freedom to 'add' new features to slots or 'remove' unnecessary ones without affecting business logic.

---

## Definitions

| Term | Definition |
|------|------------|
| **Layout** | Static structure of the service. Contains only visual elements without interactions and determines Slot positions |
| **Slot** | Dynamic region defined within a Layout. Implemented via Next.js Parallel Routes (`@slot`), managing individual loading and error states |
| **Widget** | Actual business logic unit inserted into a Slot. Includes API calls, data mutations (Actions), and user interactions (Forms, Modals, etc.) |
| **Component** | Reusable UI pieces used generically. Has no business logic or only very simple input elements |

---

## Folder Structure

```
src/
├── app/
│   ├── layout.tsx                # Overall static structure definition
│   ├── @main_slot/               # [Slot] Specific position definition
│   │   ├── layout.tsx            # Slot common wrapper (Suspense, ErrorBoundary)
│   │   ├── page.tsx              # Widget connection (calls Widgets/MyWidget)
│   │   ├── loading.tsx           # Slot-specific loading UI
│   │   ├── error.tsx             # Slot-specific error UI
│   │   └── default.tsx           # Placeholder when no data or route mismatch
│   └── (routes)/...
├── Widgets/                      # [Widget] Common widget storage (src/Widgets)
│   └── [WidgetName]/             # Widget folder by name (Colocation)
│       ├── index.tsx             # Widget Entry Point
│       ├── ui/                   # UI component folder (using barrel files)
│       ├── hooks/                # Widget-specific custom hooks
│       └── actions/              # Widget-specific Server Actions
└── Components/                   # Global common UI (Atomic Design's Atoms/Molecules level)
```

---

## Implementation Guide

### 4.1 Slot Implementation (Parallel Routes)

Slots are implemented via Parallel Routes using the `@` prefix. Each Slot directly manages its own lifecycle (Loading, Error).

**Slot Layout (Error Boundary & Suspense applied):**

```tsx
// app/@main_slot/layout.tsx
export default function MainSlotLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div className="slot-container">
      {/* Next.js provided loading.tsx and error.tsx wrap children */}
      {children}
    </div>
  );
}
```

**Slot Page (Widget connection):**

```tsx
// app/@main_slot/page.tsx
import UserProfileWidget from "@/Widgets/UserProfile";

// Slot's page.tsx only performs the role of calling a single Widget
export default function MainSlotPage() {
  return <UserProfileWidget userId="current-user" />;
}
```

**Slot Loading:**

```tsx
// app/@main_slot/loading.tsx
export default function MainSlotLoading() {
  return <div className="animate-pulse">Loading...</div>;
}
```

**Slot Error:**

```tsx
// app/@main_slot/error.tsx
'use client';

export default function MainSlotError({
  error,
  reset
}: {
  error: Error;
  reset: () => void
}) {
  return (
    <div className="error-container">
      <p>Something went wrong in this section.</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
```

**Slot Default (Fallback):**

```tsx
// app/@main_slot/default.tsx
export default function MainSlotDefault() {
  return <div>No content available</div>;
}
```

### 4.2 Widget Implementation (Colocation & Barrel Files)

Widgets are located within the `Widgets/` folder, with sub-elements organized by feature folders.

**Widget Entry Point:**

```tsx
// Widgets/UserProfile/index.tsx
import { ProfileUI } from "./ui";
import { getUserData } from "./actions";

// Widgets aim to be Server Components, calling Client Component UI internally when needed
export default async function UserProfileWidget({
  userId
}: {
  userId: string
}) {
  const userData = await getUserData(userId);

  if (!userData) {
    return <div>User not found.</div>;
  }

  return <ProfileUI data={userData} />;
}
```

**Widget UI (Client Component):**

```tsx
// Widgets/UserProfile/ui/ProfileUI.tsx
'use client';

import { User } from "../types";

interface ProfileUIProps {
  data: User;
}

export function ProfileUI({ data }: ProfileUIProps) {
  return (
    <div className="profile-card">
      <h2>{data.name}</h2>
      <p>{data.email}</p>
    </div>
  );
}
```

**Widget UI Barrel File:**

```tsx
// Widgets/UserProfile/ui/index.ts
export { ProfileUI } from "./ProfileUI";
export { ProfileSkeleton } from "./ProfileSkeleton";
```

**Widget Server Actions:**

```tsx
// Widgets/UserProfile/actions/getUserData.ts
'use server';

import { cache } from 'react';

export const getUserData = cache(async (userId: string) => {
  const res = await fetch(`/api/users/${userId}`, {
    next: { tags: [`user-${userId}`] }
  });

  if (!res.ok) return null;
  return res.json();
});
```

**Widget Folder Structure:**

```
Widgets/UserProfile/
├── index.tsx           # Widget Entry Point (Server Component)
├── types.ts            # Widget-specific types
├── ui/                 # UI Components (Client Components)
│   ├── index.ts        # Barrel file
│   ├── ProfileUI.tsx
│   └── ProfileSkeleton.tsx
├── hooks/              # Widget-specific hooks
│   └── useProfileEdit.ts
└── actions/            # Server Actions
    ├── getUserData.ts
    └── updateUserData.ts
```

### 4.3 Component Management and Promotion Rules

| Stage | Location | Criteria |
|-------|----------|----------|
| **Local Component** | Within widget/page `ui/` | Used temporarily only in specific widget |
| **Global Component** | `src/Components/` | Interaction-free UI reused across multiple places |
| **Widget Promotion** | `src/Widgets/` | When business logic is added or operates as independent functional unit |

**Promotion Triggers:**
- When data fetch is added to `src/Components/` elements
- When Server Action is needed
- When operating as independent functional unit (Form, complex Modal, etc.)

### 4.4 Combining with Intercepting Routes

Parallel Routes and Intercepting Routes can be combined to implement modal patterns. Display as modal when clicking an item from a list, display as full page when accessing URL directly.

**Folder Structure:**

```
app/
├── layout.tsx
├── @modal/                      # Parallel Route for modals
│   ├── (.)items/[id]/          # Intercept: catch same-level route
│   │   └── page.tsx            # Display as modal
│   └── default.tsx             # When no modal (return null)
├── items/
│   ├── page.tsx                # List page
│   └── [id]/
│       └── page.tsx            # Full page on direct access
└── page.tsx
```

**Root Layout (including modal slot):**

```tsx
// app/layout.tsx
export default function RootLayout({
  children,
  modal
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html>
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}
```

**Intercepting Route (modal display):**

```tsx
// app/@modal/(.)items/[id]/page.tsx
import ItemDetailWidget from "@/Widgets/ItemDetail";
import { Modal } from "@/Components/Modal";

export default function ItemModal({ params }: { params: { id: string } }) {
  return (
    <Modal>
      <ItemDetailWidget itemId={params.id} variant="compact" />
    </Modal>
  );
}
```

**Modal Default (when modal inactive):**

```tsx
// app/@modal/default.tsx
export default function ModalDefault() {
  return null;  // Render nothing when no modal
}
```

**Intercepting Route Rules:**

| Pattern | Description |
|---------|-------------|
| `(.)` | Intercept same-level segment |
| `(..)` | Intercept one level up segment |
| `(..)(..)` | Intercept two levels up segment |
| `(...)` | Intercept from root (`app`) |

---

## Core Rules (5 Key Rules)

### Rule 1: One Slot, One Widget
A `@slot`'s `page.tsx` should import and return **only one Widget** in principle.

```tsx
// ✅ Good
export default function SlotPage() {
  return <UserProfileWidget userId="123" />;
}

// ❌ Bad - Multiple widgets mixed
export default function SlotPage() {
  return (
    <>
      <UserProfileWidget />
      <UserSettingsWidget />
    </>
  );
}
```

### Rule 2: Data Self-Sufficiency
Widgets are recommended to **fetch required data internally**. Only receive minimal props like identifiers (IDs) from parent (Layout).

```tsx
// ✅ Good - Receive only ID, fetch data inside widget
export default async function OrderWidget({ orderId }: { orderId: string }) {
  const order = await getOrderData(orderId);
  return <OrderUI order={order} />;
}

// ❌ Bad - Parent passes data
export default function OrderWidget({ order }: { order: Order }) {
  return <OrderUI order={order} />;
}
```

### Rule 3: Minimize Communication
Avoid direct state sharing between widgets. When changing data, use **`revalidateTag`** to refresh server state or design other widgets to react via **URL Query String**.

```tsx
// ✅ Good - Cache invalidation via Server Action
'use server';
import { revalidateTag } from 'next/cache';

export async function updateUser(userId: string, data: UserInput) {
  await db.users.update(userId, data);
  revalidateTag(`user-${userId}`);  // Related widgets auto re-fetch
}

// ✅ Good - Inter-widget communication via URL state
// /dashboard?selectedUser=123
// UserListWidget changes URL → UserDetailWidget reacts via searchParams
```

### Rule 4: Ensure Default State
`default.tsx` must always be defined to return appropriate messages or Null UI so layouts don't break when there's no data.

```tsx
// app/@sidebar/default.tsx
export default function SidebarDefault() {
  return (
    <aside className="sidebar-placeholder">
      <p>Select an item to view details</p>
    </aside>
  );
}
```

### Rule 5: Widgets Must Be Portable
Widgets should work immediately without additional modifications when moved to a different page's slot.

```tsx
// app/dashboard/@profile/page.tsx
import UserProfileWidget from "@/Widgets/UserProfile";
export default function DashboardProfileSlot() {
  return <UserProfileWidget userId="current-user" />;
}

// app/admin/@profile/page.tsx - Reuse same widget in different slot
import UserProfileWidget from "@/Widgets/UserProfile";
export default function AdminProfileSlot({ params }: { params: { userId: string } }) {
  return <UserProfileWidget userId={params.userId} />;
}
```

---

## Checklist

### Design Phase
- [ ] Dynamic regions (Slots) in page identified
- [ ] Widgets corresponding to each Slot defined
- [ ] Data dependencies between Widgets analyzed
- [ ] URL state vs Server State usage decided

### Implementation Phase
- [ ] `@slot/` directory structure created (layout, page, loading, error, default)
- [ ] Widgets colocated in `Widgets/[Name]/` structure
- [ ] Widgets start as Server Components, separate Client Components when needed
- [ ] Server Actions separated in `actions/` folder
- [ ] UI components managed in `ui/` folder with barrel files

### Verification Phase
- [ ] Each Slot has independent loading/error states verified
- [ ] Widgets work when moved to different Slots verified
- [ ] One Widget's error doesn't affect other regions verified
- [ ] `default.tsx` defined for all Slots verified

---

## Testing Strategy

Widgets must be independently testable. Test Server Component Widgets and Client Component UIs separately.

### Widget Test Structure

```
Widgets/UserProfile/
├── __tests__/
│   ├── UserProfile.test.tsx      # Widget integration test
│   └── ui/
│       └── ProfileUI.test.tsx    # UI component unit test
├── index.tsx
└── ui/
    └── ProfileUI.tsx
```

### Server Component Widget Testing

Mock APIs with MSW (Mock Service Worker) and call Server Components directly.

```tsx
// Widgets/UserProfile/__tests__/UserProfile.test.tsx
import { render, screen } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import UserProfileWidget from '../index';

const server = setupServer(
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Test User',
      email: 'test@example.com'
    });
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('UserProfileWidget', () => {
  it('renders user data correctly', async () => {
    // Render by directly awaiting Server Component
    const Component = await UserProfileWidget({ userId: '123' });
    render(Component);

    expect(await screen.findByText('Test User')).toBeInTheDocument();
    expect(screen.getByText('test@example.com')).toBeInTheDocument();
  });

  it('shows not found message for invalid user', async () => {
    server.use(
      http.get('/api/users/:id', () => {
        return new HttpResponse(null, { status: 404 });
      })
    );

    const Component = await UserProfileWidget({ userId: 'invalid' });
    render(Component);

    expect(screen.getByText('User not found.')).toBeInTheDocument();
  });
});
```

### Client Component UI Testing

Test UI components independently based on props.

```tsx
// Widgets/UserProfile/__tests__/ui/ProfileUI.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { ProfileUI } from '../../ui/ProfileUI';

describe('ProfileUI', () => {
  const mockUser = {
    id: '1',
    name: 'John Doe',
    email: 'john@example.com'
  };

  it('displays user information', () => {
    render(<ProfileUI data={mockUser} />);

    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });

  it('handles edit button click', () => {
    const onEdit = vi.fn();
    render(<ProfileUI data={mockUser} onEdit={onEdit} />);

    fireEvent.click(screen.getByRole('button', { name: /edit/i }));
    expect(onEdit).toHaveBeenCalledWith(mockUser.id);
  });
});
```

### Server Action Testing

Test Server Actions by calling them directly.

```tsx
// Widgets/UserProfile/__tests__/actions/updateUser.test.ts
import { updateUser } from '../../actions/updateUser';
import { revalidateTag } from 'next/cache';

vi.mock('next/cache', () => ({
  revalidateTag: vi.fn()
}));

describe('updateUser action', () => {
  it('updates user and revalidates cache', async () => {
    const result = await updateUser('123', { name: 'Updated Name' });

    expect(result.success).toBe(true);
    expect(revalidateTag).toHaveBeenCalledWith('user-123');
  });
});
```

### Testing Best Practices

| Practice | Description |
|----------|-------------|
| **Widget Isolation** | Each Widget must be independently testable |
| **API Mocking** | Mock API responses with MSW, no real server calls |
| **User-Centric** | Test from user perspective with Testing Library |
| **No Implementation Details** | Test behavior, not internal implementation |
| **Minimize Snapshots** | Use snapshot tests only for essential UI |

---

## Expected Benefits

| Benefit | Description |
|---------|-------------|
| **Parallelism** | Developers work on independent widgets per slot, minimizing conflicts |
| **Readability** | Page structure visible from layout files, actual logic cohesive in widget folders |
| **Flexibility** | Moving or replacing widgets is very simple when requirements change |
| **Fault Isolation** | One widget's error doesn't bring down the entire page |
| **Performance** | Independent loading possible via Parallel Routes streaming |

---

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Business logic in Layout | Move all logic to Widgets |
| Excessive props to Widget | Pass only ID, fetch data inside Widget |
| Direct state sharing | Use `revalidateTag` or URL params |
| Missing `default.tsx` | Required for all Slots |
| Multiple Widgets in one Slot | Follow one slot, one widget principle |
| Using Client Component as Widget | Prioritize Server Component, separate only UI as Client |

---

## Architecture Diagram

### Overall Structure

```mermaid
graph TB
    subgraph "app/ (Next.js App Router)"
        Layout[layout.tsx<br/>Static Shell]

        subgraph "@slot_a"
            SlotA_Layout[layout.tsx]
            SlotA_Page[page.tsx]
            SlotA_Loading[loading.tsx]
            SlotA_Error[error.tsx]
        end

        subgraph "@slot_b"
            SlotB_Layout[layout.tsx]
            SlotB_Page[page.tsx]
            SlotB_Loading[loading.tsx]
            SlotB_Error[error.tsx]
        end
    end

    subgraph "Widgets/"
        WidgetA[WidgetA/index.tsx<br/>Server Component]
        WidgetB[WidgetB/index.tsx<br/>Server Component]

        subgraph "Widget Internal"
            UI[ui/<br/>Client Components]
            Actions[actions/<br/>Server Actions]
            Hooks[hooks/<br/>Custom Hooks]
        end
    end

    Layout --> SlotA_Layout
    Layout --> SlotB_Layout
    SlotA_Page --> WidgetA
    SlotB_Page --> WidgetB
    WidgetA --> UI
    WidgetA --> Actions
```

### Data Flow

```mermaid
sequenceDiagram
    participant User
    participant SlotPage as @slot/page.tsx
    participant Widget as Widget (Server)
    participant Action as Server Action
    participant API as API/Database
    participant UI as UI (Client)

    User->>SlotPage: Page access
    SlotPage->>Widget: Pass ID props
    Widget->>API: Data fetch (with tags)
    API-->>Widget: Return data
    Widget->>UI: Pass data props
    UI-->>User: Render UI

    User->>UI: User action (click)
    UI->>Action: Call Server Action
    Action->>API: Update data
    Action->>Action: revalidateTag()
    Note over Widget,UI: Auto re-fetch<br/>via cache invalidation
    Widget->>API: Re-request (auto)
    API-->>Widget: New data
    Widget->>UI: Updated UI
```

### Inter-Widget Communication Patterns

```mermaid
flowchart LR
    subgraph "Pattern 1: Server Cache"
        W1[Widget A] -->|revalidateTag| Cache[(Server Cache)]
        Cache -->|auto re-fetch| W2[Widget B]
    end

    subgraph "Pattern 2: URL State"
        W3[Widget C] -->|router.push| URL[URL Params]
        URL -->|searchParams| W4[Widget D]
    end
```

---

## Quick Reference

```
WSA Structure
────────────────────────────────────
app/
├── layout.tsx          # Static shell (no logic)
├── @slot/
│   ├── layout.tsx      # Slot wrapper
│   ├── page.tsx        # Widget connector
│   ├── loading.tsx     # Loading UI
│   ├── error.tsx       # Error UI
│   └── default.tsx     # Fallback UI

Widgets/
└── [Name]/
    ├── index.tsx       # Entry (Server Component)
    ├── ui/             # Client Components
    ├── actions/        # Server Actions
    └── hooks/          # Custom hooks

Data Flow
────────────────────────────────────
Parent → Widget    : ID only (minimal props)
Widget → Data      : Internal fetch (self-contained)
Widget ↔ Widget    : revalidateTag / URL params
Widget → Server    : Server Actions

Key Rules
────────────────────────────────────
1. One Slot = One Widget
2. Data Self-Sufficiency
3. Minimize Direct Communication
4. Always Define default.tsx
5. Widgets Must Be Portable
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…