Juris.js introduces a revolutionary approach to reactivity that's both intuitive and powerful. Unlike other frameworks that require complex dependency tracking systems or explicit subscription mechanisms, Juris uses a simple yet elegant distinction: functions create reactivity, values don't.
This article will walk you through this core concept, from basic understanding to advanced patterns, helping you master one of Juris.js's most innovative features.
The Foundation: Static vs Reactive
In Juris.js, whether something updates automatically depends on a simple question: Is it wrapped in a function?
Static Values (No Updates)
When you return a direct value from getState()
, you get a static snapshot that never changes:
const app = new Juris();
app.enhance('#counter', (props, { getState }) => ({
textContent: getState('count'), // Static! Gets current value once
className: 'btn-' + getState('theme') // Also static
}));
No matter how many times count
or theme
changes in your application state, this element will display the values that existed when the enhancement first ran.
Reactive Values (Auto-Updates)
Wrap the same code in a function, and it becomes reactive:
app.enhance('#counter', (props, { getState }) => ({
textContent: () => getState('count'), // Reactive! Updates when count changes
className: () => 'btn-' + getState('theme') // Reactive! Updates when theme changes
}));
Now your element automatically updates whenever count
or theme
changes. The function wrapper is what creates the subscription.
Real-World Example: Building a User Dashboard
Let's build a user dashboard to see this in action:
// Set up some initial state
const app = new Juris();
app.setState('user.name', 'John Doe');
app.setState('user.status', 'online');
app.setState('notifications.count', 3);
app.setState('theme.mode', 'dark');
// Static approach - values frozen in time
app.enhance('#static-dashboard', (props, { getState }) => ({
innerHTML: `
<h1>Welcome ${getState('user.name')}</h1>
<span class="status ${getState('user.status')}">
${getState('user.status')}
</span>
<div class="notifications">
${getState('notifications.count')} notifications
</div>
`
}));
// Reactive approach - updates automatically
app.enhance('#reactive-dashboard', (props, { getState }) => ({
innerHTML: () => `
<h1>Welcome ${getState('user.name')}</h1>
<span class="status ${getState('user.status')}">
${getState('user.status')}
</span>
<div class="notifications">
${getState('notifications.count')} notifications
</div>
`,
className: () => `dashboard ${getState('theme.mode')}-theme`
}));
When you later update the state:
app.setState('user.name', 'Jane Smith');
app.setState('notifications.count', 7);
app.setState('theme.mode', 'light');
The static dashboard remains unchanged, but the reactive dashboard automatically updates to reflect the new values.
Granular Reactivity: Mix and Match
Here's where Juris really shines - you can choose exactly which parts should be reactive:
app.enhance('#mixed-example', (props, { getState, setState }) => ({
// Static: Set once when component initializes
id: 'user-' + getState('user.id'),
// Reactive: Updates when user name changes
textContent: () => getState('user.name'),
// Reactive: Dynamic styling based on status
style: () => ({
backgroundColor: getState('user.status') === 'online' ? '#22c55e' : '#ef4444',
opacity: getState('user.isActive') ? 1 : 0.5
}),
// Static: Event handler doesn't need to change
onClick: () => {
setState('user.status', 'away');
}
}));
This gives you fine-grained control over performance - only the parts that actually need to update will re-execute.
Advanced Pattern: Computed Values
You can create computed values that depend on multiple state paths:
app.enhance('#computed-example', (props, { getState }) => ({
textContent: () => {
const user = getState('user');
const settings = getState('settings');
const lastSeen = getState('activity.lastSeen');
// Complex computation that automatically updates
// when any dependency changes
if (user.status === 'online') {
return `${user.name} is online`;
} else {
const timeAgo = Date.now() - lastSeen;
return `${user.name} was last seen ${formatTimeAgo(timeAgo)}`;
}
},
className: () => {
const status = getState('user.status');
const priority = getState('user.priority');
return `user-card ${status} priority-${priority}`;
}
}));
Juris automatically tracks all the state paths accessed within these functions and updates only when necessary.
Performance Benefits
This approach offers significant performance advantages:
1. No Unnecessary Re-renders
app.enhance('#efficient-component', (props, { getState }) => ({
// Only updates when 'title' changes, ignoring other state changes
textContent: () => getState('title'),
// Only updates when 'theme' changes
className: () => `component ${getState('theme')}`,
// Static - never re-executes
id: 'component-' + getState('componentId')
}));
2. Surgical Updates
Unlike frameworks that re-render entire components, Juris updates only the specific attributes that have changed:
// When 'user.name' changes, only textContent updates
// When 'theme.color' changes, only style updates
// The onClick handler never re-executes
app.enhance('#surgical-updates', (props, { getState, setState }) => ({
textContent: () => getState('user.name'),
style: () => ({ color: getState('theme.color') }),
onClick: () => setState('clicked', true)
}));
Common Patterns and Best Practices
When to Use Static Values
- IDs and data attributes that shouldn't change
- Event handlers (unless they need to close over changing values)
- Initial configuration values
app.enhance('#best-practices', (props, { getState, setState }) => ({
// Static: Component ID never changes
id: 'component-' + getState('componentId'),
// Static: Data attributes for CSS selectors
'data-type': getState('component.type'),
// Reactive: Visual content that should update
textContent: () => getState('content.title'),
// Static: Event handler doesn't need state closure
onClick: () => {
setState('interactions.clicks', getState('interactions.clicks') + 1);
}
}));
When to Use Reactive Functions
- Visual content (text, HTML)
- Dynamic styling
- Conditional attributes
- Computed values
app.enhance('#reactive-patterns', (props, { getState }) => ({
// Reactive: Content changes
innerHTML: () => getState('content.html'),
// Reactive: Conditional classes
className: () => {
const classes = ['base'];
if (getState('state.isActive')) classes.push('active');
if (getState('state.isSelected')) classes.push('selected');
return classes.join(' ');
},
// Reactive: Dynamic styles
style: () => ({
transform: `scale(${getState('ui.scale')})`,
opacity: getState('ui.visible') ? 1 : 0
})
}));
Debugging Reactivity
Understanding what's reactive vs static helps with debugging:
app.enhance('#debug-example', (props, { getState }) => ({
// This will log once and never again (static)
textContent: console.log('Static evaluation:', getState('debug.value')) || getState('debug.value'),
// This will log every time debug.value changes (reactive)
className: () => {
console.log('Reactive evaluation:', getState('debug.value'));
return `debug-${getState('debug.value')}`;
}
}));
Conclusion
Juris.js's static vs reactive distinction is both powerful and intuitive:
- Values = Static snapshots
- Functions = Reactive subscriptions
This simple rule gives you:
- Predictable behavior - Clear when things update
- Performance control - Choose what should be reactive
- Clean syntax - No complex subscription APIs
- Fine-grained updates - Only what changed gets updated
The beauty of this approach is its simplicity. You don't need to learn complex reactivity systems, dependency arrays, or subscription patterns. Just ask yourself: "Should this update when state changes?" If yes, wrap it in a function. If no, use the value directly.
This innovative approach to reactivity makes Juris.js unique among JavaScript frameworks, providing both the power of reactive programming and the simplicity that developers crave.
Top comments (1)
Thank you :-)