jotai-reactive-atoms

naporin0624's avatarfrom naporin0624

Implement reactive state management with Jotai atoms in Electron renderer. Use when creating atoms, implementing real-time updates via IPC, or setting up hybrid stream + HTTP fallback patterns. Covers event subscription optimization, debouncing, and Suspense integration.

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 29, 2025

When & Why to Use This Skill

This Claude skill provides specialized architectural patterns for implementing reactive state management in Electron applications using Jotai atoms. It focuses on bridging the gap between IPC-based real-time updates and standard HTTP fallbacks, offering optimized strategies for event subscriptions, debouncing, and React Suspense integration to ensure a high-performance and synchronized desktop UI experience.

Use Cases

  • Real-time Data Synchronization: Implementing live-updating user lists, notifications, or system status indicators in Electron via IPC event streams.
  • Hybrid Data Fetching: Creating robust atoms that use HTTP as an initial data source or fallback while maintaining real-time consistency through IPC subscriptions.
  • Performance Optimization: Managing high-frequency IPC messages in the renderer process using debouncing and shared event listeners to prevent UI lag.
  • Desktop UI State Management: Leveraging Jotai with React Suspense to handle complex asynchronous data states and loading transitions in desktop software.
namejotai-reactive-atoms
descriptionImplement reactive state management with Jotai atoms in Electron renderer. Use when creating atoms, implementing real-time updates via IPC, or setting up hybrid stream + HTTP fallback patterns. Covers event subscription optimization, debouncing, and Suspense integration.
allowed-toolsRead, Write, Edit, Grep, Glob

Jotai Reactive Atoms for Electron

Overview

This skill documents the Jotai atom patterns for Electron applications with IPC-based real-time updates.

Key Patterns

Pattern Use Case Description
Hybrid Atom Real-time data Stream + HTTP fallback
Stream Atom IPC subscriptions onMount with subscription
Single Fetch Atom Initial data HTTP-only async atom
Event Subscription IPC optimization Shared IPC listeners

Quick Start

1. Define Event Subscription Source

// src/renderer/src/adapters/ipc-events/index.ts
import { createEventSubscription } from '@utils/event-subscription';

// One source per IPC event type
export const usersSource = createEventSubscription<void>('app:users');
export const activeEventSource = createEventSubscription<void>('app:activeEvent');
export const notificationsSource = createEventSubscription<Notification[]>('app:notifications');

2. Create Hybrid Atom

// src/renderer/src/views/atoms/users.atom.ts
import { atom } from 'jotai';
import { atomWithRefresh } from 'jotai/utils';
import { client } from '@adapters/client';
import { usersSource } from '@adapters/ipc-events';
import { debounce } from '@utils/debounce';

// Step 1: HTTP-only fetch atom (fallback)
const singleFetchUsersAtom = atomWithRefresh(async () => {
  const res = await client.users.$get();
  if (res.status === 401) throw new UnauthorizedError();
  if (res.status === 500) throw new UnknownError();
  return res.json();
});

// Step 2: Stream atom (IPC updates)
const streamUsersAtom = atom<{ value: User[] }>();

streamUsersAtom.onMount = (set) => {
  const handleUpdate = debounce(async () => {
    const res = await client.users.$get();
    if (res.ok) {
      set({ value: await res.json() });
    }
  }, 300);

  handleUpdate();  // Immediate initial fetch

  return usersSource.subscribe(handleUpdate);  // Subscribe to IPC
};

// Step 3: Hybrid selector (exported)
export const usersAtom = atom(async (get) => {
  const stream = get(streamUsersAtom);
  if (stream === undefined) {
    return get(singleFetchUsersAtom);  // Fallback to HTTP
  }
  return stream.value;  // Use stream data
});

3. Use in Component with Suspense

// src/renderer/src/views/users/index.tsx
import { Suspense } from 'react';
import { useAtomValue } from 'jotai';
import { usersAtom } from '../atoms/users.atom';

const UserList = () => {
  const users = useAtomValue(usersAtom);  // Suspends until ready

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.displayName}</li>
      ))}
    </ul>
  );
};

export const UsersPage = () => (
  <Suspense fallback={<Loading />}>
    <UserList />
  </Suspense>
);

Architecture Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                    JOTAI HYBRID ATOM PATTERN                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│   useAtom(usersAtom)                                                 │
│         │                                                             │
│         ▼                                                             │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │ usersAtom (Hybrid Selector)                                  │   │
│   │                                                               │   │
│   │ atom(async (get) => {                                        │   │
│   │   const stream = get(streamUsersAtom);                       │   │
│   │   if (stream === undefined) return get(singleFetchAtom);     │   │
│   │   return stream.value;                                        │   │
│   │ })                                                            │   │
│   └─────────────────────────────────────────────────────────────┘   │
│         │                                 │                          │
│         │ (t=0: not mounted)              │ (t=1+: mounted)          │
│         ▼                                 ▼                          │
│   ┌─────────────────┐           ┌─────────────────┐                 │
│   │ singleFetchAtom │           │ streamUsersAtom │                 │
│   │ (HTTP Fallback) │           │ (IPC Stream)    │                 │
│   └────────┬────────┘           └────────┬────────┘                 │
│            │                              │                          │
│            ▼                              │ onMount                   │
│   client.users.$get()                    │                          │
│            │                              ▼                          │
│            │                    usersSource.subscribe()             │
│            │                              │                          │
│            │                              │ IPC: 'app:users'         │
│            │                              ▼                          │
│            │                    debounce(handleUpdate, 300)         │
│            │                              │                          │
│            └──────────────────────────────┘                          │
│                              │                                        │
│                              ▼                                        │
│                      RENDERER RESULT                                 │
│                                                                       │
└─────────────────────────────────────────────────────────────────────┘

Timing Diagram

TIME: t0 (Component Mount)
═══════════════════════════════════════════════════════════

  Component mounts
       │
       ▼
  useAtom(usersAtom) → Suspends
       │
       │ streamUsersAtom = undefined (not yet mounted)
       │          │
       │          ▼
       │   Fallback: singleFetchAtom
       │          │
       │          ▼
       │   HTTP fetch → Data arrives
       │          │
       │          ▼
  Component renders with HTTP data ✓


TIME: t1 (Stream Ready)
═══════════════════════════════════════════════════════════

  streamUsersAtom.onMount executes
       │
       ├── Immediate fetch via HTTP
       │
       └── Subscribe to IPC events
              │
              ▼
  streamUsersAtom.value = fetched data
       │
       ▼
  usersAtom re-evaluates
       │
       │ stream !== undefined
       │          │
       │          ▼
       │   Return stream.value
       │
       ▼
  Component re-renders with stream data ✓


TIME: t2+ (Updates)
═══════════════════════════════════════════════════════════

  Main Process: Service Observable emits
       │
       ▼
  webContents.send('app:users')
       │
       ▼
  IPC Event received
       │
       ▼
  usersSource notifies subscribers
       │
       ▼
  Debounced handler executes (after 300ms)
       │
       ▼
  HTTP fetch → set({ value: newData })
       │
       ▼
  Component automatically re-renders ✓

When to Use Each Pattern

Hybrid Atom (Stream + HTTP Fallback)

Use for data that:

  • Changes frequently
  • Needs real-time updates
  • Is shared across components

Examples: Users, Notifications, Active Event

Stream-Only Atom

Use for data that:

  • Is append-only (like logs)
  • Doesn't need initial fetch
export const logsAtom = atom<Log[]>([]);

logsAtom.onMount = (set) => {
  return logSource.subscribe((log) => {
    set((prev) => [log, ...prev].slice(0, 1000));
  });
};

Single Fetch Atom

Use for data that:

  • Rarely changes
  • Doesn't need real-time updates
export const worldAtom = atomFamily((id: string) =>
  atom(async () => {
    const res = await client.worlds[':id'].$get({ param: { id } });
    return res.json();
  })
);

Additional Resources