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

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. 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 (src/admin/pages/content/ContentPage.tsx) orchestrates the workspace layout and wires the data hook to the presentation layer.

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

The ContentSidebar.tsx component (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:

<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 (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:

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

Settings Panel

The 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 (src/admin/pages/content/components/ContentCollectionCreateDialog/ContentCollectionCreateDialog.tsx) handles new collection creation.

Settings Dialog: ContentCollectionSettingsDialog.tsx (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 and 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

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

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 manages all collection data, selection, and CRUD operations as the single source of truth.
  • Component Structure: 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 and 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 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 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 and src/admin/state/workspaceLayout.ts. The content workspace uses the useWorkspaceLayout hook to read and write these settings independently of collection data.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →