Complete Guide to Lithent Virtual DOM Library Features
@superlucky84|December 25, 2023 (2y ago)1,010 views
Introduction
Lithent is a lightweight (4KB gzipped) JavaScript virtual DOM library that enables predictable UI development.
With clear closure-based state management and optional extension features, it flexibly handles everything from small projects to full-scale SPAs. You can use it directly from CDN without build tools, or in modern development environments with Vite and JSX.
Core Philosophy:
- 4KB Core: Minimal core with only essential features
- Closure-based State: State management with pure JavaScript closures, no complex APIs
- Manual/Reactive Choice: Manual
renew()control or automatic reactivelstate - Progressive Adoption: From static HTML to full SPA
Table of Contents
1. Basic Features
- mount / lmount - Component creation
- updater - Render function
- renew - Manual updates
- render - DOM mounting
- Lifecycle Hooks - mountCallback, updateCallback, mountReadyCallback
- portal - DOM position control
- props & children - Component communication
- ref - Direct DOM access
- innerHTML - Raw HTML insertion
- stateless components - Components without state
- nextTick - Wait for DOM updates
2. Extension Features (Helper)
- state / lstate - Reactive state
- store / lstore - Global state
- context / lcontext - Context sharing
- computed - Computed values
- effect - Side effects
- cacheUpdate - Memoization
3. Template Options
- JSX - Vite plugin
- HTM Tags - Tagged template literals
- FTags - Functional tags
- Template Strings - Experimental templates
4. SSR
- Server-Side Rendering - renderToString, hydration
Basic Features
Lithent provides the minimum essential features needed to use virtual DOM. Basic features alone are sufficient to build practical UIs.
mount / lmount
mount is a function that creates components. It uses closures to define state and functionality, returning an updater function that returns JSX.
Manual Mode (mount):
import { mount } from 'lithent';
const Counter = mount((renew, props) => {
let count = 0;
const increase = () => {
count++;
renew(); // Manually trigger update
};
return () => <button onClick={increase}>count: {count}</button>;
});
Automatic Reactive Mode (lmount):
import { lmount } from 'lithent';
import { lstate } from 'lithent/helper';
const Counter = lmount((props) => {
const count = lstate(0);
const increase = () => {
count.value++; // Automatically updates
};
return () => <button onClick={increase}>count: {count.value}</button>;
});
Mounter Function Arguments:
- First argument:
renew- Component update function (can be omitted in lmount) - Second argument:
props- Properties received from parent - Third argument:
children- Child elements
The mounter runs only once when the component is first created. Variables and functions defined here are preserved in closures and can be continuously used in the updater.
updater
The function returned by the mounter is called the updater. The updater returns JSX and runs every time the component updates.
const Component = mount((renew, props) => {
let count = 0;
const increase = () => { count++; renew(); };
// This function is the updater
return (props) => (
<button onClick={increase}>
count: {count}, prop: {props.value}
</button>
);
});
The updater can optionally receive props as an argument. You can reference the mounter's props via closure, but using the updater's argument is safer to guarantee the latest props at update time.
renew
The renew function manually updates a component. It's provided as the first argument of the mounter, and when called, the updater re-runs to refresh the UI.
const Component = mount(renew => {
let items = ['a', 'b'];
const addItem = () => {
items.push('c');
renew(); // Trigger UI update
};
return () => (
<div>
<ul>{items.map(item => <li>{item}</li>)}</ul>
<button onClick={addItem}>Add</button>
</div>
);
});
renew() removes unnecessary automation, allowing clear control over update timing. Using Helper's state or lstate can automate renew() calls.
render
The render function connects the virtual DOM to the actual DOM.
import { h, render, mount } from 'lithent';
const App = mount(renew => {
let count = 0;
return () => <div>Count: {count}</div>;
});
// Basic usage: appendChild
render(<App />, document.getElementById('root'));
// Using insertBefore
render(
<App />,
document.getElementById('root'),
document.querySelector('.insert-before-me')
);
The render function returns a destroy function. Calling destroy() unmounts the component.
const destroy = render(<App />, document.getElementById('root'));
// Remove later
destroy();
Lifecycle Hooks
You can execute specific logic at component lifecycle points.
mountCallback: Runs immediately after the component is mounted to the DOM.
import { mount, mountCallback } from 'lithent';
const Component = mount(() => {
mountCallback(() => {
console.log('mounted');
// Cleanup function to run on unmount
return () => {
console.log('unmounted');
};
});
return () => <div>Component</div>;
});
updateCallback: Runs before and after component updates.
import { mount, updateCallback } from 'lithent';
const Component = mount((renew, props) => {
updateCallback(
() => {
console.log('before update');
return () => console.log('after update');
},
() => [props.count] // Dependencies: only runs when count changes
);
return ({ count }) => <div>count: {count}</div>;
});
mountReadyCallback:
Runs when the virtual DOM is ready, before actual DOM mounting. Executes faster than mountCallback.
import { mount, mountReadyCallback } from 'lithent';
const Component = mount(() => {
mountReadyCallback(() => {
console.log('virtual dom ready, before actual mount');
return () => {
console.log('cleanup before unmount');
};
});
return () => <div>Component</div>;
});
Execution Order: mountReadyCallback → Actual DOM mount → mountCallback
portal
Portals render child elements at a different location in the DOM.
import { mount, portal, Fragment } from 'lithent';
const Modal = mount(renew => {
let count = 0;
const increase = () => { count++; renew(); };
return () => (
<Fragment>
<button onClick={increase}>Update</button>
{portal(
<div class="modal">Count: {count}</div>,
document.getElementById('modal-root')
)}
</Fragment>
);
});
Portals are useful for UIs that need to escape parent DOM hierarchy, such as modals, tooltips, and dropdowns.
props & children
Components receive data through props and children.
const Child = mount((renew, props, children) => {
return () => (
<div>
<h1>{props.title}</h1>
<div>{children}</div>
</div>
);
});
const Parent = mount(() => {
return () => (
<Child title="Hello">
<p>This is children content</p>
</Child>
);
});
Props are accessible in both mounter and updater, while children are provided as the mounter's third argument.
ref
Using ref allows direct access to DOM elements.
import { mount, ref } from 'lithent';
const Component = mount(() => {
const inputRef = ref<HTMLInputElement>();
const focus = () => {
inputRef.value?.focus();
};
return () => (
<div>
<input ref={inputRef} type="text" />
<button onClick={focus}>Focus Input</button>
</div>
);
});
The ref's .value is only set after the component is mounted.
innerHTML
The innerHTML property allows safe insertion of raw HTML.
const Component = mount(() => {
const htmlContent = '<strong>Bold</strong> and <em>italic</em> text';
return () => <div innerHTML={htmlContent} />;
});
Warning: Do not use user input directly in innerHTML to prevent XSS attacks.
stateless components
Components without state can be written as pure functions without mount.
// Stateless component
const Greeting = ({ name }, children) => (
<div>
<h1>Hello, {name}!</h1>
{children}
</div>
);
// Usage
const App = mount(() => {
return () => (
<Greeting name="World">
<p>Welcome to Lithent</p>
</Greeting>
);
});
Stateless components only receive props and children, suitable for creating reusable UI pieces.
nextTick
nextTick waits until the next DOM update is complete.
import { mount, nextTick } from 'lithent';
const Component = mount(renew => {
let count = 0;
const increase = async () => {
count++;
renew();
await nextTick();
// DOM update is complete at this point
console.log('DOM updated');
};
return () => <button onClick={increase}>count: {count}</button>;
});
nextTick uses queueMicrotask internally to execute after all re-render requests are processed.
Extension Features (Helper)
Helpers are optional extensions built on top of basic features for more convenient use. You can import only the Helpers you need.
All Helper implementation code can be found in the GitHub repository.
state / lstate
The state Helper creates reactive state that automatically calls renew() when values change.
Manual Mode (state):
import { mount } from 'lithent';
import { state } from 'lithent/helper';
const Counter = mount(renew => {
const count = state(0, renew);
const increase = () => {
count.value++; // renew() is automatically called
};
return () => <button onClick={increase}>count: {count.value}</button>;
});
Automatic Mode (lstate):
import { lmount } from 'lithent';
import { lstate } from 'lithent/helper';
const Counter = lmount(() => {
const count = lstate(0); // No renew argument needed
return () => <button onClick={() => count.value++}>count: {count.value}</button>;
});
state is implemented with a simple getter/setter pattern:
export const state = <T>(value: T, renew: () => boolean) => {
let result = value;
return {
get value() { return result; },
set value(newValue: T) {
result = newValue;
renew();
}
};
};
store / lstore
store manages global state shared by multiple components.
import { mount } from 'lithent';
import { store } from 'lithent/helper';
// Create store
const useSharedStore = store({
text: 'shared text',
count: 0
});
const Component1 = mount(renew => {
const sharedStore = useSharedStore(renew);
const changeText = (e) => {
sharedStore.text = e.target.value;
};
return () => <input value={sharedStore.text} onInput={changeText} />;
});
const Component2 = mount(renew => {
// Subscribe to specific properties only
const sharedStore = useSharedStore(renew, (store) => [store.text]);
return () => <div>{sharedStore.text}</div>;
});
lstore (Automatic Mode):
import { lmount } from 'lithent';
import { lstore } from 'lithent/helper';
// lstore returns a { useStore, watch } object
const counterStore = lstore({ count: 0 });
const Display = lmount(() => {
// Use useStore() in lmount
const counter = counterStore.useStore();
return () => <div>Count: {counter.count}</div>;
});
const Controls = lmount(() => {
const counter = counterStore.useStore();
return () => <button onClick={() => counter.count++}>+</button>;
});
lstore returns { useStore, watch }. useStore automatically subscribes in lmount, while watch is used in mount when manually passing renew.
Using store allows sharing state across multiple components without prop drilling.
context / lcontext
context passes data through the component tree. Unlike store, each Provider can have independent values.
Creating context:
import { mount } from 'lithent';
import { createContext } from 'lithent/helper';
// 1. Define and create Context type
type ThemeContext = {
theme: string;
};
const themeContext = createContext<ThemeContext>();
const { Provider, contextState, useContext } = themeContext;
// 2. Create state with contextState in Provider
const App = mount(renew => {
// Create state with contextState (recommended without renew)
const themeState = contextState('light');
return () => (
<Provider theme={themeState}>
<Button />
<ThemeToggle />
</Provider>
);
});
// 3. Access with useContext in Consumer
const Button = mount(renew => {
const ctx = useContext(themeContext, renew);
return () => (
<button style={{ background: ctx.theme.value === 'dark' ? '#333' : '#fff' }}>
Click me
</button>
);
});
const ThemeToggle = mount(renew => {
const ctx = useContext(themeContext, renew);
const toggleTheme = () => {
ctx.theme.value = ctx.theme.value === 'light' ? 'dark' : 'light';
};
return () => <button onClick={toggleTheme}>Toggle Theme</button>;
});
Selective Subscription:
// Subscribe to specific keys only
const ctx = useContext(themeContext, renew, ['theme']);
lcontext (Automatic Mode): lcontext is context for lmount components. Renew management is automated for convenience.
computed
computed pre-processes complex calculations to keep templates clean.
import { mount } from 'lithent';
import { state, computed } from 'lithent/helper';
const Component = mount(renew => {
const count = state(0, renew);
const doubleCount = computed(() => count.value * 2);
const quadCount = computed(() => doubleCount.value * 2);
return () => (
<div>
<p>count: {count.value}</p>
<p>double: {doubleCount.value}</p>
<p>quad: {quadCount.value}</p>
<button onClick={() => count.value++}>+</button>
</div>
);
});
Computed values are getter-only and recalculated whenever the updater runs.
effect
effect is a Helper that combines mountCallback and updateCallback to manage side effects.
import { mount } from 'lithent';
import { state, effect } from 'lithent/helper';
const Timer = mount(renew => {
const seconds = state(0, renew);
let intervalId: number;
effect(
// forward: Execute side effect
() => {
intervalId = setInterval(() => {
seconds.value += 1;
}, 1000);
},
// backward: Cleanup (optional)
() => {
clearInterval(intervalId);
},
// dependencies: Function returning dependency array (optional)
() => [] // Empty array = run only on mount
);
return () => <div>Seconds: {seconds.value}</div>;
});
Argument Structure:
- First argument (forward): Function that executes the side effect
- Second argument (backward): Cleanup function (optional, can be undefined)
- Third argument (dependencies): Function returning dependency array (optional, defaults to empty array)
Dependency Management:
const count = state(0, renew);
// Empty array: run only on mount
effect(() => console.log('Once'), undefined, () => []);
// Run when specific value changes
effect(() => console.log('Count changed'), undefined, () => [count.value]);
Important: Cleanup is a separate second argument, not a return from forward.
cacheUpdate
cacheUpdate prevents re-rendering if specific values haven't changed.
import { mount } from 'lithent';
import { state, cacheUpdate } from 'lithent/helper';
const Component = mount(renew => {
const count1 = state(0, renew);
const count2 = state(0, renew);
return cacheUpdate(
() => [count1.value], // Only detect count1
() => (
<div>
count1: {count1.value}, count2: {count2.value}
<button onClick={() => count1.value++}>+count1</button>
<button onClick={() => count2.value++}>+count2 (no rerender)</button>
</div>
)
);
});
Even if count2 changes, the UI won't update, preventing unnecessary renders.
Template Options
Lithent supports various template syntaxes.
JSX
When using with Vite, you can use JSX directly with the official plugin.
npm install -D @lithent/lithent-vite
// vite.config.ts
import { defineConfig } from 'vite';
import lithent from '@lithent/lithent-vite';
export default defineConfig({
plugins: [lithent()]
});
TypeScript configuration:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "lithent"
}
}
HTM Tags
Using lTag allows JSX-like syntax without build tools.
<script src="https://cdn.jsdelivr.net/npm/lithent/dist/lithent.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lithent/tag/dist/lithentTag.umd.js"></script>
<script>
const { render, mount, Fragment } = lithent;
const { lTag } = lithentTag;
const Component = mount(renew => {
let count = 0;
const increase = () => { count++; renew(); };
return () => lTag`
<${Fragment}>
<div>count: ${count}</div>
<button onClick=${increase}>Increase</button>
<//>
`;
});
render(lTag`<${Component} />`, document.getElementById('root'));
</script>
lTag is HTM pre-bound to Lithent's h function.
FTags
fTags defines virtual DOM using function calls.
<script src="https://cdn.jsdelivr.net/npm/lithent/dist/lithent.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lithent/ftags/dist/lithentFTags.umd.js"></script>
<script>
const { render } = lithent;
const { fTags, fMount, fFragment } = lithentFTags;
const { div, button, p } = fTags;
const Counter = fMount(renew => {
let count = 0;
const increase = () => { count++; renew(); };
return () => fFragment(
p(`count: ${count}`),
button({ onClick: increase }, 'Increase')
);
});
render(Counter(), document.getElementById('root'));
</script>
fTags has the advantage of maintaining type safety without build tools.
Template Strings
You can use additional Vue-like directive syntax with JSX through the @lithent/lithent-vite plugin options. It works exactly like JSX, but with additional directives such as l-if, l-else, and l-for.
⚠️ Warning: This feature is still in experimental development stage.
npm install -D @lithent/lithent-vite
// vite.config.ts
import { defineConfig } from 'vite';
import lithent from '@lithent/lithent-vite';
export default defineConfig({
plugins: [
lithent({
template: 'string' // Enable Template Strings mode
})
]
});
import { mount } from 'lithent';
const Component = mount(renew => {
let count = 0;
const increase = () => { count++; renew(); };
return () => (
<div>
<p l-if={count > 0}>Count is positive: {count}</p>
<p l-else>Count is zero or negative</p>
<button l-on:click={increase}>Increase</button>
</div>
);
});
Supported directives: l-if, l-else, l-for, l-on:*, l-bind:*, etc.
SSR
Lithent supports server-side rendering and hydration.
renderToString
Generates HTML strings on the server.
import { renderToString } from 'lithent/ssr';
import { App } from './App';
// Express example
app.get('/', (req, res) => {
const html = renderToString(<App />);
res.send(`
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>
`);
});
hydration
Attaches events to server-rendered HTML on the client.
import { hydration } from 'lithent/ssr';
import { App } from './App';
// Client
hydration(<App />, document.getElementById('root'));
Hydration optimizes initial load performance by reusing existing DOM.
renderWithHydration
A universal render function that works on both server and client.
import { renderWithHydration } from 'lithent/ssr';
// Acts as renderToString on server
// Acts as hydration on client
const result = renderWithHydration(<App />, containerElement);
Conclusion
Lithent is designed around three core principles: small size, clear control, and selective extension.
- Build complete UIs with 4KB core
- Predictable closure-based state management
- Selectively use only needed Helpers
- Various template options: JSX, HTM, FTags, etc.
- SSR and hydration support
For more details, see the official documentation.
Project Links: