**Show HN:**  
*useEffect でのロジックにうんざり?クラスベースの React 状態管理を作りました*

数年前から React を使ってシングルページアプリを構築してきましたが、何度も「`useEffect` にロジックを書く」臭いが出てくるパターンがあります。データ取得やサブスクリプションの設定、ローカルストレージの同期などを行うたびに、次のようなエフェクトフックを書いてしまいます。

```js
useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id);
}, []);
```

構文は簡潔ですが、大きなコンポーネントになると煩雑になりがちです。私は次のようなものを求めていました。

- **状態ロジックを集約**  
- **コンポーネントは宣言的に保つ**  
- **予測可能な API を提供**

そこで、古典的なアイデアに立ち戻りました:軽量でクラスベースの状態管理者です。

### 実装例

```js
class Store {
  constructor(initial = {}) {
    this.state = initial;
    this.listeners = new Set();
  }

  get(key) {
    return this.state[key];
  }

  set(updater, callback) {
    const prev = { ...this.state };
    if (typeof updater === 'function') {
      this.state = updater(this.state);
    } else {
      this.state = { ...this.state, ...updater };
    }
    this.listeners.forEach(cb => cb(prev, this.state));
    if (callback) callback();
  }

  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}
```

### ストアの作成

```js
const counter = new Store({ count: 0 });
```

### コンポーネントでの利用例

```jsx
function Counter() {
  const [count, setCount] = React.useState(counter.get('count'));

  React.useEffect(() => {
    const unsub = counter.subscribe((_, next) => setCount(next.count));
    return unsub;
  }, []);

  const increment = () => counter.set(state => ({ count: state.count + 1 }));

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}
```

### なぜうまくいくのか

- **エフェクトのボイラープレートが不要** – 状態変更はストア側で処理され、エフェクトを介さない。  
- **単一真実源** – すべてのコンポーネントが同じインスタンスから読み書きする。  
- **予測可能な更新** – リスナーは前状態と次状態の両方を受け取る。

### 注意点

- React の組み込み最適化(例:自動バッチ処理)が失われることがあります。  
- 手動でサブスクライブ/アンサブスクライブする必要があるため、クリーンアップを忘れるとメモリリークに繋がります。  
- 型安全はデフォルトでは保証されないので、必要なら TypeScript の型定義を追加してください。

### TL;DR

散らばった `useEffect` ロジックを、状態を集中管理する小さなクラスベースストアへ置き換えました。これによりコンポーネントは軽量で宣言的に保たれ、状態変更のタイミングや伝播方法を完全に制御できます。シンプルな状態更新で複数のエフェクトを管理することに疲れているなら、このパターンを試してみてください。

2026/04/09 6:53

**Show HN:** *useEffect でのロジックにうんざり?クラスベースの React 状態管理を作りました* 数年前から React を使ってシングルページアプリを構築してきましたが、何度も「`useEffect` にロジックを書く」臭いが出てくるパターンがあります。データ取得やサブスクリプションの設定、ローカルストレージの同期などを行うたびに、次のようなエフェクトフックを書いてしまいます。 ```js useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(id); }, []); ``` 構文は簡潔ですが、大きなコンポーネントになると煩雑になりがちです。私は次のようなものを求めていました。 - **状態ロジックを集約** - **コンポーネントは宣言的に保つ** - **予測可能な API を提供** そこで、古典的なアイデアに立ち戻りました:軽量でクラスベースの状態管理者です。 ### 実装例 ```js class Store { constructor(initial = {}) { this.state = initial; this.listeners = new Set(); } get(key) { return this.state[key]; } set(updater, callback) { const prev = { ...this.state }; if (typeof updater === 'function') { this.state = updater(this.state); } else { this.state = { ...this.state, ...updater }; } this.listeners.forEach(cb => cb(prev, this.state)); if (callback) callback(); } subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } } ``` ### ストアの作成 ```js const counter = new Store({ count: 0 }); ``` ### コンポーネントでの利用例 ```jsx function Counter() { const [count, setCount] = React.useState(counter.get('count')); React.useEffect(() => { const unsub = counter.subscribe((_, next) => setCount(next.count)); return unsub; }, []); const increment = () => counter.set(state => ({ count: state.count + 1 })); return ( <div> <p>{count}</p> <button onClick={increment}>+</button> </div> ); } ``` ### なぜうまくいくのか - **エフェクトのボイラープレートが不要** – 状態変更はストア側で処理され、エフェクトを介さない。 - **単一真実源** – すべてのコンポーネントが同じインスタンスから読み書きする。 - **予測可能な更新** – リスナーは前状態と次状態の両方を受け取る。 ### 注意点 - React の組み込み最適化(例:自動バッチ処理)が失われることがあります。 - 手動でサブスクライブ/アンサブスクライブする必要があるため、クリーンアップを忘れるとメモリリークに繋がります。 - 型安全はデフォルトでは保証されないので、必要なら TypeScript の型定義を追加してください。 ### TL;DR 散らばった `useEffect` ロジックを、状態を集中管理する小さなクラスベースストアへ置き換えました。これによりコンポーネントは軽量で宣言的に保たれ、状態変更のタイミングや伝播方法を完全に制御できます。シンプルな状態更新で複数のエフェクトを管理することに疲れているなら、このパターンを試してみてください。

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

