One setup, changing values
Press Increase (+) in the starter: the large number and the sentence both change from 0 to 1. They read the same signal, a value held in this preview’s memory. createSignal gives you a getter to read it and a setter to change it.
Forma calls Counter to set up this mounted component. Both displays use {() => count()}: that function keeps the text connected to later changes. Replacing it with {count()} would take only the value at setup, leaving that display unchanged when you click.
Update from the current value
The button’s onClick callback calls the setter. setCount(n => n + 2) means “take the current number and add two”; the display then follows. Keep Decrease subtracting one so you can verify the two rules separately.
Reset count is an application button: it sets this signal to 0. After your exercise, running the code again starts a fresh counter at 10. Neither action saves a number to a server.
Your exercise
- Change the initial value in createSignal from 0 to 10.
- Make Increase count add 2 while Decrease count still subtracts 1.
- Keep the output reactive. Leave Reset count setting the value to 0.
The starter already works. Make the requested changes in app.tsx, then choose Run again. A successful run starts a fresh preview with your new code and its initial values. A compiler error leaves the last successful preview in place; check Diagnostics before judging the result.
What Run, Reset and refresh keep
The values you change in these previews live only in browser memory. Running again or refreshing the page discards them. Your source edits are separate: the editor keeps them while this page is open, but does not autosave them.
Share link puts the current source files in the URL; Download ZIP saves those files as a local project. Neither saves the current count, checkbox states or typed notes. Refreshing a lesson link reloads the source in its current URL and waits for Run; edits not captured in that URL are lost.
On a lesson link, the playground’s Reset restores the source originally loaded by that link and clears the preview. Choose Run to start it again. This is separate from an application button such as Reset count, which changes a value inside a running preview.
Read the complete starter files
app.tsx
import { createSignal, h, mount } from '@getforma/core';
import './styles.css';
function Counter() {
// Exercise: start at 10 and make Increase add 2.
const [count, setCount] = createSignal(0);
return <main>
<p class="eyebrow">01 / Signals</p>
<h1>Small state. Direct updates.</h1>
<p>The component runs once. The current count is {() => count()}.</p>
<div class="controls">
<button onClick={() => setCount(n => n - 1)} aria-label="Decrease count">−</button>
<output aria-label="Count">{() => count()}</output>
<button onClick={() => setCount(n => n + 1)} aria-label="Increase count">+</button>
</div>
<button onClick={() => setCount(0)}>Reset count</button>
<p>Press +: both numbers update from the same signal. Run starts fresh from the code’s initial value; clicks are not saved.</p>
</main>;
}
mount(Counter, '#app');
styles.css
:root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #162225; background: #f2f5f1; }
* { box-sizing: border-box; }
body { margin: 0; padding: 32px; }
main { max-width: 540px; margin: 0 auto; }
h1 { font-size: clamp(28px, 7vw, 40px); line-height: 1.1; letter-spacing: -.05em; }
p { line-height: 1.6; color: #52615d; }
button, input { font: inherit; }
button { cursor: pointer; border: 1px solid #1a3530; border-radius: 8px; padding: 10px 15px; background: #173e34; color: #fff; }
button:hover { background: #24624f; }
button:focus-visible, input:focus-visible { outline: 3px solid #cb7bff; outline-offset: 3px; }
button:disabled { opacity: .5; cursor: default; }
input { min-width: 0; padding: 10px; border: 1px solid #a5b4aa; border-radius: 8px; background: white; }
label { display: grid; gap: 8px; }
output { display: inline-block; font-variant-numeric: tabular-nums; font-size: 42px; font-weight: 700; }
.eyebrow { text-transform: uppercase; letter-spacing: .15em; font-size: 11px; font-weight: 700; color: #326b58; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin: 24px 0; }
.card { padding: 22px; border: 1px solid #cbd8ce; border-radius: 14px; background: #fff; }
ul { list-style: none; padding: 0; display: grid; gap: 10px; }
li { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 14px; border: 1px solid #cbd8ce; border-radius: 10px; background: white; }
li > span:first-child { flex: 1; }
.inventory-row { display: grid; grid-template-columns: 1fr auto; }
.inventory-row label { grid-column: 1 / -1; }
@media (max-width: 420px) { body { padding: 20px; } }
Verify the behavior
| Action | Expected result |
|---|---|
| Run the solution | The large number and the sentence both start at 10. |
| Increase twice, then decrease once | The count is 13: 10 + 2 + 2 − 1. |
| Reset count | The count returns to 0. |
These checks exercise the visible result. Running without a compiler error is only the first step.
A working solution
The initial value is 10; two increases add four and one decrease removes one, giving 13. Both text bindings read the same signal, so they agree without running Counter again or rebuilding its output node.
Read the complete solution files
app.tsx
import { createSignal, h, mount } from '@getforma/core';
import './styles.css';
function Counter() {
// Exercise: start at 10 and make Increase add 2.
const [count, setCount] = createSignal(10);
return <main>
<p class="eyebrow">01 / Signals</p>
<h1>Small state. Direct updates.</h1>
<p>The component runs once. The current count is {() => count()}.</p>
<div class="controls">
<button onClick={() => setCount(n => n - 1)} aria-label="Decrease count">−</button>
<output aria-label="Count">{() => count()}</output>
<button onClick={() => setCount(n => n + 2)} aria-label="Increase count">+</button>
</div>
<button onClick={() => setCount(0)}>Reset count</button>
<p>Press +: both numbers update from the same signal. Run starts fresh from the code’s initial value; clicks are not saved.</p>
</main>;
}
mount(Counter, '#app');
styles.css
:root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #162225; background: #f2f5f1; }
* { box-sizing: border-box; }
body { margin: 0; padding: 32px; }
main { max-width: 540px; margin: 0 auto; }
h1 { font-size: clamp(28px, 7vw, 40px); line-height: 1.1; letter-spacing: -.05em; }
p { line-height: 1.6; color: #52615d; }
button, input { font: inherit; }
button { cursor: pointer; border: 1px solid #1a3530; border-radius: 8px; padding: 10px 15px; background: #173e34; color: #fff; }
button:hover { background: #24624f; }
button:focus-visible, input:focus-visible { outline: 3px solid #cb7bff; outline-offset: 3px; }
button:disabled { opacity: .5; cursor: default; }
input { min-width: 0; padding: 10px; border: 1px solid #a5b4aa; border-radius: 8px; background: white; }
label { display: grid; gap: 8px; }
output { display: inline-block; font-variant-numeric: tabular-nums; font-size: 42px; font-weight: 700; }
.eyebrow { text-transform: uppercase; letter-spacing: .15em; font-size: 11px; font-weight: 700; color: #326b58; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin: 24px 0; }
.card { padding: 22px; border: 1px solid #cbd8ce; border-radius: 14px; background: #fff; }
ul { list-style: none; padding: 0; display: grid; gap: 10px; }
li { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 14px; border: 1px solid #cbd8ce; border-radius: 10px; background: white; }
li > span:first-child { flex: 1; }
.inventory-row { display: grid; grid-template-columns: 1fr auto; }
.inventory-row label { grid-column: 1 / -1; }
@media (max-width: 420px) { body { padding: 20px; } }
Need the API details? Read the Forma 3.0 core reference. This playground compiles TSX and runs browser code. It does not run a Rust server or demonstrate server rendering; the later native lessons cover that path.