Skip to main content
Component composition is the practice of building complex UIs by combining simpler components together. GlyphUI makes it easy to nest components and pass data between them.

Creating Child Components

Use createComponent() to include one component inside another:
import { h, createComponent, Component } from "glyphui";

class App extends Component {
  render() {
    return h("div", {}, [
      h("h1", {}, ["My App"]),
      createComponent(Counter, { initialCount: 0 })
    ]);
  }
}

Passing Props

Props flow from parent to child components:
// Child component
const UserCard = (props) => {
  return h("div", { class: "card" }, [
    h("h2", {}, [props.name]),
    h("p", {}, [props.email]),
    h("span", {}, [props.role])
  ]);
};

// Parent component
class UserList extends Component {
  render(props, state) {
    return h("div", {}, [
      createComponent(UserCard, {
        name: "Alice Smith",
        email: "[email protected]",
        role: "Developer"
      })
    ]);
  }
}

Passing Children

Components can accept children through props.children:
// Container component that wraps children
const Card = (props) => {
  return h("div", { class: "card" }, [
    h("div", { class: "card-body" }, [
      ...props.children  // Spread children here
    ])
  ]);
};

// Using the container
const app = createComponent(Card, {}, [
  h("h2", {}, ["Card Title"]),
  h("p", {}, ["Card content goes here"])
]);
Children are automatically available in props.children as an array.

List Rendering

Render multiple components from an array using map():
class CounterContainer extends Component {
  constructor() {
    super({}, {
      initialState: {
        counters: [
          { id: 1, title: "Counter 1", initialCount: 0 },
          { id: 2, title: "Counter 2", initialCount: 10 },
          { id: 3, title: "Counter 3", initialCount: 5 }
        ]
      }
    });
  }
  
  render(props, state) {
    return h("div", {}, [
      h("h2", {}, ["Counter Container"]),
      
      // Map over the array to create multiple Counter components
      ...state.counters.map(counter =>
        createComponent(Counter, {
          key: counter.id,  // Important: add a key for list items
          title: counter.title,
          initialCount: counter.initialCount
        })
      )
    ]);
  }
}
Always provide a unique key prop when rendering lists of components. This helps GlyphUI efficiently update the DOM.

Callback Props

Pass functions from parent to child to handle events:
// Child component
const TodoItem = (props) => {
  return h("div", { class: "todo-item" }, [
    h("span", {}, [props.text]),
    h("button", {
      on: { click: () => props.onDelete(props.id) }
    }, ["Delete"])
  ]);
};

// Parent component
class TodoList extends Component {
  constructor() {
    super({}, {
      initialState: {
        todos: [
          { id: 1, text: "Learn GlyphUI" },
          { id: 2, text: "Build an app" }
        ]
      }
    });
  }
  
  deleteTodo(id) {
    this.setState({
      todos: this.state.todos.filter(todo => todo.id !== id)
    });
  }
  
  render(props, state) {
    return h("div", {}, [
      ...state.todos.map(todo =>
        createComponent(TodoItem, {
          key: todo.id,
          id: todo.id,
          text: todo.text,
          onDelete: (id) => this.deleteTodo(id)  // Pass callback
        })
      )
    ]);
  }
}

Nesting Multiple Levels

Components can be nested to any depth:
// Button component
const Button = (props) => {
  return h("button", {
    class: props.variant || "primary",
    on: { click: props.onClick }
  }, [props.label]);
};

// Counter component (uses Button)
class Counter extends Component {
  constructor(props) {
    super(props, {
      initialState: { count: props.initialCount || 0 }
    });
  }
  
  increment() {
    this.setState({ count: this.state.count + 1 });
  }
  
  render(props, state) {
    return h("div", { class: "counter" }, [
      h("div", {}, [state.count.toString()]),
      createComponent(Button, {
        label: "Increment",
        variant: "primary",
        onClick: () => this.increment()
      })
    ]);
  }
}

// Container component (uses Counter)
class App extends Component {
  render() {
    return h("div", { class: "app" }, [
      h("h1", {}, ["My Counter App"]),
      createComponent(Counter, { initialCount: 0 })
    ]);
  }
}

Composition Patterns

Container/Presentational Pattern

Separate logic (container) from presentation (presentational):
// Presentational: only receives props and renders UI
const CounterDisplay = (props) => {
  return h("div", { class: "counter" }, [
    h("div", { class: "counter-value" }, [props.count.toString()]),
    h("button", { on: { click: props.onIncrement } }, ["Increment"]),
    h("button", { on: { click: props.onDecrement } }, ["Decrement"])
  ]);
};

// Container: manages state and logic
class CounterContainer extends Component {
  constructor(props) {
    super(props, {
      initialState: { count: 0 }
    });
  }
  
  increment() {
    this.setState({ count: this.state.count + 1 });
  }
  
  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
  
  render(props, state) {
    return createComponent(CounterDisplay, {
      count: state.count,
      onIncrement: () => this.increment(),
      onDecrement: () => this.decrement()
    });
  }
}

Layout Components

Create reusable layout wrappers:
// Layout component
const PageLayout = (props) => {
  return h("div", { class: "page" }, [
    h("header", { class: "header" }, [props.header]),
    h("main", { class: "main" }, props.children),
    h("footer", { class: "footer" }, [props.footer || "© 2024"])
  ]);
};

// Using the layout
const app = createComponent(PageLayout, {
  header: h("h1", {}, ["My App"])
}, [
  h("p", {}, ["Main content here"]),
  h("p", {}, ["More content"])
]);

Conditional Rendering

Conditionally include components based on state:
class App extends Component {
  constructor() {
    super({}, {
      initialState: { showDetails: false }
    });
  }
  
  toggleDetails() {
    this.setState({ showDetails: !this.state.showDetails });
  }
  
  render(props, state) {
    return h("div", {}, [
      h("button", {
        on: { click: () => this.toggleDetails() }
      }, [state.showDetails ? "Hide" : "Show"]),
      
      // Conditionally render component
      state.showDetails && createComponent(DetailPanel, {})
    ]);
  }
}

Fragments

Use hFragment() to group elements without adding extra DOM nodes:
import { hFragment } from "glyphui";

const List = (props) => {
  return hFragment([
    h("li", {}, ["Item 1"]),
    h("li", {}, ["Item 2"]),
    h("li", {}, ["Item 3"])
  ]);
};

Best Practices

Each component should do one thing well. If a component is getting too complex, split it into smaller components.
Make components reusable by accepting configuration through props instead of hardcoding values.
When multiple components need to share state, move the state to their closest common ancestor.
When rendering lists, always provide a unique key prop to help GlyphUI efficiently update the DOM.

Complete Example

Here’s a complete example showing composition in action:
import { h, Component, createComponent } from "glyphui";

// Button component
const Button = (props) => {
  return h("button", {
    class: `btn btn-${props.variant || "primary"}`,
    on: { click: props.onClick },
    disabled: props.disabled
  }, [props.children || props.label]);
};

// Alert component
const Alert = (props) => {
  return h("div", { class: `alert alert-${props.type || "info"}` }, [
    ...props.children
  ]);
};

// Counter component
class Counter extends Component {
  constructor(props) {
    super(props, {
      initialState: { count: props.initialCount || 0 }
    });
  }
  
  increment() {
    this.setState({ count: this.state.count + 1 });
  }
  
  reset() {
    this.setState({ count: 0 });
  }
  
  render(props, state) {
    return h("div", { class: "counter" }, [
      h("h3", {}, [props.title]),
      h("div", { class: "count" }, [state.count.toString()]),
      createComponent(Button, {
        label: "Increment",
        variant: "primary",
        onClick: () => this.increment()
      }),
      createComponent(Button, {
        label: "Reset",
        variant: "secondary",
        onClick: () => this.reset(),
        disabled: state.count === 0
      }),
      state.count > 10 && createComponent(Alert, { type: "warning" }, [
        h("strong", {}, ["Warning: "]),
        "Count is getting high!"
      ])
    ]);
  }
}

// App component
class App extends Component {
  render() {
    return h("div", { class: "app" }, [
      h("h1", {}, ["Counter Demo"]),
      createComponent(Counter, {
        title: "Counter 1",
        initialCount: 0
      }),
      createComponent(Counter, {
        title: "Counter 2",
        initialCount: 5
      })
    ]);
  }
}

Functional Components

Learn about functional components

Class Components

Learn about class-based components

Slots

Advanced content projection with slots

Build docs developers (and LLMs) love