Skip to main content

What are Components?

Components are reusable, self-contained pieces of UI that encapsulate their own structure, logic, and state. In GlyphUI, components are the building blocks of your application. Components can:
  • Manage their own internal state
  • Accept props from parent components
  • Emit events to communicate with parents
  • Implement lifecycle methods to react to changes
  • Be composed together to build complex UIs

Component Types

GlyphUI supports two types of components:
  1. Class Components - Full-featured components with state and lifecycle methods
  2. Functional Components - Lightweight, function-based components that use hooks

Class Components

Class components extend the Component base class from packages/runtime/src/component.js. They provide full access to state management, lifecycle methods, and event handling.

Basic Structure

import { Component, h } from 'glyphui';

class MyComponent extends Component {
  constructor(props) {
    super(props, {
      initialState: { /* initial state */ }
    });
  }
  
  render(props, state, emit) {
    return h('div', {}, ['Hello from component']);
  }
}

Counter Example

From examples/hello-world/hello.js:3-42, here’s a complete class component:
import { h, Component } from 'glyphui';

class HelloWorldApp extends Component {
  constructor() {
    super({}, {
      initialState: { greeting: 'Hello, World!' }
    });
    
    this.greetings = [
      'Hello, World!',
      'Hola, Mundo!',
      'Bonjour, Monde!',
      'Ciao, Mondo!',
      'こんにちは世界!',
      '你好,世界!',
      'Привет, мир!',
    ];
  }
  
  changeGreeting() {
    const currentIndex = this.greetings.indexOf(this.state.greeting);
    const nextIndex = (currentIndex + 1) % this.greetings.length;
    this.setState({ greeting: this.greetings[nextIndex] });
  }
  
  render(props, state) {
    return h('div', {}, [
      h('div', { class: 'greeting' }, [state.greeting]),
      h('button', {
        class: 'change-btn',
        on: { click: () => this.changeGreeting() }
      }, ['Next →'])
    ]);
  }
}

// Mount the component
const app = new HelloWorldApp();
app.mount(document.querySelector('main'));

Component Constructor

The constructor accepts props and an options object:
constructor(props = {}, { initialState = {} } = {})
  • props - Properties passed from parent component
  • initialState - Initial state object for the component
Always call super(props, { initialState }) before accessing this in the constructor.

State Management

Components have built-in state management through the setState() method:
// Update state with an object
this.setState({ count: 5 });

// Update state with a function (receives current state)
this.setState(state => ({ count: state.count + 1 }));
Calling setState() triggers a re-render of the component. The state object is shallow-merged with the existing state.

The render() Method

Every component must implement a render() method that returns virtual DOM:
render(props, state, emit) {
  return h('div', {}, [
    h('p', {}, [`Count: ${state.count}`]),
    h('button', { on: { click: () => this.increment() } }, ['Increment'])
  ]);
}
Parameters:
  • props - Current props object
  • state - Current state object
  • emit - Function to emit custom events
The render() method should be a pure function - it should always return the same output for the same props and state.

Mounting and Unmounting

Mount a component to the DOM:
const app = new MyComponent();
app.mount(document.getElementById('app'));
Unmount and clean up:
app.unmount();

Functional Components

Functional components are simple functions that return virtual DOM. They use hooks for state and side effects.

Basic Structure

import { h, useState } from 'glyphui';

function Counter(props) {
  const [count, setCount] = useState(0);
  
  return h('div', {}, [
    h('p', {}, [`Count: ${count}`]),
    h('button', {
      on: { click: () => setCount(count + 1) }
    }, ['Increment'])
  ]);
}

How Functional Components Work

From packages/runtime/src/component-factory.js:90-106, functional components are wrapped internally:
class FunctionalComponentWrapper {
  constructor(renderFn, props = {}) {
    this.renderFn = renderFn;
    this.props = props;
    this.vdom = null;
    this._renderComponent = this._renderComponent.bind(this);
  }
  
  mount(parentEl) {
    this.parentEl = parentEl;
    initHooks(this);  // Initialize hooks system
    
    const rawVdom = this.renderFn(this.props);
    finishHooks();  // Finalize hooks
    
    this.vdom = resolveSlots(rawVdom, this.slotContents);
    mountDOM(this.vdom, parentEl);
    this.isMounted = true;
  }
}
Functional components are automatically wrapped to work with the component system. You don’t need to worry about this internals.

Advantages of Functional Components

  • Simpler syntax - Less boilerplate code
  • Easier to test - Pure functions are straightforward to test
  • Hooks-based - Use hooks for state, effects, and more
  • Better composition - Easy to extract and share logic

Creating Components

Use createComponent() to create component virtual nodes:
import { createComponent } from 'glyphui';

const counterVdom = createComponent(Counter, {
  initialCount: 10,
  title: 'My Counter'
});
From packages/runtime/src/component-factory.js:22-30:
export function createComponent(ComponentClass, props = {}, children = []) {
  return {
    type: COMPONENT_TYPE,
    ComponentClass,
    props: { ...props, children },
    instance: null,
  };
}

Component Composition

Components can be composed together to build complex UIs. From examples/component-demo/component-demo.js:48-93:
class CounterContainer extends Component {
  constructor() {
    super({}, {
      initialState: {
        counters: [
          { id: 1, title: 'Counter 1', initialCount: 0 },
          { id: 2, title: 'Counter 2', initialCount: 10 }
        ]
      }
    });
  }
  
  addCounter() {
    const newId = this.state.counters.length + 1;
    this.setState({
      counters: [
        ...this.state.counters,
        {
          id: newId,
          title: `Counter ${newId}`,
          initialCount: Math.floor(Math.random() * 100)
        }
      ]
    });
  }
  
  render(props, state) {
    return h('div', {}, [
      h('h2', {}, ['Counter Container']),
      h('button', { 
        on: { click: () => this.addCounter() }
      }, ['Add Counter']),
      
      // Render child components
      ...state.counters.map(counter => 
        createComponent(Counter, {
          key: counter.id,
          title: counter.title,
          initialCount: counter.initialCount
        })
      )
    ]);
  }
}
Always provide a key prop when rendering lists of components. This helps GlyphUI efficiently update the list.

Props

Props (properties) are how parent components pass data to child components.

Accessing Props

In class components:
render(props, state) {
  return h('div', {}, [props.title]);
}
In functional components:
function MyComponent(props) {
  return h('div', {}, [props.title]);
}

Updating Props

From packages/runtime/src/component.js:117-138, when a parent re-renders with new props:
updateProps(newProps) {
  const oldProps = this.props;
  this.props = { ...this.props, ...newProps };
  
  // Extract slot content from new props.children
  if (newProps.children && Array.isArray(newProps.children)) {
    this.slotContents = extractSlotContents(newProps.children);
  }
  
  // Call lifecycle methods
  if (this.beforeUpdate) {
    this.beforeUpdate(oldProps, this.props);
  }
  
  this._renderComponent();
  
  if (this.updated) {
    this.updated(oldProps, this.props);
  }
}

Children Prop

Child elements are passed via the special children prop:
createComponent(Card, { title: 'My Card' }, [
  h('p', {}, ['Card content'])
])

// Inside Card component:
render(props) {
  return h('div', { class: 'card' }, [
    h('h3', {}, [props.title]),
    ...props.children  // Render children here
  ]);
}

Component Communication

Parent to Child - Props

Pass data down through props:
createComponent(Counter, {
  initialCount: 10,
  title: 'Clicks'
})

Child to Parent - Events

Emit custom events using the emit() method:
class ChildComponent extends Component {
  handleClick() {
    this.emit('custom-event', { data: 'some value' });
  }
  
  render() {
    return h('button', {
      on: { click: () => this.handleClick() }
    }, ['Click me']);
  }
}

Component vs Element

It’s important to understand the difference:
// Creates a component virtual node
createComponent(Counter, { initialCount: 0 })

// Type: COMPONENT_TYPE
// Has instance, lifecycle, and state

Best Practices

Each component should have a single responsibility. If a component is doing too much, split it into smaller components.
Make components reusable by accepting configuration through props instead of hardcoding values.
Let GlyphUI handle the DOM. Manipulating DOM directly can cause inconsistencies with the virtual DOM.
Use functional components with hooks unless you need lifecycle methods that hooks don’t provide.

Component Instance Properties

From packages/runtime/src/component.js:17-23, component instances have these properties:
this.props        // Current props
this.state        // Current state
this.vdom         // Current virtual DOM tree
this.parentEl     // Parent DOM element
this.isMounted    // Whether component is mounted
this.slotContents // Slot content from children

Next Steps

Build docs developers (and LLMs) love