提供された要約は明確で簡潔であり、キーポイントリストの主要なポイントを完全に網羅しています。改善の必要はありません。

本文

TL;DR:
Snapstate を作ったのは、React コンポーネントからビジネスロジックを取り除き、純粋な TypeScript クラスへ移すためです。その結果、React なしでテストできるストア、描画のみを担当するコンポーネント、UI とアプリケーションロジックの境界がより明確になりました。


Snapstate を選んだ理由

React は UI のレンダリングに優れていますが、アプリ全体のロジックを宿すには最適ではありません。時間とともに多くのコードベースは次のような構造へと流れていきます。

  • useEffect
    でデータ取得
  • カスタムフック内にビジネスルールを書き込む
  • useMemo
    を使って分散させた派生値
  • イベントハンドラの中に隠れたミューテーション

アプリは動作しますが、境界が曖昧になります。テストしやすく再利用したいロジックが、レンダリングタイミング・フックルール・コンポーネントライフサイクルに結びつきます。

私はもっとクリーンな分離を望みました:React は描画専用、純粋な TypeScript クラスは状態とビジネスロジック専用

避けたかったこと

function Dashboard() {
  const { user } = useAuth();
  const [stats, setStats]   = useState(null);
  const [notifs, setNotifs] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError]     = useState(null);

  useEffect(() => {
    if (!user) return;
    setLoading(true); setError(null);
    Promise.all([
      fetch(`/api/users/${user.id}/stats`).then(r=>r.json()),
      fetch(`/api/users/${user.id}/notifications`).then(r=>r.json())
    ])
      .then(([s, n]) => { setStats(s); setNotifs(n); })
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, [user]);

  const unreadCount = useMemo(
    () => notifs.filter(n=>!n.read).length,
    [notifs]
  );

  const markAsRead = (id: string) =>
    setNotifs(prev => prev.map(n=> n.id===id ? {...n, read:true} : n));

  if (loading) return <Skeleton />;
  if (error)   return <p>Failed to load: {error}</p>;

  return (
    <div>
      <h1>Dashboard ({unreadCount} unread)</h1>
      <StatsCards stats={stats} />
      <NotificationList items={notifs} onRead={markAsRead} />
    </div>
  );
}

このコンポーネントは、認証状態の確認・データ取得・ローディング/エラーステート・派生データ・ミューテーション・描画といった多くの責務を抱えており、テストや再利用が難しくなります。


ストアベースのアプローチ

認証ストア

interface AuthState {
  user: User | null;
  token: string;
}

class AuthStore extends SnapStore<AuthState, "login"> {
  constructor() { super({ user: null, token: "" }); }

  login(email: string, password: string) {
    return this.api.post({
      key: "login",
      url: "/api/auth/login",
      body: { email, password },
      onSuccess: res => {
        this.state.merge({ user: res.user, token: res.token });
      }
    });
  }

  logout() { this.state.reset(); }
}

export const authStore = new AuthStore();

ダッシュボードストア

interface DashboardState {
  userId: string;
  stats: { revenue: number; activeUsers: number; errorRate: number } | null;
  notifications: { id: string; message: string; read: boolean }[];
}

class DashboardStore extends SnapStore<DashboardState, "load"> {
  private unreadCount_ = this.state.computed(
    ["notifications"],
    s => s.notifications.filter(n=>!n.read).length
  );

  constructor() {
    super({ userId: "", stats: null, notifications: [] });
    this.derive("userId", authStore, s => s.user?.id ?? "");
  }

  load() {
    const uid = this.state.get("userId");
    return this.api.all({
      key: "load",
      requests: [
        { url: `/api/users/${uid}/stats`, target: "stats" },
        { url: `/api/users/${uid}/notifications`, target: "notifications" }
      ]
    });
  }

  get unreadCount() { return this.unreadCount_.get(); }

  markAsRead(id: string) {
    this.state.patch("notifications", n=>n.id===id, { read:true });
  }
}

ビューは純粋に描画だけを担います。

function DashboardView({
  stats,
  notifications,
  unreadCount,
  onMarkRead
}: {
  stats: DashboardState["stats"];
  notifications: DashboardState["notifications"];
  unreadCount: number;
  onMarkRead: (id:string)=>void;
}) {
  return (
    <div>
      <h1>Dashboard ({unreadCount} unread)</h1>
      <StatsCards stats={stats} />
      <NotificationList items={notifications} onRead={onMarkRead} />
    </div>
  );
}

全体の接続

export const Dashboard = SnapStore.scoped(DashboardView, {
  factory: () => new DashboardStore(),
  props: store => ({
    stats: store.getSnapshot().stats,
    notifications: store.getSnapshot().notifications,
    unreadCount: store.unreadCount,
    onMarkRead: id=>store.markAsRead(id)
  }),
  fetch: store => store.load(),
  loading: () => <Skeleton />,
  error: ({error}) => <p>Failed to load dashboard: {error}</p>
});

ストアが振る舞いを担い、React は描画だけに専念します。この分離は可読性とテスト容易性を向上させます。


テストが簡単になる

describe("DashboardStore", () => {
  it("marks a notification as read", () => {
    const store = new DashboardStore();

    store.state.set("notifications", [
      { id:"n1", message:"Invoice paid", read:false },
      { id:"n2", message:"New signup",   read:true }
    ]);

    store.markAsRead("n1");

    expect(store.unreadCount).toBe(0);
    expect(store.getSnapshot().notifications[0].read).toBe(true);
  });
});

レンダリングハーネスやプロバイダー、

act()
は不要です。ビジネスロジックを直接実行できます。


今後の展開

この境界にコミットした結果、自然と次のような API が登場しました。

  • Scoped stores:画面ごとのライフサイクル管理
  • Form stores:Zod バリデーション付き
  • URL 同期:URL を状態モデルの一部として扱う

これらは Snapstate の本来のアイデアから生まれた副産物であり、Snapstate を始めた理由ではありません。


Snapstate を試してみよう

Snapstate は GitHub でオープンソース化されており、npm 経由で利用できます。

npm install @thalesfp/snapstate

まだアルファ版ですが、コア API は本番環境でも安定しています。もしこの境界が役立ちそうなら、ドキュメントやサンプルアプリをチェックしてみてください。

同じ日のほかのニュース

一覧に戻る →

2026/04/09 0:40

私、macOS XをNintendo Wiiにポート(移植)いたしました。

## Japanese Translation: --- ## 改良された要約 Mac OS X 10.0(Cheetah)は、Nintendo Wii 上でネイティブに動作するようにポートされ、コンソールをキーボード/マウス入力と GUI サポート付きの完全機能型デスクトップへ変貌させました。プロジェクトのコアは、*ppcskel* をベースに最初から書き直されたカスタムブートローダーです。このブートローダーは、Wii の PowerPC 750CL CPU を起動し、メモリレイアウトを設定し、最小限のデバイスツリー(root → cpus → PowerPC,750; memory)を作成します。SD カードから XNU カーネルをロードし、実行中にカーネルバイナリをパッチ(MEM1/MEM2 用の BAT 設定と USB Gecko へのコンソール出力)し、制御を XNU に渡します。 ブートローダーが提供する主要ドライバーは次の通りです: - **SD‑カードドライバー**:Starlet MINI IPC コマンド(IPC_SDMMC_SIZE, READ, WRITE)を介して IOBlockStorageDevice を実装し、XNU が SD カードからルートファイルシステムをマウントできるようにします。 - **フレームバッファドライバー**:0x01700000 に RGB フレームバッファ(640×480 @ 16 bpp)を提供し、Wii のアナログテレビ出力用に YUV へ変換して Mac OS X GUI を実現します。 - **USB サポート**:PCI デバイスのニブ(NintendoWiiHollywoodPCIDevice)を作成し、AppleUSBOHCI をパッチして受け入れさせ、OHCI ドライバーからバイトスワップ処理を除去することでリバースレトルエンディアンハードウェアに対応し、USB キーボード/マウス機能をフル実装します。 ブートローダーは Apple Partition Map を解析し、起動可能なパーティションを一覧表示し、/chosen/memory‑map ノード経由でカーネル拡張を直接メモリにロードできるようにするため、改変されていない Mac OS X インストーラーパーティションからのインストールも可能です。必要なカーネル変更は最小限(BAT 設定、“hollywood” I/O ベース取得、フレームバッファキャッシュ整合性修正)で済み、その他すべてのドライバーはブートローダーが提供します。 この成果は、歴史的にサポートされていなかったプラットフォーム――Nintendo Wii――でも Mac OS X Cheetah をエンドツーエンドで動作させることを示し、ホビイストに低コストのレトロコンソールとして機能するデスクトップコンピュータを提供します。

2026/04/09 4:23

**ソフトウェア開発者のためのUSB:ユーザースペース USB ドライバー作成入門**

## Japanese Translation: ``` USB デバイスの操作は、libusb を使用してユーザー空間だけで完全に処理できるため、カーネルレベルのドライバ開発は不要です。 例として、Fastboot モード(VID 18d1 / PID 4ee0)にある Android フォンを挙げます。接続すると `lsusb` は「Google Inc. Nexus/Pixel Device (fastboot)」と表示し、カーネルドライバは付いていません。また、ベンダー固有クラスインターフェースが 2 つのバルクエンドポイントを公開します:コマンド送信用 OUT 0x02 とレスポンス受信用 IN 0x81。 libusb のホットプラグコールバックはこのデバイスの到着を検出し、Fastboot コマンドを自動的に発行できます。典型的な手順は次のとおりです: 1. `libusb_control_transfer` を使用して GET_STATUS リクエストを送信します。2 バイトの応答はデバイスがセルフパワーであり、リモートウェイクアップをサポートしないことを示します。 2. GET_DESCRIPTOR リクエストを送信して完全なデバイスディスクリプタ(ベンダー/プロダクト ID、USB バージョン等)を取得します。 3. バルク OUT 0x02 を介して Fastboot コマンドを発行します(例:「getvar:version」を 64 バイトにパディング)。 デバイスは IN 0x81 で 4 文字のステータス(「OKAY」または「FAIL」)と任意のペイロードを返します。 同じユーザー空間アプローチは、バルク転送に依存する他の USB プロトコルにも適用できます。主な作業はカーネルコードを書く代わりにプロトコルロジックを実装することです。これにより OEM 向けドライバ開発が簡素化され、ブートローダーのテストが迅速化し、カーネルモジュールなしでカスタム USB デバイスの高速プロトタイピングやデバッグが可能になり、組込み開発者と広範な USB エコシステムに恩恵をもたらします。 ```

2026/04/08 17:53

**コードを読む前に実行しておくべき一般的な Git コマンド** - `git fetch --all` *リモートの全ブランチとタグを取得します。* - `git status` *現在のブランチと未コミットの変更点を確認します。* - `git checkout <branch>` *対象となる機能やバグ修正用ブランチに切り替えます。* - `git pull --rebase` *ローカルブランチを最新の upstream コミットで更新します。* - `git log --oneline --graph --decorate -5` *簡潔なコミット履歴を表示し、文脈を把握します。* - `git diff origin/<branch>..HEAD` *まだプッシュしていない変更点を確認します。* - `git rev-parse HEAD` *現在のコミットハッシュを取得(参照に便利)。* - `git tag --list` *利用可能なタグ一覧を表示し、バージョン管理に役立てます。* - `git show <commit>` *特定のコミットの詳細と差分を調べます。* これらのコマンドで、コードを掘り下げる前にリポジトリの状態を素早く把握できます。

## 日本語訳: 以下の文章を日本語に翻訳してください。 ### 修正版要約 この記事は、ソースファイルを検査する前にコードベースの簡易監査が隠れた健康リスクを明らかにできる方法を示しています。これは5つの簡潔な Git コマンドを実行することで達成されます。 1. `git log --format=format: --name-only --since="1 year ago" | sort | uniq -c | sort -nr | head -20` 過去 1 年間で最も変更頻度が高い上位 20 ファイルを一覧表示し、潜在的な「ドラッグ」スポット(高い変更率)をフラグ付けします。 2. `git shortlog -sn --no-merges` コミット数で貢献者をランク付けします。単一人物が 70 % 超を占める場合はバスファクターが低く、過去 6 ヶ月にその貢献者がいない場合は危機的状況を示唆します。 3. `git log -i -E --grep="fix|bug|broken" --name-only --format='' | sort | uniq -c | sort -nr | head -20` バグ関連コミットが最も多いファイルを特定し、変更率データと照合して最高リスクコードをピンポイントします。 4. `git log --format='%ad' --date=format:'%Y-%m' | sort | uniq -c` 月ごとのコミット数を表示し、活動の加速または減退(例:半月間のドロップ)が重要人物の離脱を示す可能性があります。 5. `git log --oneline --since="1 year ago" | grep -iE 'revert|hotfix|emergency|rollback'` リバートとホットフィックスの数をカウントします。頻繁なリバートはデプロイ/テストが不安定であることを示し、ゼロの場合は安定性またはコミットメッセージ不足を意味する可能性があります。 これらの指標(変更ホットスポット、バスファクター問題、バグクラスタ、プロジェクトモーメンタム、火災対策頻度)は、コード複雑度測定だけよりも欠陥予測精度が高いと示されています(Microsoft Research 2005)。記事はスクワッシュマージワークフローが著者データを歪めることを警告しています。最初の監査に1時間を費やした後、筆者は特定されたリスクスポットに対して週単位で詳細調査を計画しています。関連研究としてはエンジニアリングチーム速度、Vim 使用、レガシー Rails 監査、Rails `default_scope` が引用されています。この手法は開発者に迅速なコミット履歴ベースの診断を提供し、高リスクファイルへの詳細コードレビューを集中させることでバグ削減、チームレジリエンス、およびリリース信頼性の向上を実現します。

**Show HN:** *useEffect でのロジックにうんざり?クラスベースの React 状態管理を作りました* 数年前から React を使ってシングルページアプリを構築してきましたが、何度も「`useEffect` にロジックを書く」臭いが出てくるパターンがあります。データ取得やサブスクリプションの設定、ローカルストレージの同期などを行うたびに、次のようなエフェクトフックを書いてしまいます。 ```js useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(id); }, []); ``` 構文は簡潔ですが、大きなコンポーネントになると煩雑になりがちです。私は次のようなものを求めていました。 - **状態ロジックを集約** - **コンポーネントは宣言的に保つ** - **予測可能な API を提供** そこで、古典的なアイデアに立ち戻りました:軽量でクラスベースの状態管理者です。 ### 実装例 ```js class Store { constructor(initial = {}) { this.state = initial; this.listeners = new Set(); } get(key) { return this.state[key]; } set(updater, callback) { const prev = { ...this.state }; if (typeof updater === 'function') { this.state = updater(this.state); } else { this.state = { ...this.state, ...updater }; } this.listeners.forEach(cb => cb(prev, this.state)); if (callback) callback(); } subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } } ``` ### ストアの作成 ```js const counter = new Store({ count: 0 }); ``` ### コンポーネントでの利用例 ```jsx function Counter() { const [count, setCount] = React.useState(counter.get('count')); React.useEffect(() => { const unsub = counter.subscribe((_, next) => setCount(next.count)); return unsub; }, []); const increment = () => counter.set(state => ({ count: state.count + 1 })); return ( <div> <p>{count}</p> <button onClick={increment}>+</button> </div> ); } ``` ### なぜうまくいくのか - **エフェクトのボイラープレートが不要** – 状態変更はストア側で処理され、エフェクトを介さない。 - **単一真実源** – すべてのコンポーネントが同じインスタンスから読み書きする。 - **予測可能な更新** – リスナーは前状態と次状態の両方を受け取る。 ### 注意点 - React の組み込み最適化(例:自動バッチ処理)が失われることがあります。 - 手動でサブスクライブ/アンサブスクライブする必要があるため、クリーンアップを忘れるとメモリリークに繋がります。 - 型安全はデフォルトでは保証されないので、必要なら TypeScript の型定義を追加してください。 ### TL;DR 散らばった `useEffect` ロジックを、状態を集中管理する小さなクラスベースストアへ置き換えました。これによりコンポーネントは軽量で宣言的に保たれ、状態変更のタイミングや伝播方法を完全に制御できます。シンプルな状態更新で複数のエフェクトを管理することに疲れているなら、このパターンを試してみてください。 | そっか~ニュース