CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-htm

JSX-like syntax using tagged template literals for Virtual DOM without transpilation

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

react-integration.mddocs/

React Integration

HTM provides a streamlined integration with React through a pre-configured html template function that's bound to React's createElement.

Import

import { html } from "htm/react";

For TypeScript:

import { html } from "htm/react";

API Reference

html (Tagged Template Function)

declare const html: (strings: TemplateStringsArray, ...values: any[]) => React.ReactElement;

Pre-bound HTM function configured for React's createElement function.

Returns:

  • React.ReactElement - React element that can be rendered by React

Usage Examples:

import React from "react";
import { createRoot } from "react-dom/client";
import { html } from "htm/react";

// Basic element
const element = html`<div className="container">Hello World</div>`;

// Dynamic content
const name = "Alice";
const greeting = html`<h1>Hello, ${name}!</h1>`;

// Event handlers
const handleClick = () => console.log('Clicked!');
const button = html`<button onClick=${handleClick}>Click me</button>`;

// Render to DOM
const root = createRoot(document.getElementById('root'));
root.render(greeting);

Component Integration

Functional Components

import React, { useState, useEffect } from "react";
import { html } from "htm/react";

// Simple functional component
const Welcome = ({ name }) => html`
  <div className="welcome">
    <h1>Welcome, ${name}!</h1>
  </div>
`;

// Component with hooks
const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);
  
  return html`
    <div className="counter">
      <p>Count: ${count}</p>
      <button onClick=${() => setCount(count + 1)}>+</button>
      <button onClick=${() => setCount(count - 1)}>-</button>
    </div>
  `;
};

// Usage
const App = () => html`
  <div>
    <${Welcome} name="Alice" />
    <${Counter} />
  </div>
`;

Class Components

import React, { Component } from "react";
import { html } from "htm/react";

class Timer extends Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
  }
  
  componentDidMount() {
    this.interval = setInterval(() => {
      this.setState({ seconds: this.state.seconds + 1 });
    }, 1000);
  }
  
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  
  render() {
    return html`
      <div className="timer">
        <h2>Timer: ${this.state.seconds}s</h2>
        <button onClick=${() => this.setState({ seconds: 0 })}>
          Reset
        </button>
      </div>
    `;
  }
}

React-Specific Features

React Attributes

HTM automatically handles React-specific attribute names:

// Use React attribute names
const element = html`
  <div 
    className="container"
    htmlFor="input-id"
    onClick=${handleClick}
    style=${{ backgroundColor: 'blue', fontSize: '16px' }}
  >
    <label htmlFor="input-id">Label</label>
    <input id="input-id" onChange=${handleChange} />
  </div>
`;

Event Handlers

React's synthetic event system works seamlessly:

const handleSubmit = (e) => {
  e.preventDefault();
  console.log('Form submitted');
};

const handleChange = (e) => {
  console.log('Input value:', e.target.value);
};

const form = html`
  <form onSubmit=${handleSubmit}>
    <input 
      type="text" 
      onChange=${handleChange}
      placeholder="Enter text..."
    />
    <button type="submit">Submit</button>
  </form>
`;

Refs

React refs work with HTM:

import React, { useRef, useEffect } from "react";
import { html } from "htm/react";

const FocusInput = () => {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  
  return html`
    <div>
      <input 
        ref=${inputRef}
        type="text" 
        placeholder="Auto-focused input"
      />
    </div>
  `;
};

Context

React Context works with HTM components:

import React, { createContext, useContext } from "react";
import { html } from "htm/react";

const ThemeContext = createContext();

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = React.useState('light');
  
  return html`
    <${ThemeContext.Provider} value=${{ theme, setTheme }}>
      ${children}
    </${ThemeContext.Provider}>
  `;
};

const ThemedButton = ({ children }) => {
  const { theme, setTheme } = useContext(ThemeContext);
  
  return html`
    <button 
      className="btn btn-${theme}"
      onClick=${() => setTheme(theme === 'light' ? 'dark' : 'light')}
    >
      ${children}
    </button>
  `;
};

Complete Usage Examples

Todo App with React Hooks

import React, { useState, useCallback } from "react";
import { createRoot } from "react-dom/client";
import { html } from "htm/react";

const TodoItem = ({ todo, onToggle, onRemove }) => html`
  <li className=${todo.completed ? 'completed' : ''}>
    <input 
      type="checkbox"
      checked=${todo.completed}
      onChange=${() => onToggle(todo.id)}
    />
    <span className="todo-text">${todo.text}</span>
    <button 
      className="remove-btn"
      onClick=${() => onRemove(todo.id)}
    >
      ×
    </button>
  </li>
`;

const TodoApp = () => {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');
  
  const addTodo = useCallback(() => {
    if (input.trim()) {
      setTodos(prev => [...prev, {
        id: Date.now(),
        text: input,
        completed: false
      }]);
      setInput('');
    }
  }, [input]);
  
  const toggleTodo = useCallback((id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);
  
  const removeTodo = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);
  
  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      addTodo();
    }
  };
  
  return html`
    <div className="todo-app">
      <h1>React Todo App</h1>
      <div className="input-section">
        <input 
          type="text"
          value=${input}
          onChange=${(e) => setInput(e.target.value)}
          onKeyPress=${handleKeyPress}
          placeholder="Add a todo..."
        />
        <button onClick=${addTodo}>Add</button>
      </div>
      <ul className="todo-list">
        ${todos.map(todo => html`
          <${TodoItem}
            key=${todo.id}
            todo=${todo}
            onToggle=${toggleTodo}
            onRemove=${removeTodo}
          />
        `)}
      </ul>
      <div className="stats">
        Total: ${todos.length}, 
        Completed: ${todos.filter(t => t.completed).length}
      </div>
    </div>
  `;
};

// Render app
const root = createRoot(document.getElementById('root'));
root.render(html`<${TodoApp} />`);

Form Handling

import React, { useState } from "react";
import { html } from "htm/react";

const ContactForm = () => {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  const [errors, setErrors] = useState({});
  
  const validateForm = () => {
    const newErrors = {};
    
    if (!formData.name.trim()) {
      newErrors.name = 'Name is required';
    }
    
    if (!formData.email.includes('@')) {
      newErrors.email = 'Valid email is required';
    }
    
    if (!formData.message.trim()) {
      newErrors.message = 'Message is required';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateForm()) {
      console.log('Form submitted:', formData);
      // Reset form
      setFormData({ name: '', email: '', message: '' });
    }
  };
  
  const handleChange = (field) => (e) => {
    setFormData(prev => ({
      ...prev,
      [field]: e.target.value
    }));
    
    // Clear error when user starts typing
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: '' }));
    }
  };
  
  return html`
    <form className="contact-form" onSubmit=${handleSubmit}>
      <h2>Contact Us</h2>
      
      <div className="form-group">
        <label htmlFor="name">Name:</label>
        <input 
          id="name"
          type="text"
          value=${formData.name}
          onChange=${handleChange('name')}
          className=${errors.name ? 'error' : ''}
        />
        ${errors.name && html`<span className="error-text">${errors.name}</span>`}
      </div>
      
      <div className="form-group">
        <label htmlFor="email">Email:</label>
        <input 
          id="email"
          type="email"
          value=${formData.email}
          onChange=${handleChange('email')}
          className=${errors.email ? 'error' : ''}
        />
        ${errors.email && html`<span className="error-text">${errors.email}</span>`}
      </div>
      
      <div className="form-group">
        <label htmlFor="message">Message:</label>
        <textarea 
          id="message"
          value=${formData.message}
          onChange=${handleChange('message')}
          className=${errors.message ? 'error' : ''}
          rows="4"
        />
        ${errors.message && html`<span className="error-text">${errors.message}</span>`}
      </div>
      
      <button type="submit">Send Message</button>
    </form>
  `;
};

Integration with React Ecosystem

React Router

import { BrowserRouter, Route, Routes, Link } from "react-router-dom";
import { html } from "htm/react";

const Home = () => html`<h1>Home Page</h1>`;
const About = () => html`<h1>About Page</h1>`;
const Contact = () => html`<h1>Contact Page</h1>`;

const App = () => html`
  <${BrowserRouter}>
    <nav>
      <${Link} to="/">Home<//>
      <${Link} to="/about">About<//>
      <${Link} to="/contact">Contact<//>
    </nav>
    
    <${Routes}>
      <${Route} path="/" element=${html`<${Home} />`} />
      <${Route} path="/about" element=${html`<${About} />`} />
      <${Route} path="/contact" element=${html`<${Contact} />`} />
    <//>
  <//>
`;

State Management

import { useReducer } from "react";
import { html } from "htm/react";

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'REMOVE_TODO':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
};

const TodoApp = () => {
  const [todos, dispatch] = useReducer(todoReducer, []);
  
  return html`
    <div>
      <button onClick=${() => dispatch({ type: 'ADD_TODO', text: 'New todo' })}>
        Add Todo
      </button>
      <ul>
        ${todos.map(todo => html`
          <li key=${todo.id}>
            <span style=${{ textDecoration: todo.done ? 'line-through' : 'none' }}>
              ${todo.text}
            </span>
            <button onClick=${() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}>
              Toggle
            </button>
            <button onClick=${() => dispatch({ type: 'REMOVE_TODO', id: todo.id })}>
              Remove
            </button>
          </li>
        `)}
      </ul>
    </div>
  `;
};

Types

// React HTML template function
declare const html: (strings: TemplateStringsArray, ...values: any[]) => React.ReactElement;

// React element type (from React)
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
  type: T;
  props: P;
  key: Key | null;
}

// Component types
type FunctionComponent<P = {}> = (props: P) => ReactElement | null;
type ComponentClass<P = {}, S = ComponentState> = new (props: P, context?: any) => Component<P, S>;

// Event handler types
type MouseEventHandler<T = Element> = (event: MouseEvent<T>) => void;
type ChangeEventHandler<T = Element> = (event: ChangeEvent<T>) => void;
type FormEventHandler<T = Element> = (event: FormEvent<T>) => void;
type KeyboardEventHandler<T = Element> = (event: KeyboardEvent<T>) => void;

// Ref types
type RefObject<T> = { readonly current: T | null };
type MutableRefObject<T> = { current: T };

docs

advanced-usage.md

core-htm.md

index.md

preact-integration.md

react-integration.md

tile.json