| name | opik-frontend |
| description | React frontend patterns for Opik. Use when working in apps/opik-frontend, on components, state, or data fetching. |
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
useEffect(() => {
fetch('/api/data').then(setData);
}, []);
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
});
Selective Memoization
const filtered = useMemo(() =>
data.filter(x => x.status === 'active').map(transform),
[data]
);
const handleClick = useCallback(() => doSomething(id), [id]);
const name = data?.name ?? '';
Zustand Selectors
const selectedEntity = useEntityStore(state => state.selectedEntity);
const { selectedEntity, filters } = useEntityStore();
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
const Component: React.FC<Props> = ({ prop }) => {
if (isLoading) return <Loader />;
if (error) return <ErrorComponent />;
return <div>...</div>;
};
Query Patterns
const { data } = useQuery({
queryKey: [ENTITY_KEY, params],
queryFn: (context) => fetchEntity(context, params),
});
const mutation = useMutation({
mutationFn: updateEntity,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [ENTITY_KEY] });
},
});
Reference Files