Guide
Component System
TSone's component system lets you split UI into independent, reusable class components.
Component Definition
Define a component by extending Component<Props, State> and implementing initState(), initStyles(), and protected render(): VNode.
Basic Component Structure
import { Component, VNode } from '@geektech/tsone';
interface MessageState {
message: string;
}
class MyComponent extends Component<object, MessageState> {
protected initState(): MessageState {
return { message: 'Hello, TSone!' };
}
protected initStyles(): void {
this.styleManager.addStyle('.my-component', {
selector: '.my-component',
properties: {
color: '#333',
fontSize: '16px',
padding: '10px',
},
});
}
protected render(): VNode {
return {
tag: 'div',
props: { className: 'my-component' },
children: [''],
};
}
}Component Lifecycle
Mounting Phase
beforeMount(): called before the component is mounted in the DOMonMounted(): called after the component is mounted in the DOM
Update Phase
beforeUpdate(): called before component state updatesonUpdated(): called after component state updates
Unmounting Phase
beforeUnmount(): called before the component is removed from the DOMonUnmounted(): called after the component is removed from the DOM
Error Handling (Error Boundaries)
Use onErrorCaptured(error, instance) to catch errors thrown while a descendant component renders or updates. Return false to mark the error as handled and stop it from propagating; return true or undefined to let it bubble up to an outer boundary.
class ErrorBoundary extends Component<object, { failed: boolean }> {
protected initState() {
return { failed: false };
}
protected initStyles(): void {}
protected onErrorCaptured(): boolean {
this.state.failed = true;
return false;
}
protected render(): VNode {
if (this.state.failed) {
return { tag: 'p', children: ['A child failed; the boundary took over.'] };
}
return { tag: 'div', children: this.props.children ?? [] };
}
}Lifecycle Example
class LifecycleComponent extends Component<object, { count: number }> {
protected initState() {
return { count: 0 };
}
protected initStyles(): void {}
protected beforeMount() {
console.log('Component will mount');
}
protected onMounted() {
console.log('Component mounted');
}
protected beforeUpdate() {
console.log('Component will update');
}
protected onUpdated() {
console.log('Component updated');
}
protected render(): VNode {
return {
tag: 'button',
children: ['Count: '],
listeners: {
click: () => this.state.count++,
},
};
}
}Component Props
Define a Props Interface
interface ButtonProps {
text: string;
disabled?: boolean;
size?: 'small' | 'medium' | 'large';
}
class Button extends Component<ButtonProps, object> {
protected initState() {
return {};
}
protected initStyles(): void {}
protected render(): VNode {
const { text, disabled = false, size = 'medium' } = this.props;
return {
tag: 'button',
props: {
className: `btn btn-${size}` ,
disabled,
},
children: [text],
};
}
}Use Component Props
class ParentComponent extends Component<object, object> {
protected initState() {
return {};
}
protected initStyles(): void {}
protected render(): VNode {
return {
tag: 'div',
children: [
{
component: Button,
props: { text: 'Click Me', size: 'large' },
},
],
};
}
}Component Events
Emit an Event
class CounterComponent extends Component<object, { count: number }> {
protected initState() {
return { count: 0 };
}
protected initStyles(): void {}
protected handleIncrement() {
this.state.count++;
this.emit('increment', this.state.count);
}
protected render(): VNode {
return {
tag: 'button',
listeners: { click: () => this.handleIncrement() },
children: ['+'],
};
}
}Listen for an Event
class ParentCounter extends Component<object, object> {
protected initState() {
return {};
}
protected initStyles(): void {}
protected handleCounterChange(value: number) {
console.log('Counter changed:', value);
}
protected render(): VNode {
return {
tag: 'div',
children: [
{
component: CounterComponent,
listeners: {
increment: (value: number) => this.handleCounterChange(value),
},
},
],
};
}
}Component Nesting
class App extends Component<object, object> {
protected initState() {
return {};
}
protected initStyles(): void {}
protected render(): VNode {
return {
tag: 'div',
props: { className: 'app' },
children: [
{ tag: 'header', children: [{ component: NavigationComponent }] },
{ tag: 'main', children: [{ component: HomeComponent }] },
],
};
}
}Component State Management
Initialize State
protected initState() {
return {
count: 0,
user: { name: 'John', age: 30 },
items: ['Item 1', 'Item 2', 'Item 3'],
};
}Update State
this.state.count = 1;
this.state.user.name = 'Jane';
this.state.items.push('Item 4');State changes are merged through the scheduler and flushed on a microtask: a burst of synchronous mutations triggers a single re-render. Use flushSync() to read the latest DOM synchronously, or await nextTick() in async code.
Component Style Management
Each component instance owns its own styleManager.
Add Styles
protected initStyles() {
this.styleManager.addStyle('.button', {
selector: '.button',
properties: {
padding: '8px 16px',
backgroundColor: '#007bff',
color: '#fff',
},
});
this.styleManager.addStyle('.button:hover', {
selector: '.button:hover',
properties: { backgroundColor: '#0069d9' },
});
}Remove Styles
this.styleManager.removeStyle('.button');
this.styleManager.clearStyles();Transitions and Component Caching
Transition plays an enter/leave transition for a single element while show toggles: on enter it applies {name}-enter-from / {name}-enter-to (each with an -active class), on leave {name}-leave-from / {name}-leave-to, and the element is removed duration ms after leave starts. Default name is transition and duration is 300.
import { Transition } from '@geektech/tsone';
{
component: Transition,
props: { show: this.state.open, name: 'fade', duration: 300 },
children: [{ tag: 'div', children: ['content'] }],
}KeepAlive keeps every keyed child component mounted: when activeKey changes, inactive children stay hidden via display:none with state and DOM preserved, which fits tab switching and other state-preserving scenarios.
import { KeepAlive } from '@geektech/tsone';
{
component: KeepAlive,
props: { activeKey: this.state.activeTab },
children: [
{ key: 'tab-a', component: PanelA },
{ key: 'tab-b', component: PanelB },
],
}Component Context
Components can read global configuration through application context and access the router through the router getter.
Get Context
const context = this.getContext();
const router = this.router;
router?.push('/about');
console.log(context?.config);Best Practices
Component Design Principles
- Single responsibility: each component should handle one concern
- Reusability: design general-purpose, reusable components
- Maintainability: keep component code concise and clear
- Performance: avoid unnecessary rendering and computation
Component Naming Conventions
- Use PascalCase for component class names
- Follow the repository's existing naming style for component files
- Use kebab-case for component style class names
Performance Tips
- Avoid creating unnecessary objects in render
- Keep only properties requiring reactive updates in state
- Use computed for complex derived values
- Keep component nesting shallow
Summary
With class components, lifecycle hooks, events, slots, and local style management, TSone maintains a clear object-oriented component model.