| name | atomico-jsx-patterns |
| description | Best practices and patterns for using JSX in Atomico web components. Triggers when the user needs guidance on JSX composition, constructor vs tag-name usage, type inference patterns, slot management, style handling, or virtualDOM rendering behavior. Covers the critical recommendation to prefer constructor instances over tag-name strings for type safety.
|
| license | MIT |
| compatibility | Atomico >=1.79, TypeScript >=5.0 |
| metadata | {"category":"patterns","priority":"high"} |
JSX Patterns & Best Practices
🏆 Golden Rule: Constructor Instances over Tag Strings
Always prefer constructor references (<MyComponent />) over tag-name
strings (<my-component />) in JSX. This is the single most important pattern
in Atomico for leveraging the type system.
Why?
| Feature | <MyComponent /> | <my-component /> |
|---|
| Prop type checking | ✅ Full | ❌ None |
| Event type inference | ✅ detail typed | ❌ Untyped |
| IDE autocompletion | ✅ All props/events | ❌ None |
| Dependency tracking | ✅ Import graph | ❌ Hidden |
| Refactoring safety | ✅ Rename cascades | ❌ Manual updates |
Example
import { c, event } from "atomico";
const TodoTask = c(
({ checked, message, changeTask }) => (
<host shadowDom>
<label onchange={(e) => changeTask((e.target as HTMLInputElement).checked)}>
<input type="checkbox" checked={checked} />
<span>{message}</span>
</label>
</host>
),
{
props: {
changeTask: event<boolean>({ bubbles: true, composed: true }),
message: String,
checked: { type: Boolean, reflect: true }
}
}
);
const TodoApp = c(() => (
<host shadowDom>
{/* ✅ Full type inference — all props validated */}
<TodoTask
message="Buy groceries"
checked={false}
onchangeTask={({ detail }) => {
console.log(detail); // boolean — inferred!
}}
/>
{/* ❌ No type inference, no validation */}
<todo-task message="Buy groceries"></todo-task>
</host>
));
The <host> Element
<host> is a special JSX element that represents the custom element itself.
It MUST be the root of every Atomico component's return value.
Props on <host>
<host
shadowDom
shadowDom={{ slotAssignment: "manual", delegatesFocus: true }}
class="my-class"
style={{ color: "red" }}
onclick={(e) => console.log(e)}
>
{children}
</host>
Slot Patterns
Default Slot
<host shadowDom>
<header>Header content</header>
<slot /> {}
<footer>Footer content</footer>
</host>
Named Slots
<host shadowDom>
<slot name="header" />
<main>
<slot /> {/* Default slot */}
</main>
<slot name="footer" />
</host>
Manual Slot Assignment
<host shadowDom={{ slotAssignment: "manual" }}>
{nodes.map((node) => (
<li>
<slot assignNode={node} />
</li>
))}
</host>
Style Patterns
Inline Styles (Object)
<div style={{
color: "red",
fontSize: "1rem",
"--custom-var": "blue"
}} />
Inline Styles (String)
<div style="color: red; font-size: 1rem;" />
CSS Variable Theming
const MyComponent = c(
() => (
<host shadowDom>
<div class="card">Themed card</div>
</host>
),
{
styles: css`
:host {
--bg: #f0f0f9;
--border: #dcdce1;
}
:host([active]) {
--bg: #a3ebd4;
--border: #6ee2c9;
}
.card {
background: var(--bg);
border: 1px solid var(--border);
padding: 1rem;
border-radius: 0.5rem;
}
`
}
);
Conditional Rendering
<host>
{isLoading ? (
<p>Loading...</p>
) : error ? (
<p>Error: {error.message}</p>
) : (
<div>{data}</div>
)}
</host>
List Rendering with Keys
<host>
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</host>
Note: Keys enable efficient list reconciliation. Without keys, Atomico
uses positional matching. With keys, nodes can be reordered via moveBefore
(if supported) preserving focus, scroll, and animation state.
Event Handler Patterns
Inline Handlers Over Extracted Functions
const handleInput = (e: InputEvent) => {
const val = (e.currentTarget as HTMLInputElement).value;
inputChange(val);
};
Note: Only extract event handlers to standalone functions if the exact same handler is shared across multiple different elements. Inline handlers leverage Atomico's deep JSX type integration, avoiding verbose overhead.
Direct Handler
<button onclick={(e) => handleClick(e)}>Click</button>
With Options
<button onclick={Object.assign(
(e) => handleClick(e),
{ capture: true, once: true, passive: true }
)}>
Click (with options)
</button>
Event Delegation via currentTarget
<form
oninput={({ currentTarget }) => {
const data = new FormData(currentTarget as HTMLFormElement);
console.log(Object.fromEntries(data));
}}
>
<input name="field1" />
<input name="field2" />
</form>
Ref Patterns
Element Reference
const ref = useRef<HTMLInputElement>();
<input ref={ref} />
Callback Ref
<input ref={(node) => console.log("Element mounted:", node)} />
Component Reference
const childRef = useRef<typeof MyChild>();
<MyChild ref={childRef} />
Static Node Optimization
Prevent re-rendering of a subtree with staticNode:
<div staticNode>
{}
<heavy-visualization data={initialData} />
</div>
Fragment Pattern
import { Fragment } from "atomico";
<host>
<Fragment>
<p>First</p>
<p>Second</p>
</Fragment>
</host>
Or with shorthand (if configured):
<host>
<>
<p>First</p>
<p>Second</p>
</>
</host>
SVG Support
Atomico auto-detects SVG context and creates elements in the SVG namespace:
<host shadowDom>
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="red" />
<foreignObject x="10" y="10" width="80" height="80">
{/* HTML content inside SVG */}
<div>HTML in SVG!</div>
</foreignObject>
</svg>
</host>
Anti-Patterns
| ❌ Don't | ✅ Do Instead |
|---|
<my-child /> in JSX | <MyChild /> |
Return <div> as root | Return <host> |
document.createElement | Use JSX |
| Mutate props directly | Use useProp or useState |
| Inline complex logic in JSX | Extract to hooks or utils |
Skip key in dynamic lists | Always provide key |