HYDROXIDE

JSX

Learn how JSX works in Hydroxide

JSX lets you write HTML-like syntax in JavaScript. Hydroxide compiles JSX into optimized DOM operations. If you've used React before, the JSX is very similar with some differences.

Key Differences from React

FeatureReactHydroxide
Class attributeclassNameclass
Event handlersonClickon-click
Real-time inputonChangeon-input
Attribute namescamelCaselowercase (HTML spec)
Inline stylesstyle={{ color: 'red' }}style="color: red"
Two-way bindingManualbind-value, bind-checked
ConditionalsTernary in JSXif, else-if, else attributes
RefsuseRef()const ref = {}

HTML-like Syntax

Hydroxide's JSX stays close to HTML. Attribute names match the HTML spec (not camelCase like React):

// React
<div className="card" tabIndex={0} autoFocus />

// Hydroxide (same as HTML)
<div class="card" tabindex={0} autofocus />

This means you can copy HTML from anywhere and use it directly. Just remember to self-close void elements:

<input />
<img />
<br />

Expressions

Use curly braces to embed JavaScript expressions:

const name = 'Alice'
const items = ['apple', 'banana', 'cherry']

<h1>Hello, {name}!</h1>
<p>You have {items.length} items</p>

When the expression contains a reactive, it becomes a reactive binding - which updates automatically when the value changes:

const count = reactive(0)

<p>Count: {count()}</p>  // updates when count changes

Event Handlers

Event handlers use the on- prefix with lowercase event names (matching browser events):

// React
<button onClick={handleClick} onMouseEnter={handleHover} />

// Hydroxide
<button on-click={handleClick} on-mouseenter={handleHover} />
import { reactive } from 'hydroxide';

function Example() {
  const count = reactive(0);

  function increment() {
    count.set(count() + 1);
  }

  // open the console to see the logs
  // when state is updated, component does not re-render
  console.log('No renders!')

  return (
    <button on-click={increment} class="primary-button">
      count is {count()}
    </button>
  );
}

export default Example;

Common events: on-click, on-input, on-change, on-submit, on-keydown, on-mouseenter, on-mouseleave, on-focus, on-blur

Coming from React?

React's onChange actually uses the "input" event instead of "change" event which fires when the input loses focus.

To have the same behavior as React, use on-input instead of on-change to listen to real-time updates instead of when the input loses focus.


Two-Way Binding

Use bind-value to sync an input's value with a reactive:

const text = reactive('')

<input type="text" bind-value={text} />
<p>You typed: {text()}</p>
import { reactive } from 'hydroxide';

function TextInput() {
  const text = reactive('Hello, Hydroxide!');

  return (
    <div class="container">
     <input
       type="text"
       bind-value={text}
       placeholder="Enter your text here"
     />
     <p class='preview'>{text() || "Nothing"}</p>
    </div>
  );
}

export default TextInput;

For checkboxes, use bind-checked:

const checked = reactive(false)

<input type="checkbox" bind-checked={checked} />

Conditional Rendering

Use if, else-if, and else attributes for conditional rendering:

const count = reactive(0)

<p if={count() === 0}>Zero</p>
<p else-if={count() < 5}>Less than 5</p>
<p else>5 or more</p>
import { reactive } from "hydroxide";

function FizzBuzz() {
  const count = reactive(0);

  return (
    <div class="container">
      <input
        type="range"
        min="0"
        max="100"
        bind-value={count}
      />
      <h1>{count()}</h1>
      <p if={count() % 15 === 0} class="result fizzbuzz">FizzBuzz</p>
      <p else-if={count() % 3 === 0} class="result fizz">Fizz</p>
      <p else-if={count() % 5 === 0} class="result buzz">Buzz</p>
      <p else class="result">{count()}</p>
    </div>
  );
}

export default FizzBuzz;

Only one of the elements renders at a time. When the condition changes, Hydroxide swaps them efficiently.

Refs

Get a reference to a DOM element with the ref attribute. Create an empty object and assign it to the ref attribute of an element.

The DOM node of that element will be assigned to the current property of the object which can accessed safely inside onConnect lifecycle hook of the component

import { onConnect } from 'hydroxide';

function Canvas() {
  const canvasRef = {};

  onConnect(() => {
    const ctx = canvasRef.current.getContext('2d');

    ctx.fillStyle = '#a855f7';
    ctx.fillRect(20, 20, 60, 60);

    ctx.fillStyle = '#22d3ee';
    ctx.beginPath();
    ctx.arc(130, 50, 30, 0, Math.PI * 2);
    ctx.fill();
  });

  return <canvas ref={canvasRef} width="180" height="100" />;
}

export default Canvas;

Styles

Inline styles use a string, just like HTML:

<div style="background-color: red; font-size: 2rem">Styled content</div>

Use template literals for reactive styles:

const hue = reactive(200)

<div style={`background-color: hsl(${hue()}, 70%, 60%)`} />
import { reactive } from 'hydroxide';

function App() {
  const hue = reactive(200);

  return (
    <div class="container">
      <div
        class="box"
        style={`background-color: oklch(0.7 0.3 ${hue()}); transform: rotate(${hue() / 2}deg)`}
      />
      <input
        type="range"
        min="0"
        max="360"
        bind-value={hue}
      />
      <p>Hue: {hue()}°</p>
    </div>
  );
}

export default App;

On this page