React hooks and components for seamless integration with the ZDR reactive entity framework.
npm install @zdr-tools/zdr-reactReact hooks and components for seamless integration with the ZDR reactive entity framework.
This package provides React-specific utilities that bridge the gap between ZDR's reactive entity system and React components. It includes custom hooks for managing reactive properties, event brokers, form fields, and visibility tracking, along with utility components for common React patterns. These tools enable React applications to leverage ZDR's reactive capabilities while maintaining optimal performance through proper React patterns like debouncing, throttling, and automatic cleanup.
``bash`
npm install @zdr-tools/zdr-react
- React 18+
- React DOM 18+
Reactive hook that subscribes to a property broker and triggers React updates when the value changes.
Purpose: Connects ZDR reactive properties to React's rendering system with optional performance optimizations through throttling and debouncing.
API:
| Parameter | Type | Description |
|-----------|------|-------------|
| broker | IPropEventBroker | Property broker to observe |options
| | IPropEventBrokerOptions? | Optional timing configuration |
Options:
- timing.wait: Wait time in millisecondstiming.throttle
- : Throttle settings for high-frequency updatestiming.debounce
- : Debounce settings for delayed updates
Returns: T - Current value of the property broker
Usage Example:
`typescript
import { usePropBrokerValue } from '@zdr-tools/zdr-react';
import { PropEventBroker } from '@zdr-tools/zdr-entities';
function UserProfile({ user }: { user: User }) {
// Basic usage - updates on every change
const name = usePropBrokerValue(user.name);
const email = usePropBrokerValue(user.email);
// Throttled updates - max once per 100ms
const description = usePropBrokerValue(user.description, {
timing: {
wait: 100,
throttle: { leading: true, trailing: true }
}
});
// Debounced updates - waits 300ms after last change
const searchQuery = usePropBrokerValue(user.searchQuery, {
timing: {
wait: 300,
debounce: { leading: false, trailing: true }
}
});
return (
Email: {email}
Description: {description}
Search: {searchQuery}
// Real-time counter with throttling
function LiveCounter({ counter }: { counter: PropEventBroker
const count = usePropBrokerValue(counter, {
timing: {
wait: 50, // Update max every 50ms
throttle: { leading: true, trailing: true }
}
});
return
$3
Hook that subscribes to multiple event brokers and triggers React updates when any of them emit events.
Purpose: Enables React components to reactively update based on various ZDR events, with automatic cleanup and null safety.
API:
| Parameter | Type | Description |
|-----------|------|-------------|
|
...props | (IEventBroker \| undefined)[] | Event brokers to observe |Returns:
void - Causes component re-render when events fireUsage Example:
`typescript
import { useEventRefresher } from '@zdr-tools/zdr-react';function UserDashboard({ user, notifications, settings }: Props) {
// Re-render when user properties change, notifications arrive, or settings update
useEventRefresher(
user.propertyChanged,
notifications.newMessage,
settings.themeChanged,
user.avatar?.updated // Safe to pass undefined
);
return (
Welcome {user.name.get()}
Notifications: {notifications.getUnreadCount()}
Theme: {settings.theme.get()}
{user.avatar &&
}
);
}// Entity collection updates
function UserList({ userCollection }: { userCollection: EntityCollection }) {
useEventRefresher(
userCollection.itemsAdded,
userCollection.itemRemoved,
userCollection.collectionChanged
);
const users = userCollection.getItems();
return (
{users.map(user => (
- {user.name.get()}
))}
);
}
`$3
Specialized hook for reactive text fields with validation metadata extraction.
Purpose: Provides a complete interface for text inputs with built-in validation state, length constraints, and violation handling.
API:
| Parameter | Type | Description |
|-----------|------|-------------|
|
textBroker | IReadablePropEventBroker | Text property broker |Returns:
ITextFieldResult with:
- text: Current text value
- minLength: Minimum length from validator metadata
- maxLength: Maximum length from validator metadata
- violations: Current validation violationsUsage Example:
`typescript
import { useTextField } from '@zdr-tools/zdr-react';
import { textMinLength, textMaxLength } from '@zdr-tools/zdr-entities';function UserForm({ user }: { user: User }) {
const nameField = useTextField(user.name); // Has min/max length validators
const emailField = useTextField(user.email);
const bioField = useTextField(user.biography);
return (
);
}// Dynamic form validation
function ContactForm() {
const [message] = useState(() => new PropEventBroker(
new AdvancedEventEmitter(),
'',
{
validators: [
textMinLength(10),
textMaxLength(500)
]
}
));
const messageField = useTextField(message);
const isValid = !messageField.violations;
return (
value={messageField.text}
onChange={e => message.set(e.target.value)}
placeholder={Write at least ${messageField.minLength} characters...}
maxLength={messageField.maxLength}
/>
Characters: {messageField.text.length}/{messageField.maxLength}
{messageField.minLength && messageField.text.length < messageField.minLength && (
(need {messageField.minLength - messageField.text.length} more)
)}
);
}
`$3
Hook providing boolean state management with open/close functionality.
Purpose: Simplifies modal, dropdown, and toggle state management with optimized callbacks.
Returns:
[boolean, () => void, () => void] - [isOpen, open, close]Usage Example:
`typescript
import { useOpenClose } from '@zdr-tools/zdr-react';function DropdownMenu({ items }: { items: string[] }) {
const [isOpen, open, close] = useOpenClose();
return (
{isOpen && (
{items.map(item => (
{item}
))}
)}
);
}// Modal dialog
function UserEditModal({ user }: { user: User }) {
const [isOpen, open, close] = useOpenClose();
return (
<>
{isOpen && (
e.stopPropagation()}>
Edit User
)}
>
);
}// Collapsible section
function CollapsibleSection({ title, children }: PropsWithChildren<{ title: string }>) {
const [isExpanded, expand, collapse] = useOpenClose();
return (
{isExpanded && (
{children}
)}
);
}
`$3
Hook for reactive timestamp management with manual update capability.
Purpose: Provides current timestamp state with ability to refresh, useful for time-based displays and cache busting.
Returns:
{ timestamp: number, setCurrentTimestamp: () => void }Usage Example:
`typescript
import { useTimestamp } from '@zdr-tools/zdr-react';function LiveClock() {
const { timestamp, setCurrentTimestamp } = useTimestamp();
useEffect(() => {
const interval = setInterval(setCurrentTimestamp, 1000);
return () => clearInterval(interval);
}, [setCurrentTimestamp]);
return (
Current time: {new Date(timestamp).toLocaleTimeString()}
);
}// Cache-busting for API calls
function RefreshableData() {
const { timestamp, setCurrentTimestamp } = useTimestamp();
const [data, setData] = useState(null);
// Refresh data when timestamp changes
useEffect(() => {
fetchData(timestamp).then(setData);
}, [timestamp]);
return (
Last updated: {new Date(timestamp).toLocaleString()}
{data && }
);
}// Session timeout warning
function SessionManager({ user }: { user: User }) {
const { timestamp, setCurrentTimestamp } = useTimestamp();
const lastActivity = usePropBrokerValue(user.lastActivity);
const sessionTimeout = 30 60 1000; // 30 minutes
useEffect(() => {
const interval = setInterval(setCurrentTimestamp, 60000); // Check every minute
return () => clearInterval(interval);
}, [setCurrentTimestamp]);
const timeSinceActivity = timestamp - lastActivity;
const isNearTimeout = timeSinceActivity > sessionTimeout - 5 60 1000; // 5 min warning
if (isNearTimeout) {
return (
Your session will expire soon.
);
} return null;
}
`$3
Internal hook providing increment functionality for triggering React updates.
Purpose: Foundation hook used by other ZDR React hooks to force component re-renders when reactive values change.
Returns:
{ currentNumber: number, increment: () => void }Usage Example:
`typescript
import { useIncrement } from '@zdr-tools/zdr-react';// Custom reactive hook
function useCustomEventBroker(broker: IEventBroker) {
const { increment } = useIncrement();
useEffect(() => {
return broker.register(() => {
increment(); // Force re-render
});
}, [broker, increment]);
}
// Manual refresh trigger
function ManualRefreshComponent({ dataSource }: { dataSource: DataSource }) {
const { increment } = useIncrement();
const refreshData = useCallback(() => {
dataSource.refresh().then(() => {
increment(); // Force component update after async operation
});
}, [dataSource, increment]);
return (
);
}
`$3
Hook for tracking element visibility using Intersection Observer.
Purpose: Provides visibility detection with customizable triggering behavior and ref management.
API:
| Parameter | Type | Description |
|-----------|------|-------------|
|
options | { triggerVisibleOnlyOnce?: boolean } | Visibility options |Returns:
{ wrapperRef: RefObjectUsage Example:
`typescript
import { useIsVisible } from '@zdr-tools/zdr-react';// Lazy loading images
function LazyImage({ src, alt }: { src: string; alt: string }) {
const { wrapperRef, isVisible } = useIsVisible({
triggerVisibleOnlyOnce: true
});
return (
{isVisible ? (

) : (
Loading...
)}
);
}// Analytics tracking
function AnalyticsTracker({ eventName, data }: { eventName: string; data: any }) {
const { wrapperRef, isVisible } = useIsVisible({
triggerVisibleOnlyOnce: true
});
useEffect(() => {
if (isVisible) {
analytics.track(eventName, data);
}
}, [isVisible, eventName, data]);
return
;
}// Infinite scroll trigger
function InfiniteScrollTrigger({ onVisible }: { onVisible: () => void }) {
const { wrapperRef, isVisible } = useIsVisible({
triggerVisibleOnlyOnce: false // Trigger every time it becomes visible
});
useEffect(() => {
if (isVisible) {
onVisible();
}
}, [isVisible, onVisible]);
return (
Loading more...
);
}
`$3
Hook for responsive design based on viewport width.
Purpose: Provides reactive mobile/desktop detection for responsive behavior.
Returns:
boolean - True if viewport is mobile widthUsage Example:
`typescript
import { useIsMobileWidth } from '@zdr-tools/zdr-react';function ResponsiveNavigation() {
const isMobile = useIsMobileWidth();
return (
);
}
// Conditional rendering
function ProductGrid({ products }: { products: Product[] }) {
const isMobile = useIsMobileWidth();
const itemsPerRow = isMobile ? 1 : 3;
return (
gridTemplateColumns: repeat(${itemsPerRow}, 1fr)
}}>
{products.map(product => (
))}
$3
Hook that subscribes to event brokers with throttling to limit update frequency.
Purpose: Provides reactive updates with performance optimization for high-frequency events.
API:
| Parameter | Type | Description |
|-----------|------|-------------|
|
throttleMs | number | Throttle interval in milliseconds |
| ...eventBrokers | IEventBroker[] | Event brokers to observe |Usage Example:
`typescript
import { useThrottledEventRefresher } from '@zdr-tools/zdr-react';// High-frequency updates from real-time data
function LiveStockTicker({ stockPrices }: { stockPrices: EntityCollection }) {
// Throttle updates to once per 100ms max
useThrottledEventRefresher(
100,
stockPrices.collectionChanged,
stockPrices.itemChanged
);
const stocks = stockPrices.getItems();
return (
{stocks.map(stock => (
{stock.symbol.get()}: ${stock.price.get()}
))}
);
}
`$3
Hook that subscribes to property event brokers for reactive updates.
Purpose: Simplified reactive updates for property-specific events.
API:
| Parameter | Type | Description |
|-----------|------|-------------|
|
...propBrokers | IPropEventBroker[] | Property brokers to observe |Usage Example:
`typescript
import { usePropEventRefresher } from '@zdr-tools/zdr-react';function UserSummary({ user }: { user: User }) {
// Re-render when specific properties change
usePropEventRefresher(
user.name,
user.email,
user.avatar
);
return (
{user.name.get()}
{user.email.get()}
{user.avatar.get() &&
}
);
}
`$3
Hook that subscribes to readable property brokers and returns current values with violations.
Purpose: Provides both value and validation state for reactive properties.
API:
| Parameter | Type | Description |
|-----------|------|-------------|
|
broker | IReadablePropEventBroker | Readable property broker |Returns:
[[T, OptionalViolations - Tuple of value and violationsUsage Example:
`typescript
import { useReadableEventRefresher } from '@zdr-tools/zdr-react';function ValidatedInput({ broker }: { broker: IReadablePropEventBroker }) {
const [[value, violations]] = useReadableEventRefresher(broker);
return (
value={value}
onChange={e => broker.set?.(e.target.value)}
style={{ borderColor: violations ? 'red' : 'green' }}
/>
{violations && (
{violations.map(v => v.result).join(', ')}
)}
);
}
`React Components
$3
Invisible component that triggers callbacks when it becomes visible.
Purpose: Provides visibility tracking for analytics, lazy loading triggers, and scroll-based actions.
Props:
-
onVisibilityChanged: (isVisible: boolean) => void - Callback when visibility changes
- triggerVisibleOnlyOnce?: boolean - Only trigger once (default: true)
- dataAid?: string - Data attribute for testing
- className?: string - CSS class nameUsage Example:
`typescript
import { VisibilityPixel } from '@zdr-tools/zdr-react';function AnalyticsSection({ sectionName }: { sectionName: string }) {
const handleVisibilityChange = useCallback((isVisible: boolean) => {
if (isVisible) {
analytics.track('section_viewed', { section: sectionName });
}
}, [sectionName]);
return (
{sectionName}
Section content...
onVisibilityChanged={handleVisibilityChange}
dataAid={
${sectionName}-visibility-tracker}
/>
);
}// Infinite scroll implementation
function InfiniteScrollList({ items, loadMore }: Props) {
const [isLoading, setIsLoading] = useState(false);
const handleLoadMore = useCallback(async (isVisible: boolean) => {
if (isVisible && !isLoading) {
setIsLoading(true);
await loadMore();
setIsLoading(false);
}
}, [isLoading, loadMore]);
return (
{items.map(item => (
))}
{!isLoading && (
onVisibilityChanged={handleLoadMore}
triggerVisibleOnlyOnce={false}
/>
)}
{isLoading && Loading more items...}
);
}
`$3
Component that prevents click event propagation to parent elements.
Purpose: Simplifies event handling in complex component hierarchies by stopping unwanted click bubbling.
Props:
-
as?: string | React.ComponentType - Component type to render (default: 'div')
- children: ReactNode - Child contentUsage Example:
`typescript
import { ClickPropagationPreventer } from '@zdr-tools/zdr-react';function Modal({ onClose, children }: { onClose: () => void; children: ReactNode }) {
return (
{children}
);
}// Dropdown menu
function DropdownMenu({ items, onItemClick }: Props) {
const [isOpen, open, close] = useOpenClose();
return (
{isOpen && (
{items.map(item => (
onItemClick(item)}>
{item.name}
))}
)}
);
}// Card with clickable content
function UserCard({ user, onUserClick, onActionClick }: Props) {
return (
onUserClick(user)}>
{user.name}
{user.email}
);
}
`$3
Component that provides 100vh height with mobile browser compatibility.
Purpose: Handles mobile browser viewport height issues by providing true 100vh experience.
Usage Example:
`typescript
import { ReactDiv100Vh } from '@zdr-tools/zdr-react';function FullScreenApp() {
return (
App Header
);
}
// Mobile-friendly modal
function FullScreenModal({ children }: { children: ReactNode }) {
return (
{children}
);
}
`Advanced Usage Patterns
$3
`typescript
import {
usePropBrokerValue,
useTextField,
useEventRefresher
} from '@zdr-tools/zdr-react';function EntityForm({ entity, onSave }: Props) {
// Track entity changes
useEventRefresher(entity.propertyChanged);
const isValid = !entity.getChangedProps().some(change =>
change.changedProps.some(prop => prop.violations)
);
const hasChanges = entity.isChanged();
const handleSave = useCallback(async () => {
if (isValid && hasChanges) {
await onSave(entity);
entity.commit();
}
}, [entity, onSave, isValid, hasChanges]);
const handleReset = useCallback(() => {
entity.restore();
}, [entity]);
return (
);
}
`$3
`typescript
function EntityCollectionManager({
collection,
renderItem
}: Props) {
useEventRefresher(
collection.itemsAdded,
collection.itemRemoved,
collection.collectionChanged
); const items = collection.getItems();
const newItems = collection.getNewItems();
const hasChanges = collection.isChanged();
return (
Total: {items.length} | New: {newItems.length}
{hasChanges && Unsaved changes}
{items.map(item => (
{renderItem(item)}
))}
);
}
`Performance Considerations
$3
`typescript
// Use throttling for high-frequency updates
const throttledValue = usePropBrokerValue(fastChangingProperty, {
timing: { wait: 100, throttle: { leading: true, trailing: true } }
});// Use debouncing for search inputs
const searchQuery = usePropBrokerValue(searchProperty, {
timing: { wait: 300, debounce: { leading: false, trailing: true } }
});
// Selective event subscription
useEventRefresher(
entity.propertyChanged, // Only for this entity
// Don't subscribe to collection.itemChanged if not needed
);
`$3
`typescript
const EntityComponent = memo(function EntityComponent({ entity }: Props) {
const name = usePropBrokerValue(entity.name);
const email = usePropBrokerValue(entity.email); return (
{name}
{email}
);
});// Use entity ID as React key for optimal reconciliation
{entities.map(entity => (
))}
``This package provides the React layer of the ZDR framework:
- Bridges ZDR reactive entities with React's rendering system
- Provides performance optimizations through throttling and debouncing
- Maintains automatic subscription cleanup and memory management
- Enables reactive forms, collections, and real-time updates
- Supports modern React patterns like hooks and concurrent features
The React integration allows developers to build highly reactive user interfaces that automatically stay in sync with ZDR entity state changes while maintaining optimal performance.