| name | electron-fsd |
| description | This skill should be used when developing Electron applications with Feature-Sliced Design (FSD) architecture and React 19. Triggers on requests to create components, features, entities, widgets, or pages following FSD layer structure. Also applies when setting up Electron main/preload/renderer process code organization. |
Electron FSD Development (React 19)
Overview
This skill provides guidance for developing Electron applications using Feature-Sliced Design (FSD) architecture with React 19. It covers the three-process model (Main, Preload, Renderer) and FSD layer organization for the Renderer process.
Architecture Patterns
Electron Process Model
┌─────────────────────────────────────────────────────────────┐
│ Main Process (Node.js) │
│ ├─ app/main.ts - App lifecycle, BrowserWindow │
│ ├─ app/ipc-handler.ts - IPC routing to services │
│ ├─ services/ - Business logic (PdfMergeService) │
│ └─ workers/ - CPU-intensive tasks │
├─────────────────────────────────────────────────────────────┤
│ Preload (Isolated) │
│ └─ index.ts - ContextBridge (window.api) │
├─────────────────────────────────────────────────────────────┤
│ Renderer (React + FSD) │
│ ├─ shared/ - Common utilities, UI, types │
│ ├─ entities/ - READ (data display) │
│ ├─ features/ - CUD (user actions) │
│ ├─ widgets/ - Feature composition │
│ └─ pages/ - Route entry points │
└─────────────────────────────────────────────────────────────┘
FSD Layer Responsibilities
| Layer | Responsibility | IPC Direction | Example |
|---|---|---|---|
entities |
Data display only | READ | document-card.tsx, page-thumbnail.tsx |
features |
User actions | CUD | use-merge-command.ts, merge-toolbar.tsx |
widgets |
Feature composition | - | merge-workspace.tsx |
shared |
Cross-cutting concerns | - | ipc-client.ts, types, UI components |
Component Creation Workflow
Creating an Entity (READ-only)
To create a new entity for displaying data:
- Create entity folder:
src/renderer/entities/{entity-name}/ - Structure:
entities/pdf-document/
├─ ui/
│ └─ document-card.tsx # Display component
└─ model/
└─ use-pdf-metadata.ts # Data fetching hook (READ)
- Entity components must:
- Only display data (no mutations)
- Use hooks for data fetching
- Accept data via props or context
Creating a Feature (CUD operations)
To create a new feature for user actions:
- Create feature folder:
src/renderer/features/{feature-name}/ - Structure:
features/pdf-merge/
├─ ui/
│ ├─ merge-toolbar.tsx # Action UI
│ └─ merge-file-grid.tsx # Interactive grid with DnD
└─ model/
└─ use-merge-command.ts # IPC command hook
- Feature components must:
- Handle user interactions
- Call IPC commands for mutations
- Compose entities for display
Creating a Widget (Composition)
To compose features and entities into workspace:
- Create widget folder:
src/renderer/widgets/{widget-name}/ - Structure:
widgets/merge-workspace/
└─ merge-workspace.tsx
- Widget implementation:
// merge-workspace.tsx
export function MergeWorkspace() {
const { files, addFiles, removeFile } = useMergeContext();
return (
<>
<MergeToolbar onAddFiles={addFiles} />
<MergeFileGrid files={files} onRemove={removeFile} />
</>
);
}
File Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Folders & files | kebab-case | pdf-merge-service.ts |
| React components | PascalCase | MergeWorkspace |
| Hooks | camelCase with use prefix |
useMergeCommand |
| Types/interfaces | PascalCase | MergeRequest |
| Services | PascalCase + Service | PdfMergeService |
| Workers | kebab-case + worker | merge-worker.ts |
React 19 Component Patterns
No forwardRef (React 19)
// ❌ React 18 (forwardRef)
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
));
// ✅ React 19 (ref as regular prop)
interface InputProps {
ref?: React.Ref<HTMLInputElement>;
placeholder?: string;
}
function Input({ ref, ...props }: InputProps) {
return <input ref={ref} {...props} />;
}
Context as Provider (React 19)
// ❌ React 18
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
// ✅ React 19
<ThemeContext value={theme}>
{children}
</ThemeContext>
use() Hook for Promise/Context
// ✅ React 19: use() for Promise (requires Suspense)
import { use, Suspense } from 'react';
function FileList({ filesPromise }: { filesPromise: Promise<PdfFile[]> }) {
const files = use(filesPromise);
return files.map(f => <FileCard key={f.id} file={f} />);
}
// Usage with Suspense
<Suspense fallback={<Loading />}>
<FileList filesPromise={fetchFiles()} />
</Suspense>
// ✅ React 19: use() for Context (conditional allowed)
function ConditionalTheme({ show }: { show: boolean }) {
if (show) {
const theme = use(ThemeContext);
return <div className={theme}>Themed</div>;
}
return null;
}
useActionState for Forms
// ✅ React 19: Form action with state
import { useActionState } from 'react';
function MergeForm() {
const [state, submitAction, isPending] = useActionState(
async (prev, formData) => {
const files = formData.getAll('files');
const result = await window.api.merge(files);
return result;
},
null
);
return (
<form action={submitAction}>
<input type="file" name="files" multiple disabled={isPending} />
<button disabled={isPending}>
{isPending ? 'Merging...' : 'Merge PDFs'}
</button>
</form>
);
}
useOptimistic for Optimistic UI
// ✅ React 19: Optimistic updates
import { useOptimistic } from 'react';
function FileList({ files }: { files: PdfFile[] }) {
const [optimisticFiles, addOptimistic] = useOptimistic(
files,
(state, newFile: PdfFile) => [...state, { ...newFile, pending: true }]
);
async function handleAdd(file: File) {
const tempFile = { id: 'temp', name: file.name, pending: true };
addOptimistic(tempFile);
await window.api.addFile(file);
}
return (
<div>
{optimisticFiles.map(f => (
<FileCard key={f.id} file={f} opacity={f.pending ? 0.5 : 1} />
))}
</div>
);
}
ref Callback Cleanup
// ✅ React 19: ref cleanup function
function AutoFocusInput() {
return (
<input
ref={(el) => {
if (el) el.focus();
return () => {
// cleanup when element removed
console.log('Input unmounted');
};
}}
/>
);
}
Type Definition Rules
No Inline Object Types
// ❌ Avoid
function process(data: { id: string; name: string }) {}
// ✅ Preferred
interface ProcessData { id: string; name: string }
function process(data: ProcessData) {}
Constants with ValueOf Pattern
// Define constants
export const MERGE_STATUS = {
IDLE: 'idle',
PENDING: 'pending',
COMPLETE: 'complete',
} as const;
// Create union type
type ValueOf<T> = T[keyof T];
type MergeStatus = ValueOf<typeof MERGE_STATUS>;
// Usage
const status: MergeStatus = MERGE_STATUS.PENDING;
Resources
references/
fsd-layers.md- Detailed FSD layer documentationelectron-patterns.md- Electron-specific patterns
To delete unused example files in scripts/ and assets/ directories as this skill focuses on architectural guidance.