atomica

React vs Atomica Component (Compare & Contrast)

This page contrasts a single component in React and Atomica and explains what changes in the mental model.

The same UI in both

Goal: a button that increments a counter and displays the current value.

React

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Count: {count}
    </button>
  );
}

Atomica

import { h, signal } from 'atomica';

export const Counter = () => {
  const count = signal(0);

  return h(
    'button',
    { onClick: () => count.set((c) => c + 1) },
    () => `Count: ${count.get()}`
  );
};

What is different

What is similar

Key mental shift

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.

A more complex example (form + async)

Goal: a small profile editor that saves a name and shows status.

React

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>
  );
}

Atomica

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';
    })
  );
}

Atomica (JSX)

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>
  );
}

Key differences in the complex case