Overview
HellaJS is a lightweight, reactive JavaScript library for building fast, tiny interfaces. Reactivity comes from a fork of Alien Signals, and templating is inspired by the granular reactivity found in SolidJS.
It offers a familiar JSX experience with extra flexibility and impressive benchmarks. You can also work without any compile step using HTML element proxies.
Let's create a text-based image app using all the HellaJS packages. We'll use the Vite plugin for JSX and the dummyjson.com to generate images.
⚠️ Heads up:
The API is still evolving and not production ready.
Setup
Create a Vite project and choose the Vanilla template:
npm create vite@latest
cd into your project and install the HellaJS packages:
npm install @hellajs/core @hellajs/css @hellajs/dom @hellajs/resource @hellajs/router @hellajs/store vite-plugin-hellajs
Create/Update vite.config.js:
import { defineConfig } from 'vite'
import viteHellaJS from 'vite-plugin-hellajs'
export default defineConfig({
plugins: [viteHellaJS()]
})
For TypeScript, add to tsconfig.json:
{
"compilerOptions": {
"jsx": "preserve",
"types": ["@hellajs/dom"]
}
}
File structure:
src/
components/
Header.jsx
pages/
Collection.jsx
Create.jsx
App.jsx
store.js
Point the index.html file to App.jsx.
<!-- Before -->
<script type="module" src="/src/main.js"></script>`
<!-- After -->
<script type="module" src="/src/App.jsx"></script>
App & Router
In App.jsx, set up our router and global css styles, Then mount it to the DOM.
You don't have to style like this, it's just to demo the css package.
import { router } from "@hellajs/router";
import { mount } from "@hellajs/dom";
import { css } from "@hellajs/css";
import { Header } from "./components/Header";
import { Collection } from "./pages/Collection";
import { Create } from "./pages/Create";
// A simple function that takes a JSX element and mounts it
const routerMount = (Page: JSX.Element) => mount(Page, "#router")
// Set the page signal when the route changes
router({
routes: {
"/": () => routerMount(<Collection />),
"/create": () => routerMount(<Create />),
}
});
// Set some global styles
css({
"*": {
boxSizing: "border-box",
},
body: {
margin: 0,
fontFamily: "sans-serif",
},
a: {
cursor: 'pointer',
fontWeight: 'bold',
color: "mediumslateblue"
},
".container": {
maxWidth: "800px",
margin: "0 auto",
padding: "1rem",
}
}, { global: true });
const App = () => {
return (
<>
<Header />
<main class="container" id="router">
Loading...
</main>
</>
);
}
// Looks for "#app" automatically
mount(App, "#app");
⚠️ Use {page} (not {page()}) to keep it reactive.
Header Component
In components/Header.jsx, create a simple navigation bar:
import { forEach } from "@hellajs/dom";
import { navigate } from "@hellajs/router";
import { css } from "@hellajs/css";
const links = [
{ path: '/', label: 'Collection' },
{ path: '/create', label: 'Create' }
];
const style = css({
padding: "1rem",
backgroundColor: "#f0f0f0",
textAlign: "center",
nav: {
display: "flex",
justifyContent: "center",
gap: "1rem"
},
h1: {
marginTop: 0
}
});
export const Header = () => (
<header class={style}>
<h1>Text Images</h1>
<nav>
{forEach(links, link => (
<a onclick={() => navigate(link.path)}>{link.label}</a>
))}
</nav>
</header>
);
Note: Use The HellaJS forEach for reactive lists, not regular array methods.
Store
In store.js, set up a reactive store with a http resource for creating images:
import { computed, effect } from "@hellajs/core";
import { store } from "@hellajs/store";
import { resource } from "@hellajs/resource";
import { navigate } from "@hellajs/router";
const API_URL = "https://dummyjson.com/image/800x400";
const createImageStore = () => {
const storeRef = store({
backgrounds: ["red", "green", "blue", "yellow", "purple", "orange"],
fonts: ["bitter", "cairo", "comfortaa", "cookie", "dosis", "gotham", "lobster"],
color: "",
font: "",
text: "",
loading: false,
error: null,
collection: []
});
const key = computed(() => ({
color: storeRef.color(),
font: storeRef.font(),
text: storeRef.text()
}));
const storeResource = resource(({ color, font, text }) =>
fetch(`${API_URL}/${color}?fontFamily=${font}&text=${text}`.replace(/\s/g, '+')),
{ key }
);
effect(() => {
if (storeResource.error()) return alert("Error");
const url = storeResource.data()?.url;
if (!url) return;
storeRef.collection([url, ...storeRef.collection()]);
storeResource.abort();
storeRef.color("");
storeRef.font("");
storeRef.text("");
navigate("/");
});
return { store: storeRef, resource: storeResource };
};
export const imageStore = createImageStore();
Pages
Collection
In pages/Collection.jsx, display the image collection:
import { forEach } from "@hellajs/dom";
import { imageStore } from "../store";
export const Collection = () => {
const { store } = imageStore;
return (
<>
{forEach(store.collection, url => <img src={url} alt="Generated" />)}
{() => store.collection().length === 0 && <p style="text-align:center">No images generated yet.</p>}
</>
);
};
Create
In pages/Create.jsx, build a form for creating images:
import { forEach } from "@hellajs/dom"
import { postStore } from "../store"
import { css } from "@hellajs/css"
const styles = css({
"> div": {
marginBottom: "1rem",
},
label: {
display: "block",
},
"input, select": {
width: "100%",
padding: "0.5rem",
},
button: {
padding: "0.5rem 1rem",
backgroundColor: "mediumslateblue",
color: "white",
border: "none",
cursor: "pointer",
width: "100%",
"&:disabled": {
opacity: 0.5,
}
}
})
export const Create = () => {
const { store, resource } = postStore;
const submit = (e: SubmitEvent) => {
e.preventDefault();
const inValid = ![store.color, store.font, store.text].every((val) => val() !== '');
if (inValid) {
return alert("Please fill all fields");
}
resource.fetch();
}
return (
<form class={styles} onsubmit={submit}>
<div>
<label for="color">
Background Color:
</label>
<select value={store.color} id="color" onchange={(e) => store.color((e.target as HTMLSelectElement).value)}>
<option value="" disabled={store.color() !== ""}>Select</option>
{forEach(store.backgrounds, (color: string) =>
<option value={color}>{color}</option>
)}
</select>
</div>
<div>
<label for="font">
Font Family:
</label>
<select value={store.font} id="font" onchange={(e) => store.font((e.target as HTMLSelectElement).value)}>
<option value="" disabled={store.font() !== ""} >Select</option>
{forEach(store.fonts, (font: string) =>
<option value={font}>{font}</option>
)}
</select>
</div>
<div>
<label for="text">
Text Content:
</label>
<input value={store.text} type="text" id="text" placeholder="e.g. Hello World!" oninput={(e) => store.text((e.target as HTMLInputElement).value)} />
</div>
<button type="submit" disabled={resource.loading}>Create</button>
</form>
)
}
Now run npm run dev.
That’s it! You’ve got a reactive app for generating text-based images. Check the HellaJS docs for more information or visit the HellaJS Github and support the project with a star ⭐.
Top comments (0)