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)