This page contrasts a single component in React and Atomica and explains what changes in the mental model.
Goal: a button that increments a counter and displays the current value.
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
);
}
import { h, signal } from 'atomica';
export const Counter = () => {
const count = signal(0);
return h(
'button',
{ onClick: () => count.set((c) => c + 1) },
() => `Count: ${count.get()}`
);
};
() => ...).onClick).If you expect the component to run again, you are thinking in React. In Atomica, the component runs once to build DOM + bindings; the bindings re-run.
Goal: a small profile editor that saves a name and shows status.
import { useState } from 'react';
export function ProfileEditor() {
const [name, setName] = useState('');
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
async function save() {
setStatus('saving');
try {
await fetch('/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
setStatus('saved');
} catch {
setStatus('error');
}
}
return (
<section>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={save}>Save</button>
<span>{status}</span>
</section>
);
}
import { bindInput, h, resource, signal } from 'atomica';
export function ProfileEditor() {
const name = signal('');
async function save() {
const response = await fetch('/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.get() })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
try {
return await response.json();
} catch {
return true;
}
}
const saveResource = resource(save, { auto: false });
return h(
'section',
null,
h('input', bindInput(name)),
h('button', { onClick: () => saveResource.refresh() }, 'Save'),
h('span', null, () => {
if (saveResource.loading()) return 'saving';
if (saveResource.error()) return 'error';
return saveResource.data() ? 'saved' : 'idle';
})
);
}
import { bindInput, resource, signal } from 'atomica';
export function ProfileEditor() {
const name = signal('');
async function save() {
const response = await fetch('/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.get() })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
try {
return await response.json();
} catch {
return true;
}
}
const saveResource = resource(save, { auto: false });
return (
<section>
<input {...bindInput(name)} />
<button onClick={() => saveResource.refresh()}>Save</button>
<span>
{() => {
if (saveResource.loading()) return 'saving';
if (saveResource.error()) return 'error';
return saveResource.data() ? 'saved' : 'idle';
}}
</span>
</section>
);
}