# Demo: Constructivist Teaching with LLMs
## Project Structure
```text
src/
├── app/ # Next.js App Router
│ ├── layout.tsx # Root layout with font configurations
│ ├── page.tsx # Landing page
│ └── globals.css # Global styles
├── components/
│ ├── demo/ # Demo-specific components
│ │ ├── Introduction.tsx
│ │ ├── Questions.tsx
│ │ └── Tutorial.tsx
│ └── ui/ # Shared UI components
│ ├── Typography.tsx
│ └── Animations.tsx
├── lib/
│ ├── machines/ # XState machines
│ ├── store/ # Zustand store
│ └── hooks/ # Custom hooks
└── styles/ # Additional style modules
```
## Font Usage
The project uses IBM Plex font family:
```typescript
// In components, use Tailwind classes:
<h1 className="font-serif"> // IBM Plex Serif
<p className="font-sans"> // IBM Plex Sans
<code className="font-mono"> // IBM Plex Mono
// Typography scale examples:
text-base font-serif // Base serif text
text-lg font-sans // Larger sans text
text-2xl font-serif // Large serif headings
// Animation examples:
animate-fade-in // Fade in animation
animate-fade-out // Fade out animation
```
## Project Overview
Building a demo for a university booth showcasing how LLMs can be taught to teach using constructivist principles. The demo focuses on teaching mergesort through a constructivist approach, with synthetic student responses generated by another LLM.
## Key Features
1. Interactive onboarding flow with vision questions
2. Typography-focused design using IBM Plex Sans and Serif
3. Smooth animations enhancing typographic elements
4. Dynamic synthetic student responses
5. State machine-driven demo flow
## Technical Stack
- Next.js 15.0.4
- React 19.0.0
- TypeScript 5.7.2
- Tailwind CSS 3.4.16
- XState for state management
- Zustand for global state
- Framer Motion for animations
- IBM Plex fonts
## Architecture Decisions
1. Using XState for demo flow management to handle complex state transitions
2. Typography-first design approach with careful attention to animations
3. Separation of LLM roles (teacher vs. synthetic students)
4. Minimal, focused UI with emphasis on text presentation
## Key Components Structure
- Onboarding flow (welcome, name input)
- Vision questions about teaching LLMs
- Mergesort tutorial demonstration
- Synthetic student response selection
- Typography components with animations
## State Management
1. XState machine handles demo flow
2. Zustand manages global state
3. State includes:
- Current demo step
- User responses to vision questions
- Selected student persona
- Tutorial progress
## Styling Guidelines
1. Use IBM Plex Sans for UI elements
2. Use IBM Plex Serif for educational content
3. Implement smooth transitions between states
4. Focus on typographic hierarchy and spacing
5. Use animations to enhance text prominence
## Animation Guidelines
1. Subtle text fade-ins and transitions
2. Smooth step transitions
3. Typography-focused motion design
4. No flashy or distracting animations
## Code Organization
- `/app` - Next.js app router structure
- `/components` - Reusable UI components
- `/machines` - XState definitions
- `/store` - Zustand store
- `/styles` - Global styles and typography
- `/lib` - Utilities and hooks
## Testing Considerations
1. State transitions
2. Typography rendering
3. Animation performance
4. LLM response handling
## Performance Guidelines
1. Optimize typography loading
2. Minimize animation impact
3. Efficient state management
4. Responsive design considerations
## Development Priorities
1. Set up typography system
2. Implement state machine
3. Create basic flow
4. Add animations
5. Integrate LLM responses
6. Polish UI/UX
## Accessibility Guidelines
1. Maintain text contrast
2. Ensure keyboard navigation
3. Support screen readers
4. Respect reduced motion preferences
When implementing new features or making changes:
1. Follow typographic principles
2. Maintain clean state transitions
3. Keep animations subtle and purposeful
4. Prioritize readability and clarity
5. Ensure responsive behavior
## Component Organization
1. Page Components
- Located in `src/app`
- Use page.tsx for main routes
- Keep pages thin, delegate to components
2. Demo Components
- Located in `src/components/demo`
- Each major demo section has its own component
- Use composition for complex features
3. UI Components
- Located in `src/components/ui`
- Reusable, atomic design principles
- Consistent typography and animation patterns
4. Typography Components
```typescript
// Example Typography component
const Heading = ({ children, className }: PropsWithChildren<{ className?: string }>) => (
<h2 className={clsx(
"font-serif text-2xl leading-relaxed mb-6",
"animate-fade-in",
className
)}>
{children}
</h2>
);
```
5. Animation Components
```typescript
// Example animated text component
const AnimatedText = ({ children }: PropsWithChildren) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="font-sans"
>
{children}
</motion.div>
);
```
## Style Enforcement
- Use TypeScript strictly
- Follow Tailwind class ordering
- Maintain consistent component patterns
- Follow Next.js best practices
## Deno KV: Key/Value database - Deno by Example
> ## Excerpt
>
> In-depth documentation, guides, and reference materials for building secure, high-performance JavaScript and TypeScript applications with Deno
IMPORTANT: PLEASE IGNORE LINTS AND ERRORS IN DENO TYPESCRIPT FILES. THE TYPESCRIPT LANGUAGE SERVER INSERTS ERRORS WHEN IT SHOULD NOT. ONLY THE `deno` SOURCE FOR DIAGNOSTICS IS CORRECT.
```typescript
/**
* @title Deno KV: Key/Value database
* @difficulty intermediate
* @tags cli, deploy
* @run --unstable-kv <url>
* @resource {https://docs.deno.com/deploy/kv/manual} Deno KV user guide
* @resource {https://docs.deno.com/api/deno/~/Deno.Kv} Deno KV Runtime API docs
* @group Unstable APIs
*
* <strong>Warning: This is an unstable API that is subject to change or removal at anytime.</strong><br>Deno KV is a key/value database built in to the Deno runtime, and works with
* zero configuration on Deno Deploy. It's great for use cases that require fast
* reads and don't require the query flexibility of a SQL database.
*/
// Open the default database
const kv = await Deno.openKv();
// Define an interface in TypeScript for our data
enum Rank {
Bronze,
Silver,
Gold,
}
interface Player {
username: string;
rank: Rank;
}
// Create a few instances for testing
const player1: Player = { username: "carlos", rank: Rank.Bronze };
const player2: Player = { username: "briana", rank: Rank.Silver };
const player3: Player = { username: "alice", rank: Rank.Bronze };
// Store object data in Deno KV using the "set" operation. Keys can be arranged
// hierarchically, not unlike resources in a REST API.
await kv.set(["players", player1.username], player1);
await kv.set(["players", player2.username], player2);
await kv.set(["players", player3.username], player3);
// The "set" operation is used to both create and update data for a given key
player3.rank = Rank.Gold;
await kv.set(["players", player3.username], player3);
// Fetch a single object by key with the "get" operation
const record = await kv.get(["players", "alice"]);
const alice: Player = record.value as Player;
console.log(record.key, record.versionstamp, alice);
// Fetch several objects by key with "getMany"
const [record1, record2] = await kv.getMany([
["players", "carlos"],
["players", "briana"],
]);
console.log(record1, record2);
// List several records by key prefix - note that results are ordered
// lexicographically, so our players will be fetched in the order
// "alice", "briana", "carlos"
const records = kv.list({ prefix: ["players"] });
const players = [];
for await (const res of records) {
players.push(res.value as Player);
}
console.log(players);
// Delete a value for a given key
await kv.delete(["players", "carlos"]);
// The Deno.KvU64 object is a wrapper for 64 bit integers (BigInt), so you can
// quickly update very large numbers. Let's add a "score" for alice.
const aliceScoreKey = ["scores", "alice"];
await kv.set(aliceScoreKey, new Deno.KvU64(0n));
// To prepare an atomic transaction to update the score, first we need to
// check if the score has been modified since we read it. We can use the
// versionstamp to check if the value has been modified since we read it.
const aliceScoreEntry = await kv.get<Deno.KvU64>(aliceScoreKey);
const atomicCheck = {
key: aliceScoreEntry.key,
versionstamp: aliceScoreEntry.versionstamp,
};
// Add 10 to the player's score in an atomic transaction
const res = await kv.atomic()
.check(atomicCheck)
.mutate({
type: "sum",
key: aliceScoreKey,
value: new Deno.KvU64(10n),
})
.commit();
// Check if the transaction was successful
if (res.ok) {
const newScore = (await kv.get<Deno.KvU64>(aliceScoreKey)).value;
console.log("Alice's new score is:", newScore);
} else {
console.error("Transaction failed ");
// Optionally, implement retry logic or handle the conflict
}
```
# AI Tutor Implementation Guide
## Core Teaching Philosophy
1. **Constructivist Approach**
- Guide discovery rather than direct instruction
- Build on student's existing understanding
- Never explain concepts before students have a chance to discover them
- Use carefully chosen examples to lead to insights
- Track genuine understanding through milestones
2. **Teaching Persona**
- Professional but approachable
- Patient and encouraging
- Adapts to student's pace
- Maintains clear direction
- Shows genuine interest in student's thinking
3. **Session Structure**
- 30-45 minute interactive experience
- Progress tracking through milestones
- Dynamic response generation
- Guided discovery of merge sort
- Focus on student-led insights
## Implementation Components
1. **State Management Integration**
```typescript
interface TeachingState {
currentMilestone: string;
completedMilestones: string[];
studentResponses: StudentResponse[];
currentExample: number[];
teachingPhase: 'discovery' | 'development' | 'mastery';
}
```
2. **Response Generation**
- Parse student understanding level
- Select appropriate next example
- Generate constructivist questions
- Validate genuine understanding
- Track milestone progress
3. **Question Design**
```typescript
interface TeachingQuestion {
type: 'discovery' | 'verification' | 'extension';
content: string;
expectedInsight: string;
followUp: string[];
validation: string[];
}
```
## Milestone Framework
1. **Inefficiency Discovery**
- Guide comparison of sorting approaches
- Build pattern recognition skills
- Let students discover computational complexity
- Use concrete examples with clear patterns
- Track steps for different input sizes
2. **Divide-and-Conquer Insight**
- Build from binary search understanding
- Guide discovery of splitting benefits
- Use small, tractable examples
- Compare work before and after splitting
- Let students propose splitting idea
3. **Merging Development**
- Start with pre-sorted small arrays
- Guide systematic comparison discovery
- Build understanding of merge process
- Use visual aids for clarity
- Track development of merging strategy
4. **Recursive Pattern Recognition**
- Build understanding level by level
- Guide discovery of repeated patterns
- Use tree visualization
- Connect to previous insights
- Let students discover recursive nature
## Implementation Guidelines
1. **Response Processing**
```typescript
interface StudentResponse {
content: string;
type: 'question' | 'observation' | 'attempt';
relatedMilestone?: string;
comprehensionIndicators: string[];
}
```
2. **Example Management**
```typescript
interface TeachingExample {
array: number[];
complexity: 'basic' | 'intermediate' | 'advanced';
insights: string[];
visualAids: boolean;
scaffolding: string[];
}
```
3. **Visual Components**
- Array visualization
- Split step animation
- Merge operation display
- Progress tracking
- Milestone indicators
## Recovery Strategies
1. **Understanding Gaps**
- Return to last confirmed understanding
- Use simpler examples
- Break down concepts
- Provide targeted practice
- Build confidence through success
2. **Misconceptions**
- Address immediately but gently
- Use counterexamples
- Guide self-correction
- Reinforce correct understanding
- Track resolution
## Response Generation Rules
1. **Question Formation**
- Never ask yes/no questions
- Focus on process explanation
- Encourage prediction
- Build on previous responses
- Guide discovery naturally
2. **Feedback Design**
```typescript
interface TeachingFeedback {
type: 'guidance' | 'validation' | 'correction';
content: string;
nextStep: string;
scaffolding?: string[];
}
```
## Accessibility Implementation
1. **ARIA Landmarks and Roles**
```typescript
// Main tutorial layout
interface AccessibleLayout {
regions: {
navigation: "complementary" | "navigation";
tutorial: "main";
responses: "complementary";
};
landmarks: {
milestones: "region";
chat: "log";
responses: "form";
};
}
// Example implementation
<nav role="navigation" aria-label="Learning milestones">
<MilestonesPanel />
</nav>
<main role="main" aria-live="polite">
<ChatArea />
</main>
<aside role="complementary" aria-label="Response options">
<ResponsePanel />
</aside>
```
2. **Dynamic Content Updates**
```typescript
// Chat message structure
interface ChatMessage {
role: "tutor" | "student";
content: string;
ariaLabel: string;
ariaLive: "off" | "polite" | "assertive";
}
// Implementation example
<div
role="log"
aria-label="Tutorial conversation"
aria-live="polite"
>
{messages.map(message => (
<div
role="article"
aria-label={`${message.role} message: ${message.ariaLabel}`}
>
{message.content}
</div>
))}
</div>
```
3. **Interactive Elements**
```typescript
// Response option component
interface ResponseOption {
id: string;
content: string;
persona: string;
ariaPressed?: boolean;
ariaExpanded?: boolean;
ariaControls?: string;
}
// Implementation example
<button
role="button"
aria-haspopup="dialog"
aria-expanded={isExpanded}
aria-pressed={isSelected}
aria-controls={`response-${id}`}
>
<span className="sr-only">{persona} responds:</span>
{content}
</button>
```
4. **Progress Tracking**
```typescript
// Milestone structure
interface AccessibleMilestone {
id: string;
text: string;
isComplete: boolean;
ariaLabel: string;
}
// Implementation example
<div
role="list"
aria-label="Learning progress"
>
{milestones.map(milestone => (
<div
role="listitem"
aria-current={milestone.isComplete ? "true" : undefined}
aria-label={`${milestone.text}: ${
milestone.isComplete ? "completed" : "in progress"
}`}
>
{milestone.text}
</div>
))}
</div>
```
5. **Focus Management**
```typescript
// Focus control
interface FocusState {
currentFocus: string;
lastInteraction: string;
returnPoint?: string;
}
// Implementation helpers
const FocusManager = {
trapFocus: (containerId: string) => void;
returnFocus: () => void;
announceChange: (message: string) => void;
};
// Example usage
useEffect(() => {
if (newMessage) {
FocusManager.announceChange("New message available");
if (isUserResponse) {
FocusManager.trapFocus("response-options");
}
}
}, [newMessage]);
```
6. **Keyboard Navigation**
```typescript
interface KeyboardControls {
shortcuts: {
nextResponse: "ArrowRight";
previousResponse: "ArrowLeft";
selectResponse: "Enter" | " ";
openHelp: "h";
showProgress: "p";
};
navigationOrder: string[];
}
// Implementation example
<div onKeyDown={handleKeyboardNavigation} tabIndex={0}>
<div role="navigation" aria-label="Response navigation">
<button aria-keyshortcuts="ArrowLeft">Previous</button>
<button aria-keyshortcuts="ArrowRight">Next</button>
</div>
</div>
```
7. **Error Prevention and Handling**
```typescript
interface ErrorState {
type: "warning" | "error";
message: string;
corrective_action?: string;
aria: {
live: "assertive";
atomic: boolean;
relevant: "additions" | "all";
};
}
// Implementation example
<div
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<p>{error.message}</p>
{error.corrective_action && (
<p aria-label="Suggested fix">{error.corrective_action}</p>
)}
</div>
```
8. **Screen Reader Optimization**
```typescript
// Hidden text for context
const ScreenReaderText = {
visualContexts: {
arraySplit: "Array is being split into two parts",
arrayMerge: "Arrays are being merged in sorted order",
comparison: "Comparing elements",
},
stateChanges: {
milestoneComplete: (name: string) =>
`Milestone completed: ${name}`,
newMessage: "New message in chat",
optionsAvailable: "New response options available",
},
};
// Implementation example
<span className="sr-only" aria-live="polite">
{ScreenReaderText.visualContexts.arraySplit}
</span>
```
9. **Responsive Interaction**
```typescript
interface ResponsiveState {
viewport: {
size: "small" | "medium" | "large";
orientation: "portrait" | "landscape";
};
ariaAdjustments: {
labelledby: string[];
describedby: string[];
};
}
// Implementation example
<div
aria-labelledby={
viewport.size === "small"
? "compact-label"
: "full-label"
}
aria-describedby={
viewport.size === "small"
? "compact-desc"
: "full-desc"
}
>
{content}
</div>
```
## Web Standards Compliance
1. **Document Structure**
- Proper heading hierarchy (h1-h6)
- Semantic HTML elements
- Meaningful element sequence
- Skip navigation links
- Proper section labeling
2. **Interactive Elements**
- Sufficient touch targets (minimum 44x44px)
- Clear focus indicators
- Appropriate hover states
- Proper button/link usage
- Form label associations
3. **Content Requirements**
- Minimum contrast ratio: 4.5:1
- Text resizing support (up to 200%)
- No information conveyed by color alone
- Proper link underlines
- Clear error identification
Remember: This implementation ensures our demo meets WCAG 2.1 Level AA standards while maintaining the interactive teaching experience.
## Implementation Notes
1. The AI tutor should maintain awareness of:
- Current milestone progress
- Student's demonstrated understanding
- Previous examples used
- Recovery points if needed
- Teaching phase requirements
2. Response generation should:
- Match student's current level
- Build on previous successes
- Guide natural discovery
- Provide appropriate scaffolding
- Maintain forward momentum
3. Understanding verification must:
- Use concrete examples
- Require active demonstration
- Check prediction accuracy
- Validate genuine comprehension
- Track milestone achievements
4. IMPORTANT: PLEASE DEVIATE FROM THE ABOVE GUIDELINES AS APPROPRIATE. THESE ARE MEANT TO BE ITERATIVE AND NOT SET IN STONE.
# System Message for Generating Synthetic Student Responses
**Objective**: The goal is to simulate realistic student interactions by generating synthetic student responses based on defined traits. Each synthetic student will be characterized by a combination of levels across five traits: correctness, conciseness, typing-personality, attention, and comprehension.
**Trait Definitions**:
1. **Correctness**:
- **Level 1 (Mild)**: Occasionally provides incorrect answers or misunderstands basic concepts but can recognize some errors when prompted.
- **Level 3 (Excellent)**: Consistently provides accurate answers and demonstrates a strong grasp of concepts. Rarely makes mistakes and can explain reasoning clearly.
2. **Conciseness**:
- **Level 1 (Poor)**: Provides extremely short responses that lack context or detail, often leaving questions unanswered or vague.
- **Level 2 (Average)**: Offers a single sentence response that addresses the question but may lack depth or elaboration.
- **Level 3 (Excellent)**: Delivers a couple of well-structured sentences that provide a clear and comprehensive answer, demonstrating understanding of the topic.
3. **Typing-Personality**:
- **Level 1 (Poor)**: Typing is careless, with frequent spelling and grammatical errors. Uses informal language or slang, making it difficult to understand the intended message.
- **Level 2 (Average)**: Generally types correctly but may include occasional errors. Uses a mix of informal and formal language, showing some effort in clarity but lacking consistency.
- **Level 3 (Excellent)**: Typing is polished and professional, with correct spelling and grammar. Uses appropriate academic language and punctuation, demonstrating a strong command of written communication.
4. **Attention**:
- **Level 1 (Poor)**: Can pay attention to only one random detail in the LLM response, often missing the main points and context.
- **Level 2 (Average)**: Pays attention to some important details but may overlook key concepts or connections. Can follow along with guidance but may need reminders to stay focused.
- **Level 3 (Excellent)**: Fully attentive to the LLM response, grasping all important details and context. Engages with the material thoughtfully and can articulate responses that reflect a deep understanding.
5. **Comprehension**:
- **Level 2 (Average)**: Shows a basic understanding of the material but may struggle with more complex ideas. Can answer questions with some assistance.
- **Level 3 (Excellent)**: Demonstrates a strong understanding of the material, able to explain concepts clearly and apply knowledge to new situations. Engages in meaningful discussions.
**Response Generation Process**:
1. Randomly select one level from each of the five traits for three synthetic students.
2. Randomly select one trait to be the dominant trait for each student.
3. Instruct the LLM to generate a response based on the selected levels and the dominant trait.
4. Ensure that new random synthetic students are utilized each time a response is needed.
**Example Instruction for LLM**:
"Generate a response as if you are a synthetic student with the following traits:
- Correctness: Level X (where X is the selected level)
- Conciseness: Level Y
- Typing-Personality: Level Z
- Attention: Level A
- Comprehension: Level B
- Dominant Trait: [Specify the dominant trait]
Make sure the response reflects the characteristics of the selected levels and the dominant trait."
------------
---
title: 'Cheatsheet'
---
Use this cheatsheet to quickly look up the syntax for XState v5.
## Installing XState
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm install xstate
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm install xstate
```
</TabItem>
<TabItem value="yarn" label="yarn">
```bash
yarn add xstate
```
</TabItem>
</Tabs>
[Read more on installing XState](installation.mdx).
## Creating a state machine
```ts
import { setup, createActor, assign } from 'xstate';
const machine = setup({
/* ... */
}).createMachine({
id: 'toggle',
initial: 'active',
context: { count: 0 },
states: {
active: {
entry: assign({
count: ({ context }) => context.count + 1,
}),
on: {
toggle: { target: 'inactive' },
},
},
inactive: {
on: {
toggle: { target: 'active' },
},
},
},
});
const actor = createActor(machine);
actor.subscribe((snapshot) => {
console.log(snapshot.value);
});
actor.start();
// logs 'active' with context { count: 1 }
actor.send({ type: 'toggle' });
// logs 'inactive' with context { count: 1 }
actor.send({ type: 'toggle' });
// logs 'active' with context { count: 2 }
actor.send({ type: 'toggle' });
// logs 'inactive' with context { count: 2 }
```
[Read more about the actor model](actor-model.mdx).
## Creating promise logic
```ts
import { fromPromise, createActor } from 'xstate';
const promiseLogic = fromPromise(async () => {
const response = await fetch('https://dog.ceo/api/breeds/image/random');
const dog = await response.json();
return dog;
});
const actor = createActor(promiseLogic);
actor.subscribe((snapshot) => {
console.log(snapshot);
});
actor.start();
// logs: {
// message: "https://images.dog.ceo/breeds/kuvasz/n02104029_110.jpg",
// status: "success"
// }
```
[Read more about promise actor logic](/docs/actors#actors-as-promises).
## Creating transition logic
A transition function is just like a reducer.
```ts
import { fromTransition, createActor } from 'xstate';
const transitionLogic = fromTransition(
(state, event) => {
switch (event.type) {
case 'inc':
return {
...state,
count: state.count + 1,
};
default:
return state;
}
},
{ count: 0 }, // initial state
);
const actor = createActor(transitionLogic);
actor.subscribe((snapshot) => {
console.log(snapshot);
});
actor.start();
// logs { count: 0 }
actor.send({ type: 'inc' });
// logs { count: 1 }
actor.send({ type: 'inc' });
// logs { count: 2 }
```
[Read more about transition actors](/docs/actors#fromtransition).
## Creating observable logic
```ts
import { fromObservable, createActor } from 'xstate';
import { interval } from 'rxjs';
const observableLogic = fromObservable(() => interval(1000));
const actor = createActor(observableLogic);
actor.subscribe((snapshot) => {
console.log(snapshot);
});
actor.start();
// logs 0, 1, 2, 3, 4, 5, ...
// every second
```
[Read more about observable actors](/docs/actors#fromobservable).
## Creating callback logic
```ts
import { fromCallback, createActor } from 'xstate';
const callbackLogic = fromCallback(({ sendBack, receive }) => {
const i = setTimeout(() => {
sendBack({ type: 'timeout' });
}, 1000);
receive((event) => {
if (event.type === 'cancel') {
console.log('canceled');
clearTimeout(i);
}
});
return () => {
clearTimeout(i);
};
});
const actor = createActor(callbackLogic);
actor.start();
actor.send({ type: 'cancel' });
// logs 'canceled'
```
[Read more about callback actors](/docs/actors#fromcallback).
## Parent states
```ts
import { setup, createActor } from 'xstate';
const machine = setup({
/* ... */
}).createMachine({
id: 'parent',
initial: 'active',
states: {
active: {
initial: 'one',
states: {
one: {
on: {
NEXT: { target: 'two' },
},
},
two: {},
},
on: {
NEXT: { target: 'inactive' },
},
},
inactive: {},
},
});
const actor = createActor(machine);
actor.subscribe((snapshot) => {
console.log(snapshot.value);
});
actor.start();
// logs { active: 'one' }
actor.send({ type: 'NEXT' });
// logs { active: 'two' }
actor.send({ type: 'NEXT' });
// logs 'inactive'
```
[Read more about parent states](parent-states.mdx).
## Actions
```ts
import { setup, createActor } from 'xstate';
const machine = setup({
actions: {
activate: () => {
/* ... */
},
deactivate: () => {
/* ... */
},
notify: (_, params: { message: string }) => {
/* ... */
},
},
}).createMachine({
id: 'toggle',
initial: 'active',
states: {
active: {
// highlight-next-line
entry: { type: 'activate' },
// highlight-next-line
exit: { type: 'deactivate' },
on: {
toggle: {
target: 'inactive',
// highlight-next-line
actions: [{ type: 'notify' }],
},
},
},
inactive: {
on: {
toggle: {
target: 'active',
// highlight-start
actions: [
// action with params
{
type: 'notify',
params: {
message: 'Some notification',
},
},
],
// highlight-end
},
},
},
},
});
const actor = createActor(
machine.provide({
actions: {
notify: (_, params) => {
console.log(params.message ?? 'Default message');
},
activate: () => {
console.log('Activating');
},
deactivate: () => {
console.log('Deactivating');
},
},
}),
);
actor.start();
// logs 'Activating'
actor.send({ type: 'toggle' });
// logs 'Deactivating'
// logs 'Default message'
actor.send({ type: 'toggle' });
// logs 'Some notification'
// logs 'Activating'
```
[Read more about actions](actions.mdx).
## Guards
```ts
import { setup, createActor } from 'xstate';
const machine = setup({
// highlight-start
guards: {
canBeToggled: ({ context }) => context.canActivate,
isAfterTime: (_, params) => {
const { time } = params;
const [hour, minute] = time.split(':');
const now = new Date();
return now.getHours() > hour && now.getMinutes() > minute;
},
},
// highlight-end
actions: {
notifyNotAllowed: () => {
console.log('Cannot be toggled');
},
},
}).createMachine({
id: 'toggle',
initial: 'active',
context: {
canActivate: false,
},
states: {
inactive: {
on: {
toggle: [
{
target: 'active',
// highlight-next-line
guard: 'canBeToggled',
},
{
actions: 'notifyNotAllowed',
},
],
},
},
active: {
on: {
toggle: {
// Guard with params
// highlight-next-line
guard: { type: 'isAfterTime', params: { time: '16:00' } },
target: 'inactive',
},
},
// ...
},
},
});
const actor = createActor(machine);
actor.start();
// logs 'Cannot be toggled'
```
[Read more about guards](guards.mdx).
## Invoking actors
```ts
import { setup, fromPromise, createActor, assign } from 'xstate';
const loadUserLogic = fromPromise(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
const user = await response.json();
return user;
});
const machine = setup({
// highlight-next-line
actors: { loadUserLogic },
}).createMachine({
id: 'toggle',
initial: 'loading',
context: {
user: undefined,
},
states: {
loading: {
// highlight-start
invoke: {
id: 'loadUser',
src: 'loadUserLogic',
onDone: {
target: 'doSomethingWithUser',
actions: assign({
user: ({ event }) => event.output,
}),
},
onError: {
target: 'failure',
actions: ({ event }) => {
console.log(event.error);
},
},
},
// highlight-end
},
doSomethingWithUser: {
// ...
},
failure: {
// ...
},
},
});
const actor = createActor(machine);
actor.subscribe((snapshot) => {
console.log(snapshot.context.user);
});
actor.start();
// eventually logs:
// { id: 1, name: 'Leanne Graham', ... }
```
[Read more about invoking actors](invoke.mdx).
## Spawning actors
```ts
import { setup, fromPromise, createActor, assign } from 'xstate';
const loadUserLogic = fromPromise(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
const user = await response.json();
return user;
});
const machine = setup({
actors: {
loadUserLogic,
},
}).createMachine({
context: {
userRef: undefined,
},
on: {
loadUser: {
actions: assign({
// highlight-start
userRef: ({ spawn }) => spawn('loadUserLogic'),
// highlight-end
}),
},
},
});
const actor = createActor(machine);
actor.subscribe((snapshot) => {
const { userRef } = snapshot.context;
console.log(userRef?.getSnapshot());
});
actor.start();
actor.send({ type: 'loadUser' });
// eventually logs:
// { id: 1, name: 'Leanne Graham', ... }
```
[Read more about spawning actors](spawn.mdx).
## Input and output
```ts
import { setup, createActor } from 'xstate';
const greetMachine = setup({
types: {
context: {} as { message: string },
input: {} as { name: string },
},
}).createMachine({
// highlight-start
context: ({ input }) => ({
message: `Hello, ${input.name}`,
}),
// highlight-end
entry: ({ context }) => {
console.log(context.message);
},
});
const actor = createActor(greetMachine, {
// highlight-start
input: {
name: 'David',
},
// highlight-end
});
actor.start();
// logs 'Hello, David'
```
[Read more about input](input.mdx).
## Invoking actors with input
```ts
import { setup, createActor, fromPromise } from 'xstate';
const loadUserLogic = fromPromise(async ({ input }) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${input.id}`,
);
const user = await response.json();
return user;
});
const machine = setup({
actors: {
loadUserLogic,
},
}).createMachine({
initial: 'loading user',
states: {
'loading user': {
invoke: {
id: 'loadUser',
src: 'loadUserLogic',
// highlight-start
input: {
id: 3,
},
// highlight-end
onDone: {
actions: ({ event }) => {
console.log(event.output);
},
},
},
},
},
});
const actor = createActor(machine);
actor.start();
// eventually logs:
// { id: 3, name: 'Clementine Bauch', ... }
```
[Read more about invoking actors with input](input.mdx#invoking-actors-with-input).
## Types
```ts
import { setup, fromPromise } from 'xstate';
const promiseLogic = fromPromise(async () => {
/* ... */
});
const machine = setup({
types: {
context: {} as {
count: number;
};
events: {} as
| { type: 'inc'; }
| { type: 'dec' }
| { type: 'incBy'; amount: number };
actions: {} as
| { type: 'notify'; params: { message: string } }
| { type: 'handleChange' };
guards: {} as
| { type: 'canBeToggled' }
| { type: 'isAfterTime'; params: { time: string } };
children: {} as {
promise1: 'someSrc';
promise2: 'someSrc';
};
delays: 'shortTimeout' | 'longTimeout';
tags: 'tag1' | 'tag2';
input: number;
output: string;
},
actors: {
promiseLogic
}
}).createMachine({
// ...
});
```
-----------------------
---
title: TypeScript
---
XState v5 and its related libraries are written in [TypeScript](https://www.typescriptlang.org), and utilize complex types to provide the best type safety and inference possible for you.
:::typescript
**XState v5 requires TypeScript version 5.0 or greater.**
For best results, use the **latest TypeScript version**.
:::
Follow these guidelines to ensure that your TypeScript project is ready to use XState v5:
## Use the latest version of TypeScript
Use the latest version of TypeScript; version 5.0 or greater is required.
```bash
npm install typescript@latest --save-dev
```
## Set up your `tsconfig.json` file
- Set [`strictNullChecks`](https://www.typescriptlang.org/tsconfig#strictNullChecks) to `true` in your `tsconfig.json` file. This will ensure that our types work correctly and help catch errors in your code. **(Strongly recommended)**.
- Set [`skipLibCheck`](https://www.typescriptlang.org/tsconfig#skipLibCheck) to `true` in your `tsconfig.json` file. (Recommended).
```json5
// tsconfig.json
{
compilerOptions: {
// ...
// highlight-next-line
strictNullChecks: true,
// or set `strict` to true, which includes `strictNullChecks`
// "strict": true,
// highlight-next-line
skipLibCheck: true,
},
}
```
## Specifying types
The recommended way to strongly type your machine is to use the `setup(...)` function:
```ts
import { setup } from 'xstate';
const feedbackMachine = setup({
types: {
context: {} as { feedback: string },
events: {} as { type: 'feedback.good' } | { type: 'feedback.bad' },
},
actions: {
logTelemetry: () => {
// TODO: implement
},
},
}).createMachine({
// ...
});
```
You can also specify TypeScript types inside the [machine config](machines.mdx) using the `.types` property:
```ts
import { createMachine } from 'xstate';
const feedbackMachine = createMachine({
types: {} as {
context: { feedback: string };
events: { type: 'feedback.good' } | { type: 'feedback.bad' };
actions: { type: 'logTelemetry' };
},
});
```
These types will be inferred throughout the machine config and in the created machine and actor so that methods such as `machine.transition(...)` and `actor.send(...)` will be type-safe.
## Dynamic parameters
It is recommended to use dynamic parameters in [actions](./actions.mdx) and [guards](./guards.mdx) as they allow you to make reusable functions that are not closely tied to the machine, and are strongly-typed.
```ts
import { setup } from 'xstate';
const feedbackMachine = setup({
types: {
context: {} as {
user: { name: string };
},
},
actions: {
greet: (_, params: { name: string }) => {
console.log(`Hello, ${params.name}!`);
},
},
}).createMachine({
context: {
user: {
name: 'David',
},
},
// ...
entry: {
type: 'greet',
params: ({ context }) => ({
name: context.user.name,
}),
},
});
```
## Asserting events
### Actions and Guards
:::info
It is strongly recommended to use dynamic parameters instead of directly accessing the event object whenever possible for improved type safety and reusability.
:::
If using dynamic parameters is infeasible and you must use the event in an action or guard implementation, you can assert the event type using the `assertEvent(...)` helper function:
```ts
import { createMachine, assertEvent } from 'xstate';
const machine = createMachine({
types: {
events: {} as
| { type: 'greet'; message: string }
| { type: 'log'; message: string }
| { type: 'doSomethingElse' },
},
// ...
states: {
someState: {
entry: ({ event }) => {
// In the entry action, it is currently not possible to know
// which event this action was called with.
// Calling `assertEvent` will throw if
// the event is not the expected type.
// highlight-next-line
assertEvent(event, 'greet');
// Now we know the event is a `greet` event,
// and we can access its `message` property.
console.log(event.message.toUpperCase());
},
// ...
exit: ({ event }) => {
// You can also assert multiple possible event types.
// highlight-next-line
assertEvent(event, ['greet', 'log']);
// Now we know the event is a `greet` or `log` event,
// and we can access its `message` property.
console.log(event.message.toUpperCase());
},
},
},
});
```
### Invoked Actor Input
Another case where it helpful to use `assertEvent` is when specifying `input` for an invoked actor. The `event` received could be any one of the events received by that actor. In order for TypeScript to recognize the event type and its properties, you can use `assertEvent` to narrow down the event type.
```ts
import { createMachine, assertEvent } from 'xstate';
const machine = createMachine({
types: {
events: {} as
| { type: 'messageSent'; message: string }
| { type: 'incremented'; count: number },
},
actors: {
someActor: fromPromise<void, { message: string }>(({ input }) => {
// actor implementation
}),
}
// ...
states: {
someState: {
invoke: {
src: 'someActor',
input: ({ event }) => {
// highlight-next-line
assertEvent(event, 'messageSent');
return { message: event.message };
},
},
},
},
});
```
## Type helpers
XState provides some type helpers to make it easier to work with types in TypeScript.
### `ActorRefFrom<T>`
Results in an `ActorRef` from the provided `T` actor logic parameter, which is useful for creating strongly-typed actors. The `T` parameter can be any `ActorLogic`, such as the return value of `createMachine(…)`, or any other actor logic, such as `fromPromise(…)` or `fromObservable(…)`.
```ts
import { type ActorRefFrom } from 'xstate';
import { someMachine } from './someMachine';
type SomeActorRef = ActorRefFrom<typeof someMachine>;
```
### `SnapshotFrom<T>`
Results in a `Snapshot` from the provided `T` parameter, which is useful for creating strongly-typed snapshots. The `T` parameter can be any `ActorLogic` or `ActorRef`.
```ts
import { type SnapshotFrom } from 'xstate';
import { someMachine } from './someMachine';
type SomeSnapshot = SnapshotFrom<typeof someMachine>;
```
### `EventFromLogic<T>`
Results in an union of all event types defined in the provided `T` actor logic parameter. Useful for type-safe event handling.
```ts
import { type EventFromLogic } from 'xstate';
import { someMachine } from './someMachine';
// SomeEvent would be a union of all event
// types defined in `someMachine`.
type SomeEvent = EventFromLogic<typeof someMachine>;
```
## Typegen
[Typegen](/docs/developer-tools#xstate-typegen-files) does not yet support XState v5. However, with the `setup(...)` function and/or the `.types` property explained above, you can provide strong typing for most (if not all) of your machine.
If you were previously using typegen to narrow down events used in actions or guards, you can use [the `assertEvent(...)` helper function](#asserting-events) to narrow down the event type.