Basic Components in Coherent.js

Learn how to create and use basic components in Coherent.js using pure JavaScript objects.

What are Coherent Components?

Coherent.js components are pure JavaScript objects or functions that return objects. No JSX, no templates, no compilation required.

Basic Object Components

Simple Static Components

// A simple component as a pure object
const WelcomeMessage = {
  div: {
    className: 'welcome',
    children: [
      { h1: { text: 'Welcome to Coherent.js!' } },
      { p: { text: 'Build with pure JavaScript objects' } }
    ]
  }
};

Text and HTML Content

const ContentExample = {
  div: {
    className: 'content',
    children: [
      // Text content
      { p: { text: 'This is plain text' } },
      
      // HTML content (be careful with user input!)
      { div: { html: '<strong>Bold HTML content</strong>' } },
      
      // Mixed content
      { p: { 
        text: 'Plain text with ',
        children: [
          { strong: { text: 'nested elements' } }
        ]
      }}
    ]
  }
};

Function Components

Basic Function Components

// Component as a function for dynamic content
const Greeting = (props = {}) => {
  const { name = 'World', mood = 'happy' } = props;
  
  return {
    div: {
      className: `greeting greeting--${mood}`,
      children: [
        { h2: { text: `Hello, ${name}!` } },
        { p: { text: `You seem ${mood} today` } }
      ]
    }
  };
};

// Usage
const myGreeting = Greeting({ name: 'Developer', mood: 'fantastic' });

Conditional Rendering

const UserStatus = ({ user, isLoggedIn }) => ({
  div: {
    className: 'user-status',
    children: [
      // Conditional rendering with pure JS
      isLoggedIn ? {
        div: {
          className: 'logged-in',
          children: [
            { h3: { text: `Welcome back, ${user.name}!` } },
            { p: { text: 'You are logged in' } }
          ]
        }
      } : {
        div: {
          className: 'logged-out',
          children: [
            { h3: { text: 'Please log in' } },
            { button: { text: 'Login', onclick: 'showLogin()' } }
          ]
        }
      }
    ].filter(Boolean) // Remove null/undefined values
  }
});

Lists and Iteration

const TodoList = ({ todos = [] }) => ({
  div: {
    className: 'todo-list',
    children: [
      { h3: { text: 'My Todo List' } },
      { ul: {
        children: todos.map(todo => ({
          li: {
            className: todo.completed ? 'completed' : 'pending',
            children: [
              { span: { text: todo.text } },
              { button: { 
                text: todo.completed ? 'Undo' : 'Done',
                onclick: `toggleTodo(${todo.id})`
              }}
            ]
          }
        }))
      }},
      // Show message if no todos
      todos.length === 0 ? {
        p: { 
          className: 'empty-message',
          text: 'No todos yet. Add one!' 
        }
      } : null
    ].filter(Boolean)
  }
});

Component Attributes and Props

HTML Attributes

const LinkComponent = ({ href, text, target = '_self' }) => ({
  a: {
    href: href,
    target: target,
    className: 'custom-link',
    rel: target === '_blank' ? 'noopener noreferrer' : undefined,
    text: text
  }
});

CSS Classes

const Button = ({ text, variant = 'primary', size = 'medium', disabled = false }) => ({
  button: {
    className: [
      'btn',
      `btn--${variant}`,
      `btn--${size}`,
      disabled ? 'btn--disabled' : null
    ].filter(Boolean).join(' '),
    disabled: disabled,
    text: text
  }
});

Event Handlers

const InteractiveCard = ({ title, onClick, onHover }) => ({
  div: {
    className: 'interactive-card',
    onclick: onClick,
    onmouseover: onHover,
    children: [
      { h3: { text: title } },
      { p: { text: 'Click or hover me!' } }
    ]
  }
});

Component Composition

Building Complex Components

const Header = ({ title, subtitle }) => ({
  header: {
    className: 'page-header',
    children: [
      { h1: { text: title } },
      subtitle ? { p: { className: 'subtitle', text: subtitle } } : null
    ].filter(Boolean)
  }
});

const Navigation = ({ links = [] }) => ({
  nav: {
    className: 'navigation',
    children: [
      { ul: {
        children: links.map(link => ({
          li: {
            children: [
              LinkComponent({ href: link.url, text: link.title })
            ]
          }
        }))
      }}
    ]
  }
});

const Layout = ({ title, subtitle, navLinks, children }) => ({
  div: {
    className: 'layout',
    children: [
      Header({ title, subtitle }),
      Navigation({ links: navLinks }),
      { main: {
        className: 'main-content',
        children: Array.isArray(children) ? children : [children]
      }}
    ]
  }
});

Rendering Components

Server-Side Rendering

import { render } from '@coherent.js/core';

const component = Greeting({ name: 'Server User', mood: 'excited' });
const html = render(component);
console.log(html);
// Output: <div class="greeting greeting--excited">...</div>

Using the Factory Function

import { render } from '@coherent.js/core';

const html = render(component);

Best Practices

1. Use Descriptive Component Names

// ✅ Good
const UserProfileCard = ({ user }) => ({ /* ... */ });

// ❌ Avoid
const Card = ({ user }) => ({ /* ... */ });

2. Validate Props

const SafeComponent = (props = {}) => {
  const { title, items = [] } = props;
  
  if (!title) {
    return { div: { text: 'Error: Title is required' } };
  }
  
  return {
    div: {
      children: [
        { h2: { text: title } },
        // ... rest of component
      ]
    }
  };
};

3. Keep Components Pure

// ✅ Pure component - same input, same output
const PureGreeting = ({ name }) => ({
  div: { text: `Hello, ${name}!` }
});

// ❌ Impure - uses external state
let globalCounter = 0;
const ImpureCounter = () => ({
  div: { text: `Count: ${++globalCounter}` }
});

4. Use Composition Over Inheritance

// ✅ Compose smaller components
const ProfilePage = ({ user }) => ({
  div: {
    className: 'profile-page',
    children: [
      Header({ title: 'User Profile' }),
      UserCard({ user }),
      UserPosts({ posts: user.posts })
    ]
  }
});

Common Patterns

1. Default Props Pattern

const ComponentWithDefaults = (props = {}) => {
  const config = {
    title: 'Default Title',
    showIcon: true,
    variant: 'primary',
    ...props // Override defaults with provided props
  };
  
  return {
    div: {
      className: `component component--${config.variant}`,
      children: [
        config.showIcon ? { i: { className: 'icon' } } : null,
        { h3: { text: config.title } }
      ].filter(Boolean)
    }
  };
};

2. Children Pattern

const Container = ({ className, children }) => ({
  div: {
    className: `container ${className || ''}`,
    children: Array.isArray(children) ? children : [children]
  }
});

// Usage
const page = Container({
  className: 'page-container',
  children: [
    { h1: { text: 'Page Title' } },
    { p: { text: 'Page content' } }
  ]
});

3. Higher-Order Components

const withLoading = (component, isLoading) => {
  if (isLoading) {
    return { div: { className: 'loading', text: 'Loading...' } };
  }
  return component;
};

// Usage
const MyComponent = { div: { text: 'Loaded content' } };
const LoadingComponent = withLoading(MyComponent, true);

Event Handling

Coherent.js supports a powerful pattern for handling events directly on elements using function-valued props. When you define a function as a prop value for event attributes (like onclick, oninput, etc.), Coherent.js automatically handles it:

  1. Server-side (Node.js): Stores the function in a global registry available during hydration
  2. Client-side (Browser): Serializes the event handler as an inline JavaScript call to a global handler
  3. During Hydration: The global handler looks up the original function and executes it with proper component context

Usage Example

const CounterComponent = withState({ count: 0 })(({ state, setState }) => ({
  div: {
    class: 'counter-widget',
    'data-coherent-component': 'counter',
    children: [
      {
        button: {
          text: `Count: ${state.count}`,
          class: 'btn btn-primary',
          onclick: (event, state, setState) => {
            setState({ count: state.count + 1 });
          }
        }
      },
      {
        input: {
          type: 'text',
          value: state.text || '',
          oninput: (event, state, setState) => {
            setState({ text: event.target.value });
          }
        }
      }
    ]
  }
}));

Function Parameters

Event handler functions receive three parameters:

  1. event - The DOM event object
  2. state - The current component state
  3. setState - Function to update the component state

How It Differs from data-action

While Coherent.js still supports the data-action/data-target pattern, the function-on-element approach provides:

  • More direct and intuitive event handling
  • Automatic context binding
  • Cleaner component definitions
  • No need to define separate action handlers

Limitations

  • Event handlers must be serializable functions (no closures over external variables)
  • The pattern is designed for simple event handling; complex logic should use separate functions

Next Steps

Now that you understand basic components, explore:

  1. State Management - Handle dynamic data
  2. Component Styling - Make components look great
  3. Advanced Components - Complex patterns and optimization