| name | typescript-react |
| description | Contains TypeScript/React patterns and conventions for the Mikoto frontend. ALWAYS activate before working on frontend code in apps/client/ or packages/. |
TypeScript/React Development in Mikoto
Activation
TRIGGER when: working on frontend code in apps/client/, packages/mikoto.js/, or any TypeScript/React files.
Project Structure
apps/client/src/
├── components/
│ ├── atoms/ # Small, single-purpose (Avatar, SpaceIcon)
│ ├── molecules/ # Compound components (markdown, editors)
│ ├── surfaces/ # Page/tab views (MessageSurface, DocumentChannel)
│ ├── modals/ # Modal dialogs
│ ├── sidebars/ # Sidebar components
│ ├── tabs/ # Tab UI components
│ ├── ui/ # Chakra UI component re-exports
│ ├── icons/ # Custom FontAwesome icons
│ └── design/ # Design system components
├── views/ # Top-level views (MainView, AuthView)
├── hooks/ # Custom React hooks
├── store/ # Jotai atoms, LocalDB
└── functions/ # Utility functions (fileUpload, notify)
packages/
├── mikoto.js/ # Core API client library
│ ├── managers/ # Resource managers (SpaceManager, ChannelManager)
│ ├── WebsocketApi.ts # WebSocket connection
│ ├── MikotoClient.ts # Main client class
│ ├── AuthClient.ts # Authentication
│ └── api.gen.ts # Auto-generated types from OpenAPI
├── mikoto-ui/ # UI component library
├── permcheck/ # Permission checking utilities
├── lexical-markdown/ # Markdown editing plugin
└── tsconfig/ # Shared TypeScript config
Component Patterns
All components are functional with typed props interfaces:
export const Avatar = (
props: {
src?: string | null;
userId?: string;
size?: number;
} & React.HTMLAttributes<HTMLImageElement>,
) => {
const { src, userId, size, ...rest } = props;
return <img src={src} size={size} {...rest} />;
};
Styling
We use Chakra UI
import { chakra } from '@chakra-ui/react';
const Card = chakra('div', {
base: { p: '4', bg: 'bg.panel' },
});
There's a lot of legacy code that uses Emotion styled-components, but we're migrating to Chakra UI.
Surface System (Tab/Panel Management)
Components map to surface kinds for dynamic rendering:
const surfaceMap = {
textChannel: MessageSurface,
voiceChannel: lazy(() => import('./Voice')),
documentChannel: lazy(() => import('./Documents')),
search: SearchSurface,
spaceSettings: SpaceSettingsSurface,
};
- Surfaces are lazy-loaded with
React.lazy() and Suspense
- DockView manages multi-pane layout
- Tabs identified by
kind/key format
Modal Pattern
Context Menu Pattern
State Management
Three state tools are used for different purposes:
Jotai (UI/Client State)
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
const rightBarOpenState = atom(false);
const themeState = atomWithStorage('theme', 'dark');
const tabNameFamily = atomFamily((id: string) => atom(''));
const [value, setValue] = useAtom(rightBarOpenState);
const value = useAtomValue(rightBarOpenState);
const setValue = useSetAtom(rightBarOpenState);
Valtio (Reactive Data/Managers)
Used in mikoto.js for reactive object proxies:
import { proxy, useSnapshot } from 'valtio';
import { proxyMap } from 'valtio/utils';
React Query (Server State)
LocalDB (Typed localStorage)
import { LocalDB } from '@/store/LocalDB';
const db = new LocalDB('key', schema, initFunction);
db.get();
db.set(value);
API Client (mikoto.js)
MikotoClient
import { useMikoto } from '@/hooks';
function MyComponent() {
const mikoto = useMikoto();
const space = await mikoto.rest['spaces.get'](undefined, {
params: { spaceId: id },
});
mikoto.ws.send('typing.start', { channelId });
}
Manager Pattern
Managers wrap REST/WebSocket with caching:
Context Hooks
export function useMikoto(): MikotoClient;
export function useAuthClient(): AuthClient;
Routing
React Router v6 with browser router:
/ → MainView (shell)
/spaces, /friends, /discover, /settings → Top-level views
/space/:spaceRef → Space view
/space/:spaceRef/channel/:channelId → Channel view
/space/:spaceRef/settings → Space settings
/login, /register, /forgotpassword → Auth views
/invite/:inviteCode → Invite handler
:spaceRef accepts both UUIDs and @handles
- Routes wrapped with
MikotoClientProvider for auth
Custom Hooks
useMikoto();
useAuthClient();
useFetchMember(space);
useContextMenu(fn);
useContextMenuX();
useModalKit();
useTabkit();
useInterval(cb, ms);
useIsMobile();
useErrorElement();
TypeScript Conventions
Config
- Strict mode:
true
- Target:
es2020
- Module resolution:
bundler
- JSX:
react-jsx (new transform)
- Path alias:
@/* → ./src/*
Type Patterns
interface ButtonProps {
variant?: 'solid' | 'outline';
size?: 'sm' | 'md' | 'lg';
}
type ConnectionState = 'connecting' | 'reconnecting' | 'disconnected';
import { z } from 'zod';
const schema = z.object({ name: z.string() });
class CachedManager<T extends { id: string }> { ... }
Strict Safety
- No implicit
any
- Optional chaining (
?.) and nullish coalescing (??) used throughout
- Type guards with
is keyword
- Discriminated unions for state types
File Naming
| Type | Convention | Example |
|---|
| Components | PascalCase | Avatar.tsx, UserArea.tsx |
| Hooks | camelCase with use prefix | useInterval.ts |
| Utilities | camelCase | fileUpload.ts |
| Types/Classes | PascalCase | LocalDB.ts, MikotoClient.ts |
| Directories | camelCase or kebab-case | atoms/, mikoto-ui/ |
Import Conventions
import { Avatar } from '@/components/atoms/Avatar';
import { Button, Dialog } from '@/components/ui';
import { useAuthClient, useMikoto } from '@/hooks';
Key Dependencies
| Category | Libraries |
|---|
| UI | @chakra-ui/react v3, @emotion/styled, framer-motion |
| State | jotai, valtio, @tanstack/react-query v5 |
| Editors | lexical, slate, y.js |
| Real-time | socket.io-client, livekit-client |
| Forms | react-hook-form, zod |
| Layout | dockview-react, re-resizable, react-virtuoso |
| Icons | @fortawesome/react-fontawesome, react-icons |
| Routing | react-router-dom v6 |
Development Commands
pnpm dev
moon :typecheck
moon :lint
moon :lint.fix
moon :format
moon :test
moon :generate
Testing
import { expect, test } from 'vitest';
test('description', () => {
expect(result).toBe(expected);
});
Best Practices
- Use
@/ imports for all app-internal imports
- No casts - prefer to check the types/use zod validators, over using
as (especially no any)
- Type all props with named interfaces, not inline types
- Use Jotai for UI state, Valtio for data proxies, React Query for server state
- Lazy load surfaces and heavy components with
React.lazy()
- Use existing hooks — check
@/hooks before writing new state logic
- Use barrel exports — add new components to relevant
index.ts
- Run
moon :typecheck after all frontend changes
- Use Chakra semantic tokens (
bg.subtle, fg.muted, border) for theme compatibility
- Prefer Chakra style props over inline CSS, but Emotion
styled is acceptable for complex components