A monorepo containing the RxBlox state management library and demo application.
packages/rxblox- The main library package (publishable to NPM)packages/rxblox-demo- Demo application using the librarypackages/rxblox-todo- TodoMVC implementation showcasing best practices
pnpm install# Run demo app
pnpm dev
# Run todo app with watch build (recommended for development)
pnpm dev:todo
# Build library
pnpm build
# Build library in watch mode
cd packages/rxblox
pnpm build:watch
# Run tests
pnpm testWhen developing the library and todo app simultaneously:
# Terminal 1: Watch build rxblox + run todo dev server
pnpm dev:todoThis command automatically:
- Rebuilds
rxbloxwhen you change library code - Hot-reloads the todo app via Vite HMR
- Runs both processes in parallel
To publish the rxblox package:
cd packages/rxblox
# Dry run (check what would be published)
pnpm dry
# Version bump
pnpm version:minor # or version:major
# Build and publish
pnpm build
npm publishWhen working with complex props in blox components, create signals with custom equality to prevent unnecessary re-renders.
import { blox, rx, signal, shallowEquals } from "rxblox";
interface Props {
user: { id: number; name: string; email: string };
config: { theme: string; locale: string };
}
export const UserCard = blox<Props>((props) => {
// ✅ GOOD: Use shallowEquals to avoid re-renders when object identity changes
// but contents are the same
const user = signal(() => props.user, { equals: shallowEquals });
const config = signal(() => props.config, { equals: shallowEquals });
return rx(() => {
// Now these only re-render when user/config contents actually change
const currentUser = user();
const currentConfig = config();
return (
<div className={`theme-${currentConfig.theme}`}>
<h2>{currentUser.name}</h2>
<p>{currentUser.email}</p>
</div>
);
});
});import { blox, rx, signal } from "rxblox";
import { isEqual } from "lodash-es";
interface Props {
data: {
nested: {
deeply: {
value: number;
};
};
};
}
export const DeepComponent = blox<Props>((props) => {
// ✅ GOOD: Use deep equality for deeply nested structures
const data = signal(() => props.data, { equals: isEqual });
return rx(() => {
// Only re-renders when deeply nested value actually changes
return <div>{data().nested.deeply.value}</div>;
});
});Without custom equality:
// ❌ BAD: Re-renders every time parent re-renders (new object identity)
export const UserCard = blox<Props>((props) => {
return rx(() => {
// props.user is a new object on every parent render
// Even if contents are identical, this rx() re-runs!
return <div>{props.user.name}</div>;
});
});With custom equality:
// ✅ GOOD: Only re-renders when contents change
export const UserCard = blox<Props>((props) => {
const user = signal(() => props.user, { equals: shallowEqual });
return rx(() => {
// user() only changes when contents differ
return <div>{user().name}</div>;
});
});interface Props {
items: Array<{ id: number; price: number; quantity: number }>;
}
export const ShoppingCart = blox<Props>((props) => {
// Signal with shallow equality
const items = signal(() => props.items, { equals: shallowEqual });
// Computed signal that only recalculates when items change
const total = signal(() => {
return items().reduce((sum, item) => sum + item.price * item.quantity, 0);
});
const tax = signal(() => total() * 0.1);
const grandTotal = signal(() => total() + tax());
return rx(() => (
<div>
<div>Subtotal: ${total()}</div>
<div>Tax: ${tax()}</div>
<div>Total: ${grandTotal()}</div>
</div>
));
});Rule of Thumb: If you need more than 3 rx() blocks in a single blox component, consider:
- Splitting into smaller
bloxcomponents - Using a single
rx()block
// Anti-pattern: Too granular, hard to maintain
export const UserProfile = blox<Props>((props) => {
return (
<div>
{rx(() => <h1>{props.user.name}</h1>)}
{rx(() => <p>{props.user.email}</p>)}
{rx(() => <p>{props.user.bio}</p>)}
{rx(() => <img src={props.user.avatar} />)}
{rx(() => <span>{props.user.status}</span>)}
{rx(() => <div>{props.user.location}</div>)}
</div>
);
});Problems:
- Hard to read and maintain
- Creates many subscriptions
- Minimal performance benefit
- Over-optimization
// Better: Single rx() block for related content
export const UserProfile = blox<Props>((props) => {
const user = signal(() => props.user, { equals: shallowEqual });
return rx(() => {
const currentUser = user();
return (
<div>
<h1>{currentUser.name}</h1>
<p>{currentUser.email}</p>
<p>{currentUser.bio}</p>
<img src={currentUser.avatar} />
<span>{currentUser.status}</span>
<div>{currentUser.location}</div>
</div>
);
});
});Benefits:
- Cleaner, more readable code
- Easier to maintain
- Good enough performance for most cases
- All related data updates together
// Best: Split into logical components
export const UserProfile = blox<Props>((props) => {
return (
<div>
<UserHeader user={props.user} />
<UserBio user={props.user} />
<UserStatus user={props.user} />
</div>
);
});
const UserHeader = blox((props: { user: User }) => {
const user = signal(() => props.user, { equals: shallowEqual });
return rx(() => {
const { name, avatar } = user();
return (
<div>
<img src={avatar} />
<h1>{name}</h1>
</div>
);
});
});
const UserBio = blox((props: { user: User }) => {
const user = signal(() => props.user, { equals: shallowEqual });
return rx(() => (
<p>{user().bio}</p>
));
});
const UserStatus = blox((props: { user: User }) => {
const user = signal(() => props.user, { equals: shallowEqual });
return rx(() => (
<span>{user().status}</span>
));
});Benefits:
- Clear separation of concerns
- Each component optimizes independently
- Easier to test and reuse
- Better code organization
Use 2-3 rx() blocks when they have truly independent update patterns:
export const Dashboard = blox(() => {
const user = signal(getCurrentUser());
const notifications = signal(getNotifications());
const messages = signal(getMessages());
return (
<div>
{/* Updates only when user changes */}
{rx(() => (
<header>Welcome, {user().name}</header>
))}
{/* Updates only when notifications change */}
{rx(() => (
<aside>
<h3>Notifications ({notifications().length})</h3>
{notifications().map(n => <div key={n.id}>{n.text}</div>)}
</aside>
))}
{/* Updates only when messages change */}
{rx(() => (
<main>
<h3>Messages ({messages().length})</h3>
{messages().map(m => <div key={m.id}>{m.text}</div>)}
</main>
))}
</div>
);
});This is fine because:
- ✅ Each section updates independently
- ✅ Clear performance benefit (e.g., new message doesn't re-render notifications)
- ✅ Still maintainable
DO:
- ✅ Use
shallowEqualfor object/array props - ✅ Use
isEqual(deep) for deeply nested structures - ✅ Create signals with custom equality for expensive computations
- ✅ Use 1-3
rx()blocks per component - ✅ Split large components into smaller
bloxcomponents
DON'T:
- ❌ Create signals without equality checking for complex props
- ❌ Use more than 3
rx()blocks in one component - ❌ Over-optimize with too many tiny
rx()blocks - ❌ Create
rx()blocks for static content
Use batch() to group multiple signal updates, preventing unnecessary recomputations:
import { batch, signal } from "rxblox";
const firstName = signal("John");
const lastName = signal("Doe");
const fullName = signal(() => `${firstName()} ${lastName()}`);
// ❌ Without batch: fullName recomputes twice
firstName.set("Jane");
lastName.set("Smith");
// ✅ With batch: fullName recomputes once
batch(() => {
firstName.set("Jane");
lastName.set("Smith");
});When to use batch():
- ✅ Updating multiple related signals
- ✅ Performance-critical paths (loops, event handlers)
- ✅ Preventing inconsistent intermediate states
For more details, see the Batching Guide.
For more examples, see the TodoMVC implementation in packages/rxblox-todo.
- API Reference - Complete API documentation
- Batching Guide - How to batch updates efficiently
- Patterns & Best Practices - Common patterns and tips
- vs. Other Libraries - Feature comparison with other state libraries