Getting Started

Installation

Install nuclo via npm, yarn, or pnpm:

npm install nuclo
yarn add nuclo
pnpm add nuclo

That's it! No additional dependencies, no complicated setup.

TypeScript Setup

nuclo is built with TypeScript and provides full type definitions for all APIs. To get global type definitions for all tag builders and utilities, add one of the following:

Option 1: tsconfig.json

Add to your tsconfig.json:

{
  "compilerOptions": {
    "types": ["nuclo/types"]
  }
}

Option 2: Environment Declaration File

Create or modify your vite-env.d.ts (or similar):

/// <reference types="nuclo/types" />

After this setup, you'll have full IntelliSense for all 140+ HTML and SVG tags, plus all nuclo utilities.

Your First App

Let's build a simple counter application to understand the basics:

import 'nuclo';

// State - just plain JavaScript variables
let count = 0;

// Create the UI
const app = div(
  h1(() => `Count: ${count}`),
  button('Increment', on('click', () => {
    count++;
    update();
  })),
  button('Decrement', on('click', () => {
    count--;
    update();
  })),
  button('Reset', on('click', () => {
    count = 0;
    update();
  }))
);

// Render to the DOM
render(app, document.body);

Breaking it down:

  1. Import: import 'nuclo' registers all global functions
  2. State: Just use regular variables. No hooks, no stores, no magic.
  3. Reactive UI: Use functions () => ... for dynamic content
  4. Events: Use on('event', handler) to attach listeners
  5. Updates: Call update() to refresh the UI after state changes
  6. Render: Use render(element, container) to mount to the DOM

Understanding update()

The update() function is the heart of nuclo's reactivity system. nuclo requires you to explicitly trigger updates, giving you full control over when the UI refreshes.

Why explicit updates?

Advantages:

  • Performance: Batch multiple mutations into a single update cycle
  • Control: You decide exactly when the UI should refresh
  • Predictability: Zero surprise re-renders, explicit update flow
  • Simplicity: No proxies, no dependency graphs, just objects and functions
  • Debugging: Set a breakpoint at update() to trace all state changes

Batching Updates

One of the key benefits is the ability to batch multiple state changes:

// Good: Multiple changes, single update
function handleSubmit() {
  user.name = 'Alice';
  user.email = 'alice@example.com';
  user.age = 30;
  todos.push({ id: 1, text: 'New todo', done: false });
  update(); // One update for all changes
}

// Works but inefficient: Update after each change
function handleSubmit() {
  user.name = 'Alice';
  update();
  user.email = 'alice@example.com';
  update();
  user.age = 30;
  update();
  // 3 updates instead of 1!
}

Reactive Functions

Any zero-argument function becomes reactive. nuclo will call these functions during updates to get the current value.

Text Content

let name = 'World';

div(
  'Hello, ',
  () => name,  // Reactive
  '!'
);

// After name changes and update() is called,
// the div will show "Hello, Alice!"

Attributes

let isActive = false;

div({
  className: () => isActive ? 'active' : 'inactive',
  'aria-pressed': () => isActive,
  disabled: () => !isActive
});

Styles

let opacity = 1;

div({
  style: {
    opacity: () => opacity,
    display: () => opacity > 0 ? 'block' : 'none'
  }
});

Complex Expressions

let items = [1, 2, 3];

div(
  () => `You have ${items.length} item${items.length !== 1 ? 's' : ''}`
);

Event Handling

Use the on() modifier to attach event listeners:

Basic Events

button('Click me',
  on('click', () => {
    console.log('Clicked!');
  })
);

Multiple Events

input(
  on('input', (e) => {
    inputValue = e.target.value;
    update();
  }),
  on('focus', () => {
    isFocused = true;
    update();
  }),
  on('blur', () => {
    isFocused = false;
    update();
  })
);

Event Options

div(
  on('scroll', handleScroll, { passive: true }),
  on('click', handleClick, { capture: true, once: true })
);

Keyboard Events

input(
  on('keydown', (e) => {
    if (e.key === 'Enter') {
      handleSubmit();
    } else if (e.key === 'Escape') {
      handleCancel();
    }
  })
);

Best Practices

1. Batch Your Updates

// Good
function handleMultipleChanges() {
  state.field1 = value1;
  state.field2 = value2;
  state.field3 = value3;
  update();
}

// Avoid
function handleMultipleChanges() {
  state.field1 = value1;
  update();
  state.field2 = value2;
  update();
  state.field3 = value3;
  update();
}

2. Use Computed Values

// Extract complex logic into functions
function activeCount() {
  return todos.filter(t => !t.done).length;
}

div(
  () => `${activeCount()} tasks remaining`
);

3. Organize Code with Component Functions

function UserCard(user) {
  return div(
    { className: 'user-card' },
    img({ src: user.avatar, alt: user.name }),
    h3(user.name),
    p(user.bio)
  );
}

// Use it
div(
  UserCard(currentUser)
);

4. Keep State Simple

// Just use plain objects and arrays
let user = {
  name: 'Alice',
  email: 'alice@example.com',
  preferences: {
    theme: 'dark',
    notifications: true
  }
};

let todos = [
  { id: 1, text: 'Learn nuclo', done: true },
  { id: 2, text: 'Build awesome app', done: false }
];

5. Handle Async Operations

let status = 'idle'; // 'idle' | 'loading' | 'success' | 'error'
let data = null;
let error = null;

async function fetchData() {
  status = 'loading';
  update();

  try {
    const response = await fetch('/api/data');
    data = await response.json();
    status = 'success';
  } catch (err) {
    error = err.message;
    status = 'error';
  }

  update();
}

Next Steps