---
title: Your first component
description: Build a small grocery list in four steps, from plain markup to state, a list, and an input.
sidebar: { label: First component }
---

You have a counter running. Now build a small grocery list from scratch.

Write every step in one file. In the browser-only app, that file is `src/App.tsrx`. In a starter app, use `pages/index.tsrx`. Save after each step and look at the page.

A component is a function whose body is `@{ ... }`. You write markup as a statement inside it, so you never `return` it.

## Step 1: Some markup

```tsrx src/App.tsrx
export default function App() @{
	<main>
		<h1>My groceries</h1>
		<p>Nothing here yet.</p>
	</main>
}
```

## Step 2: Add state

`state(0)` makes a value that the page follows. You change it with plain code, like `count++`.

```tsrx src/App.tsrx
import { state } from '@markless/core';

export default function App() @{
	let count = state(0);

	<main>
		<h1>My groceries</h1>
		<button onClick={() => count++}>Count {count}</button>
	</main>
}
```

Press **Count** a few times.

<StartCounterFigure />

Notice the counts: each click changes one piece of text, and `App` does not run again.

## Step 3: A list

`@for` repeats markup for each item. The `key` gives each row a stable identity. `@empty` shows when the list has no items.

```tsrx src/App.tsrx
import { state } from '@markless/core';

export default function App() @{
	let items = state([
		{ id: 1, name: 'Milk' },
		{ id: 2, name: 'Bread' },
	]);

	<main>
		<h1>My groceries</h1>
		<ul>
			@for (const item of items; key item.id) {
				<li>{item.name}</li>
			} @empty {
				<li>Nothing yet</li>
			}
		</ul>
		<button onClick={() => (items = [])}>Clear</button>
	</main>
}
```

Click **Clear**. The two rows go away and "Nothing yet" shows.

## Step 4: An input

`onInput` gets the browser's own event. Write the typed text back into state. The **Add** button pushes a new row, moves the id on, and clears the input.

```tsrx src/App.tsrx
import { state } from '@markless/core';

export default function App() @{
	let items = state([{ id: 1, name: 'Milk' }]);
	let draft = state('');
	let nextId = state(2);

	<main>
		<h1>My groceries</h1>
		<input value={draft} onInput={(event) => (draft = event.currentTarget.value)} />
		<button
			onClick={() => {
				items.push({ id: nextId, name: draft });
				nextId++;
				draft = '';
			}}
		>
			Add
		</button>
		<ul>
			@for (const item of items; key item.id) {
				<li>{item.name}</li>
			} @empty {
				<li>Nothing yet</li>
			}
		</ul>
		<p>{items.length} items</p>
	</main>
}
```

Type "Eggs" and click **Add**. A new row shows, the count says "2 items", and the input clears.

:::warning[Why did the build stop with MARKLESS_STATE_CREATION_SITE_UNSTABLE?]
Create every `state()` at the top of the component body. The compiler stops the build if you create state inside a click handler or inside an `@if` branch.
:::

:::info[No new row after a 0.4.0 build?]
With 0.4.0 from npm, step 4 fails in a production build of the browser-only app. The count goes up, but no new row shows. The dev server shows the new row. The repo at 0.5.0 fixes this.
:::

**Next:** What are all the other files in a starter app? [Project tour →](/start/project-tour)
