Skip to main content
ClaudeWave
Skill1.1k estrellas del repoactualizado today

ccstate

# ccstate The ccstate skill documents patterns and best practices for state management in the vm0 platform, with a focus on handling asynchronous DOM event callbacks safely. Use this skill when implementing React components that trigger async commands from DOM events, particularly to understand the proper use of the `detach()` function with `Reason.DomCallback` instead of the `void` operator to handle floating promises while maintaining TypeScript lint compliance.

Instalar en Claude Code
Copiar
git clone --depth 1 https://github.com/vm0-ai/vm0 /tmp/ccstate && cp -r /tmp/ccstate/.claude/skills/ccstate ~/.claude/skills/ccstate
Después abre una sesión nueva de Claude Code; el skill carga automáticamente.

SKILL.md

# ccstate Patterns and Best Practices

This document records common patterns and best practices when using ccstate in the vm0 platform.

## DOM Callback Pattern

When handling DOM events (like button clicks) that trigger async commands, follow this pattern:

### Problem

DOM event handlers that call async commands will trigger TypeScript lint error `@typescript-eslint/no-floating-promises`.

### Solution

Use the `detach()` function with `Reason.DomCallback` to explicitly mark the promise as intentionally fire-and-forget.

### Pattern

```typescript
import { useSet, useGet } from "ccstate-react";
import { pageSignal$ } from "../../signals/page-signal.ts";
import { detach, Reason } from "../../signals/utils.ts";
import { someCommand$ } from "../../signals/some-command.ts";

function MyComponent() {
  const commandFn = useSet(someCommand$);
  const pageSignal = useGet(pageSignal$);

  const handleClick = () => {
    detach(commandFn(pageSignal), Reason.DomCallback);
  };

  return <button onClick={handleClick}>Click me</button>;
}
```

### Key Points

0. This pattern only applies to React views — it is forbidden to use in the signals directory
1. **Always use `pageSignal$`**: Get the page signal using `useGet(pageSignal$)` instead of creating a new `AbortController`
2. **Use `detach()` instead of `void`**: The `detach()` function properly handles promise rejection and tracks the promise for testing
3. **Use `Reason.DomCallback`**: This enum value indicates the promise is from a DOM event handler
4. **Never use `void` operator**: Using `void` silences the lint error but doesn't properly handle the promise

## Related Patterns

### Getting pageSignal$ in Components

```typescript
import { useGet } from "ccstate-react";
import { pageSignal$ } from "../../signals/page-signal.ts";

function MyComponent() {
  const pageSignal = useGet(pageSignal$);
  // Use pageSignal to call commands
}
```

### pageSignal$ is Automatically Set by Route System

**Important**: You do NOT need to manually set `pageSignal$` in your setup commands. The route system automatically handles this through `setupPageWrapper`.

```typescript
// ✅ Correct: setupPageWrapper automatically sets pageSignal$
export const setupLogsPage$ = command(({ set }, signal: AbortSignal) => {
  // NO need to call set(setPageSignal$, signal) - it's automatic!

  // Just do your page-specific initialization
  set(initLogs$, signal);
  set(updatePage$, createElement(LogsPage));
});

// In bootstrap.ts, routes use setupAuthPageWrapper which calls setupPageWrapper:
const ROUTE_CONFIG = [
  {
    path: "/logs",
    setup: setupAuthPageWrapper(setupLogsPage$), // Wrapper sets pageSignal$ automatically
  },
];
```

**How it works**:

1. Route navigation triggers `loadRoute$` (in route.ts)
2. `loadRoute$` calls `setupAuthPageWrapper(setupLogsPage$)`
3. `setupAuthPageWrapper` internally calls `setupPageWrapper`
4. `setupPageWrapper` sets `pageSignal$` before calling your setup command
5. Your setup command receives the signal and can access `pageSignal$` in components

**Never manually set pageSignal$ in setup commands** — the wrapper does it for you.

## Computed Memoization — No Manual Cache Needed

ccstate `computed` automatically memoizes the last result. If none of the dependencies have changed, reading the computed returns the cached value without re-executing the callback. **Do not add a manual `Map` or cache layer on top.**

```typescript
// ❌ Redundant cache — computed already memoizes
const cache = new Map<string, Result>();
export const result$ = computed((get) => {
  const key = get(someKey$);
  if (cache.has(key)) return cache.get(key)!;
  const value = expensiveCreate(key);
  cache.set(key, value);
  return value;
});

// ✅ Just create — computed won't re-run if someKey$ hasn't changed
export const result$ = computed((get) => {
  const key = get(someKey$);
  return expensiveCreate(key);
});
```

This is especially relevant for signal factories: a `computed` that calls `createSomeSignals(id)` won't re-create the signals unless `id` actually changes.

## Storing Function Values in State — The Updater Gotcha

When you call `set(atom$, value)`, ccstate checks if `value` is a function. If it is, ccstate treats it as an **updater** — it calls `value(previousValue)` and stores the **return value**, not the function itself. This is the same convention as React's `setState(fn)`.

This means **you cannot directly store a function in a `state()` atom using `set()`**. The function will be executed immediately instead of stored.

### The problem

```typescript
const cleanup$ = state<(() => void) | null>(null);

// ❌ BUG: ccstate calls the arrow function as an updater
// It executes: (() => { reader.cancel(); audioCtx.close(); })(previousValue)
// The return value (undefined) is stored, and the side effects fire immediately
set(cleanup$, () => {
  reader.cancel();
  audioCtx.close();
});
```

This is especially dangerous because:
1. The side effects (cancel, close) execute **immediately** instead of being deferred
2. The stored value becomes `undefined` (the return value of the arrow function), not the function
3. There is no runtime error at the `set()` call site — the bug is silent

### The fix: wrap in an updater that returns the function

```typescript
const cleanup$ = state<(() => void) | null>(null);

// ✅ Outer arrow is the updater; it returns the cleanup function to store
const cleanupFn = () => {
  reader.cancel();
  audioCtx.close();
};
set(cleanup$, () => cleanupFn);
```

The outer `() => cleanupFn` is called as the updater — it receives `previousValue` (ignored) and returns `cleanupFn`, which is then stored in the atom.

### Why this happens

From ccstate's core (`ccstate/core/index.js`):

```javascript
if (typeof val === 'function') {
  var updater = val;
  newValue = updater(previousValue);
} else {
  newValue = val;
}
```

This is by design — it mirrors React's `useState` updater pattern:

```typescript
// React: setState(prev => prev