Build a Counter App

Let’s build an interactive counter app to learn the fundamentals of HellaJS step-by-step.

What You’ll Learn

Core reactive concepts:

  • Signals - How to create and use reactive state
  • Derived Values - How to derive state automatically
  • Effects - How to run side effects when state changes

Practical patterns:

  • Event handling with reactive state updates
  • Conditional rendering and styling based on state

Each section builds on the previous one, so you’ll see how these concepts work together to create reactive applications.

Project Setup

Installation

Use your favorite package manager to scaffold a new Vite project.

npm create vite@latest counter-app -- --template vanilla

Navigate into your new project directory and install the HellaJS packages, Vite plugin, and Tailwind CSS.

cd counter-app
npm install @hellajs/core @hellajs/dom
npm install -D vite-plugin-hellajs @tailwindcss/vite@next

Configuration

Update your vite.config.js to use the HellaJS plugin and Tailwind CSS.

// vite.config.js
import { defineConfig } from 'vite';
import viteHellaJS from 'vite-plugin-hellajs';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [viteHellaJS(), tailwindcss()],
});

If you’re using TypeScript, you can add type definitions for HellaJS. Update your tsconfig.json.

{
  "compilerOptions": {
    //...
    "jsx": "preserve",
    "types": ["@hellajs/dom"]
    //...
  }
}

Finally, import Tailwind into your css file.

@import "tailwindcss";

Reactive Component

Components are simply functions that return an element and signals automatically update when their values change.

Replace the content of main.js with the following.

import { signal } from "@hellajs/core";
import { mount } from "@hellajs/dom";
import './style.css';

const Counter = () => {
  const count = signal(0);

  return (
    <div class="p-4 text-center">
      <h1 class="text-2xl mb-4">Counter: {count}</h1>
      <button 
        onClick={() => count(count() + 1)}
        class="px-4 py-2 bg-blue-500 text-white rounded"
      >
        Click me!
      </button>
    </div>
  );
};

mount(Counter, '#app');

Start the development server with npm run dev to see the app in action.

Code Explanation

  • signal(0) - Creates a reactive state with an intial value of 0
  • {count} - Creates a reactive binding that updates automatically
  • onClick={() => count(count() + 1)} - Updates the signal value
  • mount(Counter, '#app') - Renders the component into the DOM

Derived State

What if you want to show information that depends on the count? We can use derived functions.

import { signal } from "@hellajs/core";
import { mount } from "@hellajs/dom";
import "./style.css";

const Counter = () => {
  const count = signal(0);
  
  const isEven = () => count() % 2 === 0;
  const message = () => 
    count() === 0 ? "Click to start!" : 
    isEven() ? `${count()} is even` : `${count()} is odd`;

  return (
    <div class="p-4 text-center">
      <h1 class="text-2xl mb-4">Counter: {count}</h1>
      <p class={`mb-4 ${isEven() ? 'text-green-600' : 'text-blue-600'}`}>
        {message}
      </p>
      <button 
        onClick={() => count(count() + 1)}
        class="px-4 py-2 bg-blue-500 text-white rounded"
      >
        +
      </button>
    </div>
  );
};

mount(Counter, '#app');

Code Explanation

  • () => count() % 2 === 0 - Creates a derived value that updates when count changes
  • Dependency tracking - message depends on both count and isEven, so it updates when either changes
  • Reactive styling - The dyanmic class changes automatically based on isEven()

You don’t need computed for simple derived state. You can use regular functions or inline expressions.

Conditional Controls

Let’s make our counter more interactive by adding multiple ways to change the count.

Add increment, decrement, and reset buttons, plus some smart button disabling.

import { signal, computed } from "@hellajs/core";
import { mount } from "@hellajs/dom";
import "./global.css";

const Counter = () => {
  const count = signal(0);

  const isEven = () => count() % 2 === 0;
  const message = () => 
    count() === 0 ? "Click to start!" : 
    isEven() ? `${count()} is even` : `${count()} is odd`;

  return (
    <div class="p-4 text-center">
      <h1 class="text-2xl mb-4">Counter: {count}</h1>
      
      <p class={`mb-4 ${isEven() ? 'text-green-600' : 'text-blue-600'}`}>
        {message}
      </p>
      
      <div class="flex gap-2 justify-center">
        <button 
          onClick={() => count(count() - 1)}
          class="px-3 py-2 bg-red-500 text-white rounded"
          disabled={count() === 0}
        >
          -
        </button>
        
        <button 
          onClick={() => count(count() + 1)}
          class="px-3 py-2 bg-blue-500 text-white rounded"
        >
          +
        </button>
        
        <button 
          onClick={() => count(0)}
          class="px-3 py-2 bg-gray-500 text-white rounded"
          disabled={count() === 0}
        >
          Reset
        </button>
      </div>
    </div>
  );
};

mount(Counter, '#app');

Code Explanation

  • Event handler functions - Simple functions that update state
  • Conditional attributes - disabled={count() === 0} disables buttons reactively

Signal Side Effects

What about actions that happen when state changes? Like updating the browser tab title or saving to localStorage?

Effects run side effects automatically when their dependencies change

import { signal, computed, effect } from "@hellajs/core";
import { mount } from "@hellajs/dom";
import "./global.css";

const Counter = () => {
  const count = signal(0);
  
  const isEven = () => count() % 2 === 0;
  const message = () => 
    count() === 0 ? "Click to start!" : 
    isEven() ? `${count()} is even` : `${count()} is odd`;

  // Side effect: update document title
  effect(() => {
    document.title = `Counter: ${count()}`;
  });

  return (
    <div class="p-4 text-center">
      <h1 class="text-2xl mb-4">Counter: {count}</h1>
      <p class={`mb-4 ${isEven() ? 'text-green-600' : 'text-blue-600'}`}>
        {message}
      </p>
      
     <div class="flex gap-2 justify-center">
        <button 
          onClick={() => count(count() - 1)}
          class="px-3 py-2 bg-red-500 text-white rounded"
          disabled={count() === 0}
        >
          -
        </button>
        
        <button 
          onClick={() => count(count() + 1)}
          class="px-3 py-2 bg-blue-500 text-white rounded"
        >
          +
        </button>
        
        <button 
          onClick={() => count(0)}
          class="px-3 py-2 bg-gray-500 text-white rounded"
          disabled={count() === 0}
        >
          Reset
        </button>
      </div>
    </div>
  );
};

mount(Counter, '#app');

Code Explanation

  • effect(() => { document.title = ... }) - Side effect that runs when count changes

Next Steps

Ready to level up? Here are your best next steps.

The patterns you learned here are the foundation for any HellaJS application. You’re ready to build amazing reactive web apps!