スキル一覧に戻る
naporin0624

jotai-reactive-atoms

by naporin0624

0🍴 0📅 2026年1月9日
GitHubで見るManusで実行

SKILL.md


name: jotai-reactive-atoms description: 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. allowed-tools: Read, 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

PatternUse CaseDescription
Hybrid AtomReal-time dataStream + HTTP fallback
Stream AtomIPC subscriptionsonMount with subscription
Single Fetch AtomInitial dataHTTP-only async atom
Event SubscriptionIPC optimizationShared 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

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です