Version: 4.0.0
GitHub: https://github.com/ramjam97/ram-state-js
Author: Ram Jam
A vanilla JavaScript state management library inspired by React’s useState, useEffect, and useMemo – but without any framework.
It helps you manage stateful data and DOM bindings easily with reactive watchers and side effects.
✅ Framework-independent (pure JavaScript)
- ✅ Reactive states (
useState) - ✅ Derived/computed states (
useMemo) - ✅ Reactive side effects (
useEffect) - ✅ Automatic DOM synchronization (
input,checkbox,radio,select,textarea) - ✅ View binding via
{ selector: callback }map - ✅ Watchers and cleanup support
- ✅ Optimized batched updates using a microtask queue
Download the minified file and include it in your project:
<script src="ram-state.min.js"></script>Use the jsDelivr CDN:
<script src="https://cdn.jsdelivr.net/gh/ramjam97/ram-state-js@v4.0.0/dist/ram-state.min.js"></script>const { version, useState, useMemo, useEffect } = RamState();
console.log(version) // v4.0.0Creates a reactive state that can be bound to DOM elements and watched for updates.
const { useState } = RamState();
const name = useState("Ram Jam", "#username");
name.watchEffect(({ value }) => console.log("Name changed:", value));<input type="checkbox" id="toggle" />
<span id="status"></span>
<script>
const { useState } = RamState();
const active = useState(false, "#toggle");
active.watchEffect(({ value }) => {
document.querySelector("#status").textContent = value ? "Active" : "Inactive";
});
</script><label><input type="radio" name="gender" value="male" /> Male</label>
<label><input type="radio" name="gender" value="female" /> Female</label>
<script>
const { useState } = RamState();
const gender = useState("male", 'input[name="gender"]');
gender.watchEffect(({ value }) => console.log("Selected:", value));
</script><select id="fruits" multiple>
<option value="apple">Apple</option>
<option value="orange">Orange</option>
<option value="banana">Banana</option>
</select>
<script>
const { useState } = RamState();
const fruits = useState(["apple"], "#fruits");
fruits.watchEffect(({ value }) => console.log("Selected fruits:", value));
</script><div id="counter"></div>
<div id="mirror"></div>
<script>
const { useState } = RamState();
const counter = useState(0, null, {
"#counter": ({ state }) => state,
"#mirror": ({ state }) => `Count: ${state}`,
});
setInterval(() => counter.set(v => v + 1), 1000);
</script>useState provides two types of local watchers: .watch() and .watchEffect(), both supporting cleanup functions that automatically run before each re-execution.
Runs every time .set() is called — even if the value didn’t actually change.
- Executes immediately once upon registration
(hasChange = false) - Then runs on every
.set()call - The callback receives:
{ model, value, hasChange }- If your callback returns a function, that function is treated as a cleanup, which will run before the watcher re-executes
- Does not return an unwatch/stop function
Example:
counter.watch(({ value }) => {
console.log('watch counter:', value);
return () => console.log('watch clean up value:', value);
});Runs only when the value actually changes.
Supports { immediate: true } to trigger once right away when registered.
- Executes only when
state.set()changes the value(hasChange = true) - If
opt.immediateistrue, runs once on registration - The callback receives:
{ model, value }- Supports cleanup functions (returned from callback)
- Does not return an unwatch/stop function
Example:
counter.watchEffect(({ value }) => {
console.log('watchEffect counter:', value);
return () => console.log('watchEffect clean up value:', value);
}, { immediate: true });Both watchers support returning a cleanup function:
return () => { /* cleanup logic before next call */ }- Cleanup runs automatically before the watcher re-runs
- There’s no API yet to remove a watcher — they persist for the lifetime of the state
- Cleanups make it easy to manage event listeners, timers, or subscriptions within watchers
Creates a derived/computed state that automatically re-computes when its dependencies change.
const { useState, useMemo } = RamState();
const num1 = useState(10);
const num2 = useState(20);
const sum = useMemo(() => num1.value + num2.value, [num1, num2]);
sum.watch(({ value }) => console.log("Sum updated:", value));
num1.set(50); // → auto recomputes → "Sum updated: 70"Runs a side effect when specific dependencies change, similar to React’s useEffect.
If no dependencies are provided, it reacts to all states.
const { useState, useEffect } = RamState();
const count = useState(0);
// Runs once at mount
useEffect(() => {
console.log("Mounted");
}, []);
// Runs when count changes
useEffect(() => {
console.log("Count changed:", count.value);
return () => console.log("Clean up previous effect");
}, [count]);
// Runs on every state change
useEffect(() => console.log("Something changed!"));Initializes the library instance.
const { version, useState, useMemo, useEffect } = RamState();Creates a new reactive state.
Parameters
| Name | Type | Description |
|---|---|---|
initialValue |
any | Initial value for the state |
model? |
string / HTMLElement / HTMLElement[] | DOM element(s) or selector(s) to bind the state to |
view? |
object | Optional configuration object mapping selectors to render callbacks |
Returns
A reactive state object with the following API:
| Property / Method | Description |
|---|---|
.value |
Get current state value |
.set(valueOrFn) |
Update state. Accepts direct value or updater function (prev) => next. |
.model |
Array of bound DOM elements |
.view |
Array of view bindings { dom, run } |
.watch(cb) |
Run on every .set() call (even unchanged). |
.watchEffect(cb, opt?) |
Run only when value changes. Supports { immediate: true }. |
DOM Binding Behavior
| Element Type | Behavior |
|---|---|
<input type="text"> |
Two-way binding of text value |
<input type="checkbox"> |
Boolean value binding |
<input type="radio"> |
Syncs by comparing value attribute with state |
<select> |
Single or multiple select support |
<textarea> |
Two-way text binding |
Other elements (<div>, <span>, etc.) |
Text or HTML rendered via view config or textContent |
Computes and memoizes a derived value.
| Method | Description |
|---|---|
.value |
Current memoized value |
.watch(cb) |
Subscribe to computed value changes |
Runs side effects with optional cleanup.
| Parameter | Description |
|---|---|
cb |
Callback function (can return a cleanup function) |
deps? |
Array of state dependencies, or null for all states |
RamState batches multiple updates in a microtask queue, ensuring efficient DOM updates and reduced layout thrashing.
All DOM updates, view renders, and watchers are executed asynchronously using:
Promise.resolve().then(flush);All watcher, effect, and render callbacks are executed safely inside a try/catch wrapper.
Any errors are logged with the prefix:
[RamState] error:
MIT License