Skip to content

Components

AIDK supports both class-based and functional components. Both can be nested, composed, and reused.

Component Types

Class components have full access to lifecycle hooks and are the preferred style:

tsx
import { Component, COM, TickState, signal, comState } from 'aidk';

class MyAgent extends Component {
  // Local state (component-only)
  private count = signal(0);

  // Shared state (persisted across ticks)
  private timeline = comState<any[]>('timeline', []);

  // Lifecycle: Called when component mounts
  async onMount(com: COM) {
    console.log('Component mounted');
    await this.loadInitialState();
  }

  // Lifecycle: Called before each tick
  onTickStart(com: COM, state: TickState) {
    if (state.current?.timeline) {
      this.timeline.update(t => [...t, ...state.current.timeline]);
    }
  }

  // Required: Render method
  render(com: COM, state: TickState) {
    return (
      <>
        <AiSdkModel model={openai('gpt-5.2')} />
        <Timeline>
          {this.timeline().map((entry, i) => (
            <Message key={i} {...entry.message} />
          ))}
        </Timeline>
      </>
    );
  }

  // Lifecycle: Called when component unmounts
  onUnmount(com: COM) {
    console.log('Component unmounting');
  }
}

Benefits:

  • Full lifecycle hook support
  • Signal-based state management
  • this context for methods
  • Preferred for agents and complex components

Functional Components

Functional components are simpler and support both stateless and stateful patterns:

Simple Presentational Component

tsx
import { Section, H2, List, ListItem } from 'aidk';

interface UserProfileProps {
  user: User;
}

export function UserProfile({ user }: UserProfileProps) {
  return (
    <Section audience="model">
      <H2>User Profile</H2>
      <List>
        <ListItem>Name: {user.name}</ListItem>
        <ListItem>Email: {user.email}</ListItem>
        <ListItem>Tier: {user.tier}</ListItem>
      </List>
    </Section>
  );
}

Stateful Component with Hooks

tsx
import { useSignal, useComState, useOnMount, useTickStart } from 'aidk';

function MessageTimeline() {
  // Local state
  const count = useSignal(0);

  // Shared state
  const timeline = useComState<Message[]>('timeline', []);

  // Lifecycle hooks
  useOnMount((com) => {
    console.log('Timeline mounted');
  });

  useTickStart((com, state) => {
    if (state.current?.timeline) {
      timeline.update(t => [...t, ...state.current.timeline]);
    }
  });

  return (
    <Timeline>
      {timeline().map((msg, i) => (
        <Message key={i} {...msg} />
      ))}
    </Timeline>
  );
}

Benefits:

  • Simple, concise syntax
  • Props-based (fully typed)
  • Great for reusable components
  • Full hook support - state, lifecycle, effects
  • Async-first hooks (unlike React)

When to Use Each

Use CaseRecommended
Root agentClass component (preferred) or functional with hooks
Tool componentClass component or functional with hooks
Stateful componentEither - class with signal() or functional with useSignal()
Simple presentationFunctional component (no hooks needed)
Reusable UI pieceFunctional component
Nested formattingFunctional component

General Rule: Both styles have full capabilities. Choose based on preference:

  • Class components: Traditional OOP style, clear lifecycle methods
  • Functional components: Modern hooks style, more concise

Nesting Components

Components can render other components, creating a tree:

Functional in Class

tsx
// Functional component
function FormattedMessage({ message }: { message: Message }) {
  return (
    <Message role={message.role}>
      {message.content.map((block, i) => {
        if (block.type === 'image') {
          return (
            <Text key={i}>
              [Image]: {block.altText}
            </Text>
          );
        }
        return block;
      })}
    </Message>
  );
}

// Class component using it
class ChatAgent extends Component {
  private timeline = comState<any[]>('timeline', []);

  render(com: COM, state: TickState) {
    return (
      <>
        <AiSdkModel model={openai('gpt-5.2')} />

        <Timeline>
          {this.timeline().map((entry, index) => (
            <FormattedMessage
              key={`msg-${index}`}
              message={entry.message}
            />
          ))}
        </Timeline>
      </>
    );
  }
}

Class in Class

tsx
class UserContext extends Component {
  render(com: COM) {
    const ctx = context();
    return (
      <Section audience="model">
        <H3>User Context</H3>
        <Paragraph>User: {ctx.user.name}</Paragraph>
      </Section>
    );
  }
}

class MainAgent extends Component {
  render(com: COM, state: TickState) {
    return (
      <>
        <AiSdkModel model={openai('gpt-5.2')} />

        {/* Nested class component */}
        <UserContext />

        <Timeline>{/* ... */}</Timeline>
      </>
    );
  }
}

Deep Nesting

Components can be nested arbitrarily deep:

tsx
function MessageBlock({ block }: { block: ContentBlock }) {
  if (block.type === 'image') {
    return <Text>[Image: {block.altText}]</Text>;
  }
  if (block.type === 'text') {
    return <Text>{block.text}</Text>;
  }
  return null;
}

function FormattedMessage({ message }: { message: Message }) {
  return (
    <Message role={message.role}>
      {message.content.map((block, i) => (
        <MessageBlock key={i} block={block} />
      ))}
    </Message>
  );
}

function SlidingWindow({ messages }: { messages: Message[] }) {
  const recent = messages.slice(-10);

  return (
    <Timeline>
      {recent.map((msg, i) => (
        <FormattedMessage key={i} message={msg} />
      ))}
    </Timeline>
  );
}

class ChatAgent extends Component {
  private timeline = comState<Message[]>('timeline', []);

  async onMout(com: COM) {
    const ctx = context();
    const userInput = com.getUserInput();
    const messages = await loadThreadMessages(ctx.user.id, ctx.metadata.threadId);
    this.timeline.set(message);
  }

  onTickStart(com: COM, state: TickState) {
    this.timeline.update((timeline) => [...timeline, ...state.current.timeline.map(e => e.message)]);
  }

  render() {
    return (
      <>
        <AiSdkModel model={openai('gpt-5.2')} />
        <SlidingWindow messages={this.timeline()} />
      </>
    );
  }
}

Component tree:

ChatAgent (class)
└── SlidingWindow (function)
    └── FormattedMessage (function)
        └── MessageBlock (function)
            └── Text (primitive)

Lifecycle Hooks

Class Components

Class components use lifecycle methods:

tsx
class LifecycleExample extends Component {
  async onMount(com: COM) {
    // Called once when component mounts
    console.log('Mounted');
  }

  async onStart(com: COM) {
    // Called before first tick
    console.log('Starting');
  }

  onTickStart(com: COM, state: TickState) {
    // Called before each render
    console.log(`Tick ${state.tick} starting`);
  }

  render(com: COM, state: TickState) {
    // Called every tick to build context
    return <>{/* ... */}</>;
  }

  onAfterCompile(com: COM, compiled: any, state: TickState) {
    // Called after compilation, before model call
    console.log('Context compiled');
  }

  onTickEnd(com: COM, state: TickState) {
    // Called after model responds
    console.log(`Tick ${state.tick} complete`);
  }

  onComplete(com: COM, finalState: any) {
    // Called when execution finishes
    console.log('Complete');
  }

  onUnmount(com: COM) {
    // Called when component is removed
    console.log('Unmounting');
  }

  onError(com: COM, error: Error, state: TickState) {
    // Called on errors
    console.error('Error:', error);
  }
}

Function Components

Function components use lifecycle hooks:

tsx
import {
  useOnMount,
  useOnUnmount,
  useTickStart,
  useTickEnd,
  useInit,
  useEffect
} from 'aidk';

function LifecycleExample() {
  // Initialization (blocking, runs during render)
  await useInit(async (com, state) => {
    console.log('Initializing...');
  });

  // Mount (non-blocking, runs after first render)
  useOnMount((com) => {
    console.log('Mounted');
  });

  // Before each tick
  useTickStart((com, state) => {
    console.log(`Tick ${state.tick} starting`);
  });

  // After each tick
  useTickEnd((com, state) => {
    console.log(`Tick ${state.tick} complete`);
  });

  // Unmount
  useOnUnmount((com) => {
    console.log('Unmounting');
  });

  // Side effects with dependencies
  useEffect(async () => {
    console.log('Effect running');
    return () => console.log('Effect cleanup');
  }, [/* deps */]);

  return <Text>Hello</Text>;
}

Component Props

Both component types support typed props:

Functional Component Props

tsx
interface UserCardProps {
  user: User;
  showEmail?: boolean;
  tier?: string;
}

export function UserCard({ user, showEmail = true, tier }: UserCardProps) {
  return (
    <Section audience="model">
      <Paragraph>Name: {user.name}</Paragraph>
      {showEmail && <Paragraph>Email: {user.email}</Paragraph>}
      {tier && <Paragraph>Tier: {tier}</Paragraph>}
    </Section>
  );
}

// Usage
<UserCard user={user} showEmail={false} tier="premium" />

Class Component Props

tsx
interface AgentProps {
  model?: string;
  temperature?: number;
}

class ConfigurableAgent extends Component<AgentProps> {
  render(com: COM, state: TickState) {
    const { model = 'gpt-5.2', temperature = 0.7 } = this.props;

    return (
      <>
        <AiSdkModel
          model={openai(model)}
          temperature={temperature}
        />
        <Timeline>{/* ... */}</Timeline>
      </>
    );
  }
}

// Usage
<ConfigurableAgent model="gpt-5.2-mini" temperature={0.9} />

Component Composition Patterns

Container/Presenter Pattern

tsx
// Presenter (functional)
function MessageList({ messages }: { messages: Message[] }) {
  return (
    <Timeline>
      {messages.map((msg, i) => (
        <Message key={i} role={msg.role} content={msg.content} />
      ))}
    </Timeline>
  );
}

// Container (class)
class ChatContainer extends Component {
  private messages = comState<Message[]>('messages', []);

  onTickStart(com, state) {
    if (state.current?.timeline) {
      this.messages.update(m => [...m, ...state.current.timeline]);
    }
  }

  render() {
    return (
      <>
        <AiSdkModel model={openai('gpt-5.2')} />
        <MessageList messages={this.messages()} />
      </>
    );
  }
}

Higher-Order Components

tsx
// HOC that adds user context
function withUserContext<P>(Component: (props: P) => JSX.Element) {
  return (props: P) => {
    const ctx = context();
    return (
      <>
        <Section audience="model">
          <Paragraph>User: {ctx.user.name}</Paragraph>
        </Section>
        <Component {...props} />
      </>
    );
  };
}

// Use it
const UserAwareProfile = withUserContext(UserProfile);

<UserAwareProfile user={user} />

Render Props Pattern

tsx
interface DataFetcherProps {
  endpoint: string;
  children: (data: any) => JSX.Element;
}

class DataFetcher extends Component<DataFetcherProps> {
  private data = comState<any>('data', null);

  async onMount(com) {
    const result = await fetch(this.props.endpoint);
    this.data.set(await result.json());
  }

  render() {
    const data = this.data();
    return data ? this.props.children(data) : null;
  }
}

// Usage
<DataFetcher endpoint="/api/users">
  {(users) => (
    <List>
      {users.map(u => <ListItem key={u.id}>{u.name}</ListItem>)}
    </List>
  )}
</DataFetcher>

Best Practices

1. Use Class Components for Agents

tsx
// ✅ Good: Agent as class component
class ChatAgent extends Component {
  private timeline = comState<any[]>('timeline', []);

  onTickStart(com, state) { /* ... */ }
  render(com, state) { /* ... */ }
}

// ❌ Less good: Agent as functional component
function ChatAgent() {
  // Can't use lifecycle hooks or state
  return <>{/* ... */}</>;
}

2. Use Functional Components for Presentation

tsx
// ✅ Good: Simple presenter
function UserCard({ user }: { user: User }) {
  return (
    <Section>
      <Paragraph>{user.name}</Paragraph>
    </Section>
  );
}

// ❌ Overkill: Class for simple presentation
class UserCard extends Component {
  render() {
    return (
      <Section>
        <Paragraph>{this.props.user.name}</Paragraph>
      </Section>
    );
  }
}

3. Extract Reusable Components

tsx
// ✅ Good: Extracted reusable component
function MessageTimestamp({ timestamp }: { timestamp: Date }) {
  return <Text>[{timestamp.toLocaleString()}]</Text>;
}

// Use it everywhere
<Message>
  <MessageTimestamp timestamp={msg.createdAt} />
  {msg.content}
</Message>

4. Type Your Props

tsx
// ✅ Good: Typed props
interface Props {
  user: User;
  showDetails: boolean;
}

function UserProfile({ user, showDetails }: Props) {
  // TypeScript knows the types
}

// ❌ Avoid: Untyped props
function UserProfile(props: any) {
  // No type safety
}

Next: State Management

Released under the MIT License.