# Opik Frontend

> React frontend patterns for Opik. Use when working in apps/opik-frontend, on components, state, or data fetching.

- Skill: `comet-ml/opik-frontend` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add comet-ml/opik-frontend`
- Raw SKILL.md: https://api.skillmd.com/api/skills/comet-ml/opik-frontend/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: comet-ml (https://skillmd.com/u/comet-ml)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/comet-ml/opik-frontend

---


# Opik Frontend

## Architecture Decisions
- **Routing**: TanStack Router (file-based)
- **Data fetching**: TanStack Query (never raw fetch/useEffect)
- **State**: Zustand for global, React state for local
- **Components**: shadcn/ui + Radix UI base
- **Forms**: React Hook Form + Zod validation

## Critical Gotchas

### Never useEffect for Data Fetching
```typescript
// ❌ BAD
useEffect(() => {
  fetch('/api/data').then(setData);
}, []);

// ✅ GOOD
const { data } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
});
```

### Selective Memoization
```typescript
// ✅ USE useMemo for: complex computations, large data transforms
const filtered = useMemo(() =>
  data.filter(x => x.status === 'active').map(transform),
  [data]
);

// ✅ USE useCallback for: functions passed to children
const handleClick = useCallback(() => doSomething(id), [id]);

// ❌ DON'T memoize: simple values, primitives, local functions
const name = data?.name ?? '';  // No useMemo needed
```

### Zustand Selectors
```typescript
// ✅ GOOD - specific selector
const selectedEntity = useEntityStore(state => state.selectedEntity);

// ❌ BAD - selecting entire store causes re-renders
const { selectedEntity, filters } = useEntityStore();
```

### Browser Translation Safety (Google Translate)
Many users auto-translate the page; the translator wraps text nodes in `<font>` elements, so React throws `NotFoundError: removeChild` when it reconciles a **bare dynamic text node** it re-parented. Wrap dynamic/conditional strings in their own element instead of rendering bare text.
```typescript
// ❌ bare dynamic text → crash under translation
<button>{icon}{label}</button>
// ✅ wrap it → React swaps a stable element, stays translatable
<button>{icon}<span>{label}</span></button>
```
For timer-driven text (typewriter/counter), also avoid per-tick `setState` — write into a ref'd node's `textContent` (React never reconciles it), or mark a decorative node `translate="no"`. Ref: [facebook/react#11538](https://github.com/facebook/react/issues/11538) (OPIK-7428, OPIK-7435).

## Layer Architecture

### Shared layers (used by all versions)
`ui → shared` (one-way only)

### Per-version layers
`ui → shared → v1/pages-shared → v1/pages` (one-way only)
`ui → shared → v2/pages-shared → v2/pages` (one-way only)

### Module boundaries
- v1/ CANNOT import from v2/
- v2/ CANNOT import from v1/
- `src/components/` is BLOCKED (old structure, no longer exists)
- After modifying imports: `npm run deps:validate`

### Shared component rules
- Backward-compatible changes only
- Must not be version-aware (use `showProjectSelector={true}` not `isV2={true}`)
- If behavior needs to change, create a new component instead

## State Location Decisions
- **URL state**: filters, pagination, selected items
- **Zustand**: user preferences, cross-component state
- **React state**: form inputs, UI toggles

## Component Structure
```typescript
const Component: React.FC<Props> = ({ prop }) => {
  // 1. State hooks
  // 2. Queries/mutations
  // 3. Memoization (only when needed)
  // 4. Event handlers

  if (isLoading) return <Loader />;
  if (error) return <ErrorComponent />;

  return <div>...</div>;
};
```

## Query Patterns
```typescript
// Query with params
const { data } = useQuery({
  queryKey: [ENTITY_KEY, params],
  queryFn: (context) => fetchEntity(context, params),
});

// Mutation with invalidation
const mutation = useMutation({
  mutationFn: updateEntity,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: [ENTITY_KEY] });
  },
});
```

## Reference Files
- [forms.md](forms.md) - React Hook Form + Zod patterns
- [ui-components.md](ui-components.md) - Button variants, typography, dark theme
- [responsive-design.md](responsive-design.md) - Tailwind breakpoints vs useIsPhone
- [testing.md](testing.md) - When to test, Vitest patterns
- [code-quality.md](code-quality.md) - Lodash imports, naming, deps:validate
- [performance.md](performance.md) - Bundle optimization, rendering, memoization
- [permissions.md](permissions.md) - `usePermissions()` guard guidance for UI actions

