What are Functional Components?
Functional components are JavaScript functions that take properties (props) and return virtual nodes. They’re the building blocks for creating reusable, composable UI in O!.
import { h } from '@zserge/o';
const Greeting = (props) => {
return h('div', {}, `Hello, ${props.name}!`);
};
Component Signature
Functional components in O! receive three arguments:
const MyComponent = (props, children, forceUpdate) => {
// props: object with component properties
// children: array of child VNodes passed to the component
// forceUpdate: callback to trigger re-render
return h('div', {}, 'Component UI');
};
Props
Props are properties passed to your component from its parent:
const UserCard = ({ name, age, role = 'User' }) => {
return h('div', { className: 'card' },
h('h2', {}, name),
h('p', {}, `Age: ${age}`),
h('p', {}, `Role: ${role}`)
);
};
// Usage
render(
h(UserCard, { name: 'Alice', age: 30 }),
document.body
);
Use destructuring with default values for cleaner prop handling!
Children
The second argument contains child VNodes passed to your component:
const Card = (props, children) => {
return h('div', { className: 'card' },
h('div', { className: 'card-body' }, ...children)
);
};
// Usage with children
render(
h(Card, {},
h('h1', {}, 'Title'),
h('p', {}, 'Content')
),
document.body
);
The forceUpdate Callback
The third argument is a forceUpdate function that triggers a re-render of your component:
const ClickCounter = (props, children, forceUpdate) => {
let clicks = 0;
return h('button', {
onclick: () => {
clicks++;
forceUpdate(); // Re-render to show new count
}
}, `Clicks: ${clicks}`);
};
In practice, you’ll rarely use forceUpdate directly. Instead, use the useState or useReducer hooks which handle updates automatically.
Real Example: Counter Component
Here’s the complete counter example from counter.html:
import { h, x, render, useState } from '@zserge/o';
const Counter = ({ name = 'Counter', initialValue = 0 }) => {
const [value, setValue] = useState(initialValue);
return x`
<div className="counter">
<h1>${name}</h1>
<div>${value}</div>
<div className="row">
<button onclick=${() => setValue(value + 1)}>+</button>
<button onclick=${() => setValue(value - 1)}>-</button>
</div>
</div>
`;
};
// Render with initial props
render(h(Counter, { initialValue: 10 }), document.body);
Breaking Down the Counter
- Props with defaults:
{ name = 'Counter', initialValue = 0 } provides fallback values
- State hook:
useState(initialValue) creates reactive state that persists across renders
- Event handlers:
onclick=${() => setValue(value + 1)} updates state and triggers re-render
- Template syntax:
x` ` provides JSX-like HTML syntax (covered in Template Syntax)
Component Lifecycle
Here’s what happens when O! renders a functional component:
// From o.mjs:264-271
while (typeof v.e === 'function') {
const k = (v.p && v.p.k) || '' + v.e + (ids[v.e] = (ids[v.e] || 1) + 1);
hooks = hs[k] || [];
index = 0;
v = v.e(v.p, v.c, forceUpdate); // Call the component function
dom.h[k] = hooks; // Store hooks for next render
}
- Execute: O! calls your component function with props, children, and forceUpdate
- Hooks: Any hooks you call (useState, useEffect) are tracked and stored
- Return: Your component returns a VNode tree
- Render: O! recursively processes the returned VNodes and updates the DOM
Component Composition
Components can render other components, creating a tree:
const Button = ({ onClick, label }) => {
return h('button', { onclick: onClick }, label);
};
const ButtonGroup = () => {
const [count, setCount] = useState(0);
return h('div', { className: 'button-group' },
h(Button, {
onClick: () => setCount(count + 1),
label: '+'
}),
h('span', {}, count),
h(Button, {
onClick: () => setCount(count - 1),
label: '-'
})
);
};
Returning VNodes
Functional components must return a virtual node. You can create VNodes using:
h() function: return h('div', {}, 'content')
- Template syntax:
return x`<div>content</div>`
- Another component:
return h(OtherComponent, { prop: 'value' })
// Using h()
const WithH = () => {
return h('div', { className: 'container' },
h('h1', {}, 'Title')
);
};
// Using template syntax (equivalent)
const WithTemplate = () => {
return x`
<div className="container">
<h1>Title</h1>
</div>
`;
};
State Management with Hooks
Functional components become powerful when combined with hooks:
useState
For simple state values:
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
return h('button', {
onclick: () => setIsOn(!isOn)
}, isOn ? 'ON' : 'OFF');
};
useReducer
For complex state logic:
const reducer = (state, action) => {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: 0 };
default: return state;
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return h('div', {},
h('div', {}, state.count),
h('button', { onclick: () => dispatch({ type: 'increment' }) }, '+'),
h('button', { onclick: () => dispatch({ type: 'decrement' }) }, '-'),
h('button', { onclick: () => dispatch({ type: 'reset' }) }, 'Reset')
);
};
useEffect
For side effects:
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []); // Empty deps = run once on mount
return h('div', {}, `Seconds: ${seconds}`);
};
Component Keys
Use the special k property to help O! track component identity across renders:
const TodoList = ({ todos }) => {
return h('ul', {},
...todos.map(todo =>
h(TodoItem, { k: todo.id, text: todo.text })
)
);
};
The k property is the only “artificial” property in O!. It helps maintain component state when the component’s position in the DOM tree changes.
Best Practices
- Keep components pure: Same props should always return the same VNode structure
- Use hooks at the top level: Don’t call hooks inside loops, conditions, or nested functions
- Destructure props: Makes it clear what props your component expects
- Provide defaults: Use default parameters for optional props
- One responsibility: Each component should do one thing well
Next Steps