rin-lib
v0.3.0
Published
**Rin** is a minimal, TypeScript-first UI library for building reactive browser interfaces with a small runtime and predictable architecture.
Downloads
1,093
Readme
Rin Documentation
Overview
Rin is a minimal, TypeScript-first UI library for building reactive browser interfaces with a small runtime and predictable architecture.
It focuses on:
- simplicity
- explicit rendering flow
- strong typing
- minimal abstraction
- minimal virtual DOM
Rin targets developers who want component-based UI without framework overhead.
Motivation
Modern UI frameworks often introduce:
- virtual DOM overhead
- hidden lifecycle complexity
- schedulers
- large dependency graphs
- indirection between state and DOM
Rin instead provides:
- direct DOM rendering
- explicit execution model
- predictable updates
- very small runtime surface
- full control over rendering behavior
Suitable use cases:
- internal dashboards
- Tauri apps
- tooling interfaces
- experimental UI systems
- lightweight SPAs
Core Principles
Functions Are Components
Components are plain functions that return a render function:
function App() {
return () => <div>Hello</div>
}No classes. No decorators. No lifecycle APIs.
Transparent Virtual DOM
Rin builds minimal Virtual DOM templates strictly to power local, fast DOM patching.
Note: Rin may migrate entirely to an Ahead-Of-Time (AOT) compiler architecture in the future. This would completely eliminate the Virtual DOM and DOM-patching engines, allowing for true direct-to-DOM updates like Svelte or SolidJS, while preserving our explicit component execution model.
Rendering pipeline:
Component → DOM NodesThis avoids diffing cost and improves predictability.
Explicit Reactivity
State changes trigger controlled rerenders explicitly.
No hidden scheduler.
No implicit batching.
TypeScript First
Rin is designed around strict typing:
- JSX typing
- children typing
- DOM typing
- component typing
Architecture
Rendering pipeline:
JSX
↓
jsx-runtime
↓
element objects
↓
renderer
↓
real DOMJSX Runtime
Transforms:
<div>Hello</div>into:
jsx("div", { children: "Hello" });Element Structure
Example internal node:
{
type: "div",
props: {},
children: ["Hello"]
}Renderer
Renderer converts nodes into DOM:
string → TextNode
number → TextNode
element → HTMLElement
component → execute function
array → flatten recursivelyMount Engine
Attach component tree to container:
mount(<App />, document.body);Comparison With React
| Feature | Rin | React | | ----------------- | ---------- | ---------- | | Virtual DOM | Minimal | Yes | | Scheduler | No | Yes | | Hooks | Optional | Core | | Lifecycle | No | Yes | | Bundle Size | Very small | Large | | Rendering | Direct DOM | Diff-based | | Abstraction Level | Low | Medium | | Control | High | Medium |
Rin prioritizes control over automation.
Component Model
Example:
function Button() {
return <button>Click</button>;
}Usage:
function App() {
return (
<div>
<Button />
</div>
);
}Props
Props are standard function parameters:
type Props = {
title: string;
};
function Header(props: Props) {
return <h1>{props.title}</h1>;
}Usage:
<Header title="Dashboard" />Children
Children pass through JSX normally:
function Card(props) {
return () => <div>{props.children}</div>;
}Usage:
<Card>Content here</Card>Refs
To directly access a physical HTML Node (e.g. for interacting with a <canvas> element or a localized third-party charting library), you can securely bind a function closure directly to the ref prop.
Rin executes the function right after the component paints that node cleanly to the DOM:
function CanvasComponent(props, ctx) {
let canvasEl = null;
ctx.onMount(() => {
// Guaranteed to be physically mapped here because `onMount` runs safely after execution!
const context = canvasEl.getContext("2d");
context.fillRect(10, 10, 100, 100);
});
return () => <canvas ref={(el) => canvasEl = el} width="200" height="200" />;
}Rendering & Reactivity
Rin provides three primary APIs for rendering and updating the DOM explicitly.
Initial Mounting
Mount your root application to the DOM:
import { mount } from "rin-lib";
mount(<App />, document.getElementById("app"));Targeted Rerendering
Rin does not use an automated Virtual DOM scheduler. Instead, reactivity is explicit and localized. You precisely dictate when updates generate patching routines by using rerender:
- Rerender by Component Reference: Globally updates all active instances of a specific component function on the page.
import { rerender } from "rin-lib";
function Header() {
return <header>Local Time: {new Date().toLocaleTimeString()}</header>;
}
// Somewhere else, trigger an update for all <Header /> components:
rerender(Header);- Rerender by Group string: Updates specific elements or components tagged with an explicit
groupproperty. Useful for highly specific or targeted updates.
import { rerender } from "rin-lib";
// Component or element rendered with <section group="user-stats" />
rerender("user-stats");- Rerender by Component Context: Renders solely the specific component instance issuing the call. To ensure zero-magic predictability, Rin passes a reliable execution context as the second argument to any component.
function Dropdown(props, ctx) {
let isOpen = false;
const toggle = () => {
isOpen = !isOpen;
ctx.rerender(); // Triggers update ONLY for this exact dropdown instance
};
// Return a closure that handles future re-renders
return () => (
<div>
<button onclick={toggle}>Toggle</button>
{isOpen && <div>Content</div>}
</div>
);
}Demounting & Cleanup
To prevent memory leaks when components are removed dynamically, Rin provides an explicit unmount API to clean up active trees and unbind listeners.
import { unmount } from "rin-lib";
// Tear down a whole container explicitly (e.g. when unmounting the whole application)
unmount(document.getElementById("app") as HTMLElement);Lifecycles
While Rin eliminates complex implicit lifecycle scheduling, you will still need to perform side-effects predictably (like data fetching or WebGL rendering) when working with direct DOM mutations.
Because component functions are executed only once to initialize state, your ctx.onMount logic is inherently immune to infinite-loop fetch bugs. Rin saves and executes the inner closure returned by your component on every ctx.rerender:
function APIDataLoader(props, ctx) {
let data = null;
// Triggers precisely once after the element first hits the DOM.
ctx.onMount(() => {
fetch("/api/data").then(async res => {
data = await res.json();
ctx.rerender(); // Trigger local UI update
});
});
// Triggers precisely once when unmount() destroys this node.
ctx.onUnmount(() => {
console.log("Cleaning up active subscriptions...");
});
// The rendering logic closure dynamically evaluated on every update
return () => <div>{data ? data : "Loading..."}</div>;
}Example Counter
function Counter(props, ctx) {
// Initialization - runs exactly once
let count = 0;
const increment = () => {
count++;
ctx.rerender(); // rerender self only
};
// Rendering - execution closure bound to rerender
return () => (
<div>
<p>{count}</p>
<button onclick={increment}>Increment</button>
</div>
);
}JSX Runtime Setup
Example tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "rin-lib"
}
}Example Project Structure
rin-lib/
├── jsx-runtime.ts
├── types.ts
├── mount.ts
└── index.tsWhen To Use Rin
Recommended for:
- internal tools
- dashboards
- Tauri apps
- lightweight SPA
- experimental rendering engines
Not ideal for:
- plugin ecosystems
- React-dependent environments
- large teams requiring framework conventions
Design Philosophy
Rin prioritizes:
clarity > abstraction
control > automation
size > ecosystemIt is a foundation UI library rather than a full framework.
