# Instatic Content Workspace Collections UI Architecture: A Deep Dive into the Hook-Driven Design

> Explore the Instatic content workspace collections UI architecture, driven by a unified hook for data management and CRUD operations. Learn how this design simplifies complex UI interactions.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: architecture
- Published: 2026-07-29

---

**The Instatic content workspace collections UI architecture centers on a single `useContentWorkspace` hook that acts as the single source of truth for collection data, selection state, and CRUD operations, while specialized presentational components in the sidebar, toolbar, and dialogs consume this unified data layer.**

The *Content* workspace in Instatic is where editors manage **collections**—the post types that store CMS entries. Understanding how the UI orchestrates collection loading, selection, and mutation helps developers extend the admin interface or build custom collection management features. This architecture follows a clean separation between data logic and presentation, implemented through React hooks and modular components.

## Core Data Layer: The useContentWorkspace Hook

At the heart of the Instatic content workspace collections UI architecture lies the `useContentWorkspace` hook located in [`src/admin/pages/content/hooks/useContentWorkspace.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/hooks/useContentWorkspace.ts). This hook serves as the **single source of truth** for all collection-related state.

### Loading and Selection

The hook initializes by fetching all tables where `kind === 'postType'` via the `loadCollections()` function. It maintains two critical pieces of state:

- **`collections`**: An array of collection objects representing the available post types
- **`selectedCollectionId`**: The ID of the currently active collection

The hook computes a derived `selectedCollection` value based on the current selection, ensuring components always access consistent data without additional memoization (React Compiler handles optimization).

### CRUD Operations and Entry Helpers

The hook exposes atomic operations that mutate local state and persist changes to the CMS API:

- **`createCollection`**: Wraps `createCmsDataTable` and updates the local `collections` array
- **`updateCollection`**: Calls `updateCmsDataTable` and merges changes into state
- **`deleteCollection`: Invokes `deleteCmsDataTable` and filters the local array
- **Entry-level helpers**: Functions like `moveEntryToCollection` and `createEntry` delegate to a generic `withEntryOp` helper while keeping the workspace synchronized

All setters use standard React `useState` calls, returning a stable object that UI components can destructure safely.

## Page Layout and Navigation Structure

The top-level [`ContentPage.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ContentPage.tsx) ([`src/admin/pages/content/ContentPage.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/ContentPage.tsx)) orchestrates the workspace layout and wires the data hook to the presentation layer.

```tsx
export function ContentPage() {
  const workspace = useContentWorkspace({ permissionUser });
  // Security gating with step-up check
  runStepUp(() => {
    // Render workspace
  });
  
  return (
    <>
      <ContentSidebar workspace={workspace} />
      <ContentToolbar selectedCollection={workspace.selectedCollection} />
      <ContentDocumentCanvas workspace={workspace} />
    </>
  );
}

```

The page component also gates collection-level mutations behind a **step-up** security check using `runStepUp`, ensuring only privileged users can create, update, or delete collections.

## Collection Navigation Components

### Sidebar and Explorer Panel

The [`ContentSidebar.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ContentSidebar.tsx) component ([`src/admin/pages/content/components/ContentSidebar/ContentSidebar.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/components/ContentSidebar/ContentSidebar.tsx)) renders the Collections section with proper accessibility markup (`<section aria-label="Collections">`). It displays each collection row with entry counts and highlights the currently selected collection.

The sidebar wires hook callbacks directly to UI events:

```tsx
<ContentExplorerPanel
  selectedCollection={workspace.selectedCollection}
  selectedCollectionId={workspace.selectedCollectionId}
  collections={workspace.collections}
  canCreateCollection={canManageCollections}
  onSelectCollection={workspace.selectCollection}
  createCollection={() => setCollectionDialogOpen(true)}
  updateCollection={workspace.updateCollection}
  deleteCollection={workspace.deleteCollection}
  moveEntryToCollection={workspace.moveEntryToCollection}
/>

```

### Toolbar Controls

[`ContentToolbar.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ContentToolbar.tsx) ([`src/admin/pages/content/components/ContentToolbar/ContentToolbar.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/components/ContentToolbar/ContentToolbar.tsx)) provides quick actions for the active collection. It renders a collection selector, mode toggle (list vs. canvas), and a create-entry button that dynamically disables when no collection is selected:

```tsx
<Button 
  variant="primary" 
  size="md" 
  onClick={onCreateEntry} 
  disabled={!selectedCollection}
>
  New {selectedCollection?.singularLabel ?? 'Entry'}
</Button>

```

### Settings Panel

The [`ContentSettingsPanel.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ContentSettingsPanel.tsx) component allows per-collection configuration such as toggling SEO fields, featured media support, and custom fields. It receives `selectedCollection` and `onCollectionChange` props, calling the workspace update handlers when users modify collection settings.

## Collection Management Dialogs

The architecture separates modal concerns into dedicated dialog components that receive workspace callbacks and close themselves on success.

**Create Dialog**: [`ContentCollectionCreateDialog.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ContentCollectionCreateDialog.tsx) ([`src/admin/pages/content/components/ContentCollectionCreateDialog/ContentCollectionCreateDialog.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/components/ContentCollectionCreateDialog/ContentCollectionCreateDialog.tsx)) handles new collection creation.

**Settings Dialog**: [`ContentCollectionSettingsDialog.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ContentCollectionSettingsDialog.tsx) ([`src/admin/pages/content/components/ContentCollectionSettingsDialog/ContentCollectionSettingsDialog.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/components/ContentCollectionSettingsDialog/ContentCollectionSettingsDialog.tsx)) manages editing existing collections.

Both dialogs receive `createCollection`, `updateCollection`, and `deleteCollection` callbacks from the workspace hook. They are rendered conditionally from `ContentPage` via local state flags (`collectionDialogOpen`, `collectionSettingsOpen`), keeping the dialog lifecycle separate from the workspace state machine.

## State Persistence and Layout Storage

Workspace layout state—such as panel sizes and active panel configuration—persists across sessions through a generic workspace layout store. The content workspace uses `useWorkspaceLayout` from [`src/admin/state/workspaceLayout.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/state/workspaceLayout.ts) and [`src/admin/state/workspaceLayoutStorage.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/state/workspaceLayoutStorage.ts) to read and write panel configurations.

This separation allows the collection UI to remain responsive to resize events while maintaining user-customized layouts without polluting the collection data layer.

## Data Flow and Interaction Patterns

The interaction flow follows a predictable unidirectional pattern:

1. **Mount**: `ContentPage` calls `useContentWorkspace`, which triggers `loadCollections()` to fetch post types from the CMS API.
2. **Render**: `ContentSidebar` displays the list; user selection calls `workspace.selectCollection`, updating `selectedCollectionId`.
3. **Propagation**: Toolbar and canvas components react to the new `selectedCollection` value automatically.
4. **Mutation**: User actions trigger dialog opens; submission calls workspace CRUD methods (e.g., `workspace.createCollection`), which update local state and persist to `createCmsDataTable` endpoints.
5. **Sync**: All subscribed components re-render with updated collection data thanks to the shared hook state.

## Implementation Examples

### Consuming the Hook in Custom Components

```tsx
import { useContentWorkspace } from '@content/hooks/useContentWorkspace';

export function MyCollectionList() {
  const { collections, selectedCollectionId, selectCollection } = useContentWorkspace({
    permissionUser: currentUser,
  });

  return (
    <ul>
      {collections.map(col => (
        <li
          key={col.id}
          style={{ fontWeight: col.id === selectedCollectionId ? 'bold' : 'normal' }}
          onClick={() => selectCollection(col.id)}
        >
          {col.pluralLabel} ({col.entryCount ?? 0})
        </li>
      ))}
    </ul>
  );
}

```

### Programmatic Collection Creation

```tsx
import { useContentWorkspace } from '@content/hooks/useContentWorkspace';
import { CreateDataTableInput } from '@core/types';

export function AddBlogCollection() {
  const { createCollection } = useContentWorkspace({ permissionUser: currentUser });

  const newCollection: CreateDataTableInput = {
    name: 'Blog posts',
    slug: 'blog',
    kind: 'postType',
    fields: [], // start empty; UI will add defaults later
  };

  async function handleClick() {
    await createCollection(newCollection);
    // UI updates automatically because `collections` state is refreshed
  }

  return <button onClick={handleClick}>Add “Blog” collection</button>;
}

```

## Summary

- **Centralized State**: The `useContentWorkspace` hook in [`src/admin/pages/content/hooks/useContentWorkspace.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/hooks/useContentWorkspace.ts) manages all collection data, selection, and CRUD operations as the single source of truth.
- **Component Structure**: [`ContentPage.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ContentPage.tsx) orchestrates the layout, delegating to `ContentSidebar`, `ContentToolbar`, and dialog components that receive workspace callbacks via props.
- **Security Integration**: Collection mutations are gated behind step-up authentication checks in the page component.
- **Persistence Separation**: Layout state persists through [`workspaceLayoutStorage.ts`](https://github.com/CoreBunch/Instatic/blob/main/workspaceLayoutStorage.ts) and [`workspaceLayout.ts`](https://github.com/CoreBunch/Instatic/blob/main/workspaceLayout.ts), keeping collection data and UI layout concerns decoupled.
- **Reactivity Model**: Standard React `useState` with React Compiler optimization ensures components re-render efficiently when collection data changes.

## Frequently Asked Questions

### Where is the collection data actually fetched in the Instatic content workspace?

Collection data is fetched inside [`src/admin/pages/content/hooks/useContentWorkspace.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/hooks/useContentWorkspace.ts) via the `loadCollections()` function, which queries the CMS API for all tables where `kind === 'postType'`. This occurs when the hook initializes, typically when `ContentPage` mounts.

### How does the sidebar know which collection is currently selected?

The `ContentSidebar` component receives `selectedCollectionId` and `selectedCollection` from the `useContentWorkspace` hook passed down via props. The hook maintains this state internally and updates it when `selectCollection()` is called, causing all subscribed components to re-render with the new selection.

### Can I customize the collection creation dialog without modifying the core hook?

Yes. The [`ContentCollectionCreateDialog.tsx`](https://github.com/CoreBunch/Instatic/blob/main/ContentCollectionCreateDialog.tsx) component is a presentational layer that receives the `createCollection` callback from the workspace hook. You can extend or replace this dialog component while maintaining the same interface—calling the workspace method on submission—to preserve the architecture's data integrity.

### What happens to the workspace layout when I refresh the browser?

Panel sizes and active workspace configurations persist across sessions through the workspace layout store in [`src/admin/state/workspaceLayoutStorage.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/state/workspaceLayoutStorage.ts) and [`src/admin/state/workspaceLayout.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/state/workspaceLayout.ts). The content workspace uses the `useWorkspaceLayout` hook to read and write these settings independently of collection data.