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
code
┌─────────────────────────────────────────────────────────────┐ │ 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:
code
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:
code
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:
code
widgets/merge-workspace/ └─ merge-workspace.tsx
- •Widget implementation:
typescript
// 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)
typescript
// ❌ 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)
typescript
// ❌ React 18
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
// ✅ React 19
<ThemeContext value={theme}>
{children}
</ThemeContext>
use() Hook for Promise/Context
typescript
// ✅ 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
typescript
// ✅ 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
typescript
// ✅ 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
typescript
// ✅ 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
typescript
// ❌ Avoid
function process(data: { id: string; name: string }) {}
// ✅ Preferred
interface ProcessData { id: string; name: string }
function process(data: ProcessData) {}
Constants with ValueOf Pattern
typescript
// 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 documentation - •
electron-patterns.md- Electron-specific patterns
To delete unused example files in scripts/ and assets/ directories as this skill focuses on architectural guidance.