or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-usage.mdcore-htm.mdindex.mdpreact-integration.mdreact-integration.md
tile.json

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 };