<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][README.md]" value="# State Management

Demonstrates persistent state management in Rivet Actors with automatic state saving and restoration.

## Getting Started

```sh
git clone https://github.com/rivet-dev/rivet.git
cd rivet/examples/state
npm install
npm run dev
```


## Features

- **Persistent state**: Actor state automatically saved and restored across restarts
- **Typed state management**: Full TypeScript type safety for state objects
- **State initialization**: Define initial state with `createState` or `state` property
- **Automatic serialization**: State changes automatically persisted without manual saves

## Implementation

This example demonstrates state management in Rivet Actors with a simple counter:

- **Actor Definition** ([`src/backend/registry.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/state/src/backend/registry.ts)): Defines the `counter` actor with a count state that persists across actor restarts

## Resources

Read more about [state management](/docs/actors/state), [actions](/docs/actors/actions), and [lifecycle hooks](/docs/actors/lifecycle).

## License

MIT
">
<input type="hidden" name="project[files][index.html]" value="&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Quickstart: State&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .chat-container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .header {
            background: #007bff;
            color: white;
            padding: 30px;
            text-align: center;
        }
        .header h1 {
            margin: 0 0 10px 0;
            font-size: 2em;
        }
        .header p {
            margin: 0;
            opacity: 0.9;
        }
        .user-input {
            padding: 20px;
            border-bottom: 1px solid #eee;
            display: flex;
            gap: 10px;
            align-items: center;
        }
        .user-input label {
            font-weight: 500;
        }
        .user-input input {
            flex: 1;
            padding: 8px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
        }
        .messages {
            height: 400px;
            overflow-y: auto;
            padding: 20px;
            border-bottom: 1px solid #eee;
        }
        .message {
            margin-bottom: 15px;
            padding: 12px;
            background: #f8f9fa;
            border-radius: 8px;
            border-left: 4px solid #007bff;
        }
        .message-header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
        }
        .message-sender {
            font-weight: bold;
            color: #007bff;
        }
        .message-timestamp {
            font-size: 0.85em;
            color: #666;
        }
        .message-text {
            color: #333;
        }
        .empty-message {
            text-align: center;
            color: #666;
            padding: 40px;
            font-style: italic;
        }
        .input-area {
            display: flex;
            padding: 20px;
            gap: 10px;
        }
        .input-area input {
            flex: 1;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 16px;
        }
        .input-area button {
            padding: 12px 24px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 16px;
            font-weight: 500;
        }
        .input-area button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        .input-area button:hover:not(:disabled) {
            background: #0056b3;
        }
        .input-area .clear-button {
            background: #dc3545;
        }
        .input-area .clear-button:hover:not(:disabled) {
            background: #c82333;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/frontend/main.tsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;state&quot;,&quot;version&quot;:&quot;2.0.21&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;vite&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest run&quot;,&quot;build&quot;:&quot;vite build &amp;&amp; vite build --mode server&quot;,&quot;start&quot;:&quot;srvx --static=public/ dist/server.js&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^22.13.9&quot;,&quot;@types/react&quot;:&quot;^18.2.0&quot;,&quot;@types/react-dom&quot;:&quot;^18.2.0&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.2.0&quot;,&quot;tsx&quot;:&quot;^3.12.7&quot;,&quot;typescript&quot;:&quot;^5.5.2&quot;,&quot;vite&quot;:&quot;^5.0.0&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;,&quot;vite-plugin-srvx&quot;:&quot;^1.0.0&quot;},&quot;dependencies&quot;:{&quot;@hono/node-server&quot;:&quot;^1.19.7&quot;,&quot;@hono/node-ws&quot;:&quot;^1.3.0&quot;,&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@b7a6ac3488d8d64f2cddf185312f05cf519f9413&quot;,&quot;hono&quot;:&quot;^4.0.0&quot;,&quot;react&quot;:&quot;^18.2.0&quot;,&quot;react-dom&quot;:&quot;^18.2.0&quot;,&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/rivetkit@b7a6ac3488d8d64f2cddf185312f05cf519f9413&quot;,&quot;srvx&quot;:&quot;^0.10.0&quot;},&quot;template&quot;:{&quot;technologies&quot;:[&quot;typescript&quot;],&quot;tags&quot;:[],&quot;priority&quot;:1000,&quot;frontendPort&quot;:5173},&quot;license&quot;:&quot;MIT&quot;}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
	&quot;compilerOptions&quot;: {
		/* Visit https://aka.ms/tsconfig.json to read more about this file */

		/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
		&quot;target&quot;: &quot;esnext&quot;,
		/* Specify a set of bundled library declaration files that describe the target runtime environment. */
		&quot;lib&quot;: [&quot;esnext&quot;, &quot;dom&quot;],
		/* Specify what JSX code is generated. */
		&quot;jsx&quot;: &quot;react-jsx&quot;,

		/* Specify what module code is generated. */
		&quot;module&quot;: &quot;esnext&quot;,
		/* Specify how TypeScript looks up a file from a given module specifier. */
		&quot;moduleResolution&quot;: &quot;bundler&quot;,
		/* Specify type package names to be included without being referenced in a source file. */
		&quot;types&quot;: [&quot;node&quot;, &quot;vite/client&quot;],
		/* Enable importing .json files */
		&quot;resolveJsonModule&quot;: true,

		/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
		&quot;allowJs&quot;: true,
		/* Enable error reporting in type-checked JavaScript files. */
		&quot;checkJs&quot;: false,

		/* Disable emitting files from a compilation. */
		&quot;noEmit&quot;: true,

		/* Ensure that each file can be safely transpiled without relying on other imports. */
		&quot;isolatedModules&quot;: true,
		/* Allow &#39;import x from y&#39; when a module doesn&#39;t have a default export. */
		&quot;allowSyntheticDefaultImports&quot;: true,
		/* Ensure that casing is correct in imports. */
		&quot;forceConsistentCasingInFileNames&quot;: true,

		/* Enable all strict type-checking options. */
		&quot;strict&quot;: true,

		/* Skip type checking all .d.ts files. */
		&quot;skipLibCheck&quot;: true,
		&quot;allowImportingTsExtensions&quot;: true,
		&quot;rewriteRelativeImportExtensions&quot;: true
	},
	&quot;include&quot;: [&quot;src/**/*&quot;, &quot;frontend/**/*&quot;, &quot;tests/**/*&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
	&quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
	&quot;extends&quot;: [&quot;//&quot;],
	&quot;tasks&quot;: {
		&quot;build&quot;: {
			&quot;dependsOn&quot;: [&quot;@rivetkit/react#build&quot;, &quot;rivetkit#build&quot;]
		}
	}
}
">
<input type="hidden" name="project[files][vercel.json]" value="{
	&quot;framework&quot;: &quot;hono&quot;
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;
import srvx from &quot;vite-plugin-srvx&quot;;

export default defineConfig({
	plugins: [react(), ...srvx({ entry: &quot;src/server.ts&quot; })],
});
">
<input type="hidden" name="project[files][vitest.config.ts]" value="import { defineConfig } from &quot;vitest/config&quot;;

export default defineConfig({
	server: {
		port: 5173,
	},
	test: {
		include: [&quot;tests/**/*.test.ts&quot;],
	},
});
">
<input type="hidden" name="project[files][frontend/App.tsx]" value="import { createRivetKit } from &quot;@rivetkit/react&quot;;
import { useEffect, useRef, useState } from &quot;react&quot;;
import type { Message, registry } from &quot;../src/actors.ts&quot;;

const { useActor } = createRivetKit&lt;typeof registry&gt;(`${window.location.origin}/api/rivet`);

export function App() {
	const [username, setUsername] = useState(&quot;User&quot;);
	const [messageText, setMessageText] = useState(&quot;&quot;);
	const [messages, setMessages] = useState&lt;Message[]&gt;([]);
	const messagesEndRef = useRef&lt;HTMLDivElement&gt;(null);

	const chatRoom = useActor({
		name: &quot;chatRoom&quot;,
		key: [&quot;lobby&quot;],
	});

	// Load initial messages when connected
	useEffect(() =&gt; {
		if (chatRoom.connection) {
			chatRoom.connection.getMessages().then(setMessages);
		}
	}, [chatRoom.connection]);

	// Listen for new messages
	chatRoom.useEvent(&quot;newMessage&quot;, (message: Message) =&gt; {
		setMessages((prev) =&gt; [...prev, message]);
	});

	// Listen for messages cleared event
	chatRoom.useEvent(&quot;messagesCleared&quot;, () =&gt; {
		setMessages([]);
	});

	// Auto-scroll to bottom when new messages arrive
	useEffect(() =&gt; {
		messagesEndRef.current?.scrollIntoView({ behavior: &quot;smooth&quot; });
	}, [messages]);

	const sendMessage = async () =&gt; {
		if (chatRoom.connection &amp;&amp; messageText.trim()) {
			await chatRoom.connection.sendMessage(username, messageText);
			setMessageText(&quot;&quot;);
		}
	};

	const clearMessages = async () =&gt; {
		if (chatRoom.connection) {
			await chatRoom.connection.clearMessages();
		}
	};

	const handleKeyPress = (e: React.KeyboardEvent) =&gt; {
		if (e.key === &quot;Enter&quot;) {
			sendMessage();
		}
	};

	return (
		&lt;div className=&quot;chat-container&quot;&gt;
			&lt;div className=&quot;header&quot;&gt;
				&lt;h1&gt;Quickstart: State&lt;/h1&gt;
				&lt;p&gt;A simple chat room demonstrating persistent state&lt;/p&gt;
			&lt;/div&gt;

			&lt;div className=&quot;user-input&quot;&gt;
				&lt;label&gt;Username:&lt;/label&gt;
				&lt;input
					type=&quot;text&quot;
					value={username}
					onChange={(e) =&gt; setUsername(e.target.value)}
					placeholder=&quot;Enter your username&quot;
				/&gt;
			&lt;/div&gt;

			&lt;div className=&quot;messages&quot;&gt;
				{messages.length === 0 ? (
					&lt;div className=&quot;empty-message&quot;&gt;
						No messages yet. Start the conversation!
					&lt;/div&gt;
				) : (
					messages.map((msg) =&gt; (
						&lt;div key={msg.id} className=&quot;message&quot;&gt;
							&lt;div className=&quot;message-header&quot;&gt;
								&lt;span className=&quot;message-sender&quot;&gt;{msg.sender}&lt;/span&gt;
								&lt;span className=&quot;message-timestamp&quot;&gt;
									{new Date(msg.timestamp).toLocaleTimeString()}
								&lt;/span&gt;
							&lt;/div&gt;
							&lt;div className=&quot;message-text&quot;&gt;{msg.text}&lt;/div&gt;
						&lt;/div&gt;
					))
				)}
				&lt;div ref={messagesEndRef} /&gt;
			&lt;/div&gt;

			&lt;div className=&quot;input-area&quot;&gt;
				&lt;input
					type=&quot;text&quot;
					value={messageText}
					onChange={(e) =&gt; setMessageText(e.target.value)}
					onKeyPress={handleKeyPress}
					placeholder=&quot;Type a message...&quot;
					disabled={!chatRoom.connection}
				/&gt;
				&lt;button
					onClick={sendMessage}
					disabled={!chatRoom.connection || !messageText.trim()}
				&gt;
					Send
				&lt;/button&gt;
				&lt;button
					onClick={clearMessages}
					disabled={!chatRoom.connection}
					className=&quot;clear-button&quot;
				&gt;
					Clear
				&lt;/button&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
}
">
<input type="hidden" name="project[files][frontend/main.tsx]" value="import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import { App } from &quot;./App&quot;;

const root = document.getElementById(&quot;root&quot;);
if (!root) throw new Error(&quot;Root element not found&quot;);

createRoot(root).render(
	&lt;StrictMode&gt;
		&lt;App /&gt;
	&lt;/StrictMode&gt;
);
">
<input type="hidden" name="project[files][tests/chat.test.ts]" value="import { setupTest } from &quot;rivetkit/test&quot;;
import { describe, expect, test } from &quot;vitest&quot;;
import { registry } from &quot;../src/actors.ts&quot;;

describe(&quot;chat room state&quot;, () =&gt; {
	test(&quot;send and receive messages&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);

		// Create two clients connected to the same room
		const client1 = client.chatRoom.getOrCreate([&quot;room1&quot;]);
		const client2 = client.chatRoom.getOrCreate([&quot;room1&quot;]);

		// Client 1 sends a message
		const sentMessage = await client1.sendMessage(&quot;Alice&quot;, &quot;Hello!&quot;);

		// Verify message structure
		expect(sentMessage).toMatchObject({
			id: expect.any(String),
			sender: &quot;Alice&quot;,
			text: &quot;Hello!&quot;,
			timestamp: expect.any(Number),
		});

		// Verify getMessages includes the new message
		const messages = await client2.getMessages();
		expect(messages).toHaveLength(1);
		expect(messages[0]).toEqual(sentMessage);
	});

	test(&quot;message persistence&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);

		// Send multiple messages
		const room1 = client.chatRoom.getOrCreate([&quot;persistent-room&quot;]);
		await room1.sendMessage(&quot;Alice&quot;, &quot;Message 1&quot;);
		await room1.sendMessage(&quot;Bob&quot;, &quot;Message 2&quot;);
		await room1.sendMessage(&quot;Charlie&quot;, &quot;Message 3&quot;);

		// Get messages from a different client instance for the same room
		const room2 = client.chatRoom.getOrCreate([&quot;persistent-room&quot;]);
		const messages = await room2.getMessages();

		// Verify all previously sent messages are still there
		expect(messages).toHaveLength(3);
		expect(messages[0].sender).toBe(&quot;Alice&quot;);
		expect(messages[0].text).toBe(&quot;Message 1&quot;);
		expect(messages[1].sender).toBe(&quot;Bob&quot;);
		expect(messages[1].text).toBe(&quot;Message 2&quot;);
		expect(messages[2].sender).toBe(&quot;Charlie&quot;);
		expect(messages[2].text).toBe(&quot;Message 3&quot;);
	});

	test(&quot;message ordering&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);
		const room = client.chatRoom.getOrCreate([&quot;ordering-room&quot;]);

		// Send 5 messages in sequence
		const msg1 = await room.sendMessage(&quot;User&quot;, &quot;Message 1&quot;);
		const msg2 = await room.sendMessage(&quot;User&quot;, &quot;Message 2&quot;);
		const msg3 = await room.sendMessage(&quot;User&quot;, &quot;Message 3&quot;);
		const msg4 = await room.sendMessage(&quot;User&quot;, &quot;Message 4&quot;);
		const msg5 = await room.sendMessage(&quot;User&quot;, &quot;Message 5&quot;);

		// Verify messages are returned in the correct order
		const messages = await room.getMessages();
		expect(messages).toHaveLength(5);
		expect(messages[0].text).toBe(&quot;Message 1&quot;);
		expect(messages[1].text).toBe(&quot;Message 2&quot;);
		expect(messages[2].text).toBe(&quot;Message 3&quot;);
		expect(messages[3].text).toBe(&quot;Message 4&quot;);
		expect(messages[4].text).toBe(&quot;Message 5&quot;);

		// Verify timestamps are sequential
		expect(msg2.timestamp).toBeGreaterThanOrEqual(msg1.timestamp);
		expect(msg3.timestamp).toBeGreaterThanOrEqual(msg2.timestamp);
		expect(msg4.timestamp).toBeGreaterThanOrEqual(msg3.timestamp);
		expect(msg5.timestamp).toBeGreaterThanOrEqual(msg4.timestamp);
	});

	test(&quot;clear messages&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);

		// Send several messages
		const room = client.chatRoom.getOrCreate([&quot;clear-room&quot;]);
		await room.sendMessage(&quot;Alice&quot;, &quot;Message 1&quot;);
		await room.sendMessage(&quot;Bob&quot;, &quot;Message 2&quot;);
		await room.sendMessage(&quot;Charlie&quot;, &quot;Message 3&quot;);

		// Verify messages exist
		let messages = await room.getMessages();
		expect(messages).toHaveLength(3);

		// Call clearMessages
		const result = await room.clearMessages();
		expect(result.success).toBe(true);

		// Verify getMessages returns empty array
		messages = await room.getMessages();
		expect(messages).toHaveLength(0);
	});

	test(&quot;multiple rooms&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);

		// Create clients for &quot;room1&quot; and &quot;room2&quot;
		const room1 = client.chatRoom.getOrCreate([&quot;room1&quot;]);
		const room2 = client.chatRoom.getOrCreate([&quot;room2&quot;]);

		// Send messages to room1
		await room1.sendMessage(&quot;Alice&quot;, &quot;Room 1 message 1&quot;);
		await room1.sendMessage(&quot;Bob&quot;, &quot;Room 1 message 2&quot;);

		// Verify room2 has no messages
		const room2Messages = await room2.getMessages();
		expect(room2Messages).toHaveLength(0);

		// Verify room1 has its messages
		const room1Messages = await room1.getMessages();
		expect(room1Messages).toHaveLength(2);

		// Send message to room2
		await room2.sendMessage(&quot;Charlie&quot;, &quot;Room 2 message&quot;);

		// Verify messages are isolated per room
		const room1Final = await room1.getMessages();
		const room2Final = await room2.getMessages();
		expect(room1Final).toHaveLength(2);
		expect(room2Final).toHaveLength(1);
		expect(room2Final[0].text).toBe(&quot;Room 2 message&quot;);
	});
});
">
<input type="hidden" name="project[files][src/actors.ts]" value="import { actor, setup } from &quot;rivetkit&quot;;

export type Message = {
	id: string;
	sender: string;
	text: string;
	timestamp: number;
};

export const chatRoom = actor({
	// Persistent state that survives restarts: https://rivet.dev/docs/actors/state
	state: {
		messages: [] as Message[],
	},

	actions: {
		// Callable functions from clients: https://rivet.dev/docs/actors/actions
		sendMessage: (c, sender: string, text: string) =&gt; {
			const message: Message = {
				id: crypto.randomUUID(),
				sender,
				text,
				timestamp: Date.now(),
			};
			// State changes are automatically persisted
			c.state.messages.push(message);
			// Send events to all connected clients: https://rivet.dev/docs/actors/events
			c.broadcast(&quot;newMessage&quot;, message);
			return message;
		},

		// Returns all messages for initial state loading
		getMessages: (c) =&gt; c.state.messages,

		// Clears all messages and notifies clients
		clearMessages: (c) =&gt; {
			c.state.messages = [];
			c.broadcast(&quot;messagesCleared&quot;);
			return { success: true };
		},
	},
});

// Register actors for use: https://rivet.dev/docs/setup
export const registry = setup({
	use: { chatRoom },
});
">
<input type="hidden" name="project[files][src/server.ts]" value="import { Hono } from &quot;hono&quot;;
import { registry } from &quot;./actors.ts&quot;;

const app = new Hono();
app.all(&quot;/api/rivet/*&quot;, (c) =&gt; registry.handler(c.req.raw));
export default app;
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="state">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>