Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ directories until the move is complete.

| Directory | Status |
|---|---|
| `apps/web/` | Active migration target — Vite + React Router v7 SPA for the assistant web app. Code is being incrementally migrated from a separate repo. See [`apps/web/README.md`](apps/web/README.md) for local dev setup. |
| `apps/web/` | Active migration target — Vite + React Router v7 SPA for the assistant web app. Code is being incrementally migrated from a separate repo. See [`apps/web/README.md`](apps/web/README.md) for local dev setup, [`apps/web/CONVENTIONS.md`](apps/web/CONVENTIONS.md) for architecture and state management patterns, and [`apps/web/STYLE_GUIDE.md`](apps/web/STYLE_GUIDE.md) for coding style. |
| `apps/chrome-extension/` | Move in progress from [`clients/chrome-extension/`](https://github.com/vellum-ai/vellum-assistant/tree/main/clients/chrome-extension). |

## Submitting a pull request
Expand Down
75 changes: 50 additions & 25 deletions apps/web/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,13 @@ References:
- [Zustand — TypeScript guide](https://zustand.docs.pmnd.rs/guides/typescript)
- [Bulletproof React — project structure](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md)

Store creation pattern — separate `State` and `Actions` interfaces
so consumers can subscribe to only the slice they need:
Store creation pattern — separate `State` and `Actions` interfaces,
wrap with `createSelectors` for auto-generated per-field hooks:

```ts
import { create } from "zustand";
import { useShallow } from "zustand/shallow";

import { createSelectors } from "@/utils/create-selectors.js";
import type { Message } from "./types.js";

// State — the data
Expand All @@ -286,7 +287,7 @@ export interface MessageActions {
// Combined store type
export type MessageStore = MessageState & MessageActions;

export const useMessageStore = create<MessageStore>()((set) => ({
const useMessageStoreBase = create<MessageStore>()((set) => ({
messages: [],
activeThreadId: null,
addMessage: (message) =>
Expand All @@ -297,31 +298,19 @@ export const useMessageStore = create<MessageStore>()((set) => ({
set({ messages: [], activeThreadId: null }),
}));

// Convenience hooks for common access patterns
export function useMessageState(): MessageState {
return useMessageStore(
useShallow((s) => ({
messages: s.messages,
activeThreadId: s.activeThreadId,
})),
);
}

export function useMessageActions(): MessageActions {
return useMessageStore(
useShallow((s) => ({
addMessage: s.addMessage,
setActiveThread: s.setActiveThread,
clearMessages: s.clearMessages,
})),
);
}
export const useMessageStore = createSelectors(useMessageStoreBase);
```

Consumers use `.use.field()` in render bodies and `.getState()` in
callbacks — see
[Reading state: `.use.*` vs `.getState()`](#reading-state-use-vs-getstate).

Keep store definitions in their domain folder — adding or removing a
domain means adding or removing a folder.

Reference: [Zustand — TypeScript guide](https://zustand.docs.pmnd.rs/guides/typescript)
References:
- [Zustand — TypeScript guide](https://zustand.docs.pmnd.rs/guides/typescript)
- [Zustand — Auto Generating Selectors](https://zustand.docs.pmnd.rs/learn/guides/auto-generating-selectors)

### Auth state lives in a Zustand store

Expand Down Expand Up @@ -422,10 +411,46 @@ const { bears } = useBearStore.getState();

Prefer `.use.field()` over manual `(s) => s.field` selectors. For
derived/computed values (e.g. `user?.id`), use `.use.user()` and
access the property from the result.
access the property from the result. See
[Reading state: `.use.*` vs `.getState()`](#reading-state-use-vs-getstate)
for when to use each API.

Reference: [Zustand — Auto Generating Selectors](https://zustand.docs.pmnd.rs/learn/guides/auto-generating-selectors)

### Reading state: `.use.*` vs `.getState()`

Zustand exposes two ways to read store state. Using the wrong one
causes either missed re-renders or unnecessary subscriptions.

| Context | API | Why |
|---------|-----|-----|
| **React render body** (component/hook top level) | `useStore.use.field()` | Creates a subscription — component re-renders when `field` changes. Required for reactive UI. |
| **Event handlers, callbacks, effects, `useCallback` bodies** | `useStore.getState().field` | Reads the latest value at call time without creating a subscription. No stale-closure risk. |
| **Outside React** (middleware, interceptors, stream handlers, `main.tsx`) | `useStore.getState().field` | No React context available — `.use.*` would throw. |
| **Calling actions** (anywhere) | `useStore.getState().actionName()` | Actions are stable references — calling via `.getState()` is always correct and avoids adding the action to dependency arrays. |

```ts
// Render body — reactive subscription
const count = useMessageStore.use.count();

// Event handler — imperative read + action
const handleClick = useCallback(() => {
useMessageStore.getState().increment();
}, []);

// Middleware — outside React
const { isLoggedIn } = useAuthStore.getState();
```

Zustand's `set()` is synchronous — `.getState()` after an action
returns already-mutated values. Read state *before* calling an action
when the caller needs pre-mutation values.

References:
- [Zustand — Updating state](https://zustand.docs.pmnd.rs/guides/updating-state)
- [Zustand — Reading/writing state outside components](https://zustand.docs.pmnd.rs/guides/extracting-state-outside-components)
- [React — Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)

### Data fetching: React Query vs direct SDK calls

Use **React Query** for data consumed primarily by React components —
Expand Down
Loading