Modern reactive systems face a fundamental challenge: how do you track which state values a function depends on without requiring manual dependency declarations? React solves this with explicit dependency arrays in hooks, but Juris.js takes a radically different approach - automatic dependency detection that tracks state access across deep call stacks and conditional branches.
The Dependency Problem
Consider this complex component with nested function calls and conditional logic:
const UserDashboard = (props, { getState }) => {
const renderUserSection = () => {
const user = getState('auth.user');
if (!user) return { div: { text: 'Not logged in' } };
const permissions = getState('auth.permissions', []);
if (permissions.includes('admin')) {
const adminStats = getState('admin.stats');
return renderAdminPanel(adminStats);
}
return renderUserPanel(user);
};
const renderAdminPanel = (stats) => {
const notifications = getState('admin.notifications', []);
const criticalAlerts = getState('system.alerts.critical', []);
if (stats.serverLoad > 0.8) {
const serverMetrics = getState('monitoring.servers');
return renderCriticalView(serverMetrics, criticalAlerts);
}
return renderNormalAdminView(notifications);
};
return {
div: {
class: 'dashboard',
children: renderUserSection
}
};
};
This component's dependencies vary dramatically based on:
- Whether the user is logged in
- Whether they have admin permissions
- Whether server load is critical
- Which execution path the code takes
React would require you to manually track all possible dependencies. Juris automatically detects them.
How Automatic Detection Works
Juris uses a tracking context that captures getState()
calls regardless of where they occur in the call stack:
track(fn, isolated = false) {
let saved = this.deps;
let deps = isolated ? null : (this.deps = new Set());
let result;
try {
result = fn(); // Execute the tracked function
} finally {
this.deps = saved; // Restore previous tracking context
}
return { result, deps: deps ? [...deps] : [] };
}
getState(path, defaultValue = null, track = true) {
if (track && this.deps) this.deps.add(path); // Capture dependency
// ... get value from state tree
}
When a reactive function executes, Juris:
- Sets up a tracking context (
this.deps = new Set()
) - Executes the function and all its nested calls
- Any
getState()
call automatically adds its path to the tracking set - Returns both the result and discovered dependencies
Deep Call Stack Tracking
The tracking persists across function boundaries. Consider this execution:
// Initial render - tracking context active
const renderUserSection = () => {
const user = getState('auth.user'); // Tracked: 'auth.user'
if (user.role === 'admin') {
return renderAdminPanel(); // Nested call maintains tracking
}
return renderUserPanel();
};
const renderAdminPanel = () => {
const stats = getState('admin.stats'); // Tracked: 'admin.stats'
const alerts = getState('system.alerts'); // Tracked: 'system.alerts'
return processAdminData(stats, alerts); // Even deeper nesting
};
const processAdminData = (stats, alerts) => {
if (stats.criticalIssues > 0) {
const escalation = getState('admin.escalation.rules'); // Tracked: 'admin.escalation.rules'
return handleCriticalFlow(escalation);
}
return handleNormalFlow();
};
All getState()
calls, regardless of nesting depth, are captured in the same dependency set. The component automatically subscribes to:
auth.user
-
admin.stats
system.alerts
-
admin.escalation.rules
(only if critical issues exist)
Branch-Aware Detection
The tracking is execution-path sensitive. Dependencies only register when code actually runs:
const ConditionalComponent = (props, { getState }) => {
return {
div: {
children: () => {
const userType = getState('auth.userType'); // Always tracked
if (userType === 'premium') {
const premiumFeatures = getState('features.premium'); // Only tracked if premium
return renderPremiumUI(premiumFeatures);
}
if (userType === 'admin') {
const adminTools = getState('admin.tools'); // Only tracked if admin
const systemHealth = getState('system.health'); // Only tracked if admin
return renderAdminUI(adminTools, systemHealth);
}
const basicFeatures = getState('features.basic'); // Only tracked if neither premium nor admin
return renderBasicUI(basicFeatures);
}
}
};
};
Dependencies change based on execution path:
-
Premium user: subscribes to
auth.userType
,features.premium
-
Admin user: subscribes to
auth.userType
,admin.tools
,system.health
-
Basic user: subscribes to
auth.userType
,features.basic
The component only subscribes to state paths it actually accesses during execution.
Dynamic Dependency Evolution
Dependencies can change on each render as execution paths shift:
const AdaptiveComponent = (props, { getState }) => {
return {
div: {
children: () => {
const mode = getState('app.mode'); // Always tracked
const data = getState('app.data', []); // Always tracked
// Dependencies change based on current mode
switch (mode) {
case 'chart':
const chartConfig = getState('ui.chart.config'); // Only when mode=chart
const colorTheme = getState('ui.theme.colors'); // Only when mode=chart
return renderChart(data, chartConfig, colorTheme);
case 'table':
const sortOrder = getState('ui.table.sortOrder'); // Only when mode=table
const filters = getState('ui.table.filters'); // Only when mode=table
return renderTable(data, sortOrder, filters);
case 'export':
const exportFormat = getState('export.format'); // Only when mode=export
const exportOptions = getState('export.options'); // Only when mode=export
return renderExportDialog(data, exportFormat, exportOptions);
default:
return { div: { text: 'Unknown mode' } };
}
}
}
};
};
When app.mode
changes from 'chart' to 'table', the component:
- Unsubscribes from chart-related paths
- Re-executes the function with tracking
- Discovers new table-related dependencies
- Subscribes to the new paths
Subscription Management
The system automatically manages subscription lifecycles:
#triggerPathSubscribers(path) {
let subs = this.subscribers.get(path);
if (subs && subs.size > 0) {
new Set(subs).forEach(callback => {
try {
// Re-execute with fresh tracking
let { deps } = this.track(() => callback());
// Subscribe to newly discovered dependencies
deps.forEach(newPath => {
let existingSubs = this.subscribers.get(newPath);
if (!existingSubs || !existingSubs.has(callback)) {
this.subscribeInternal(newPath, callback);
}
});
} catch (error) {
console.error('Subscriber error:', error);
}
});
}
}
On each reactive update:
- Function re-executes with fresh dependency tracking
- New dependencies are automatically subscribed
- Old dependencies naturally fade away when no longer accessed
- No manual cleanup required
Performance Implications
This automatic system has interesting performance characteristics:
Advantages:
- No manual dependency management overhead
- Only subscribes to actually accessed state
- Automatically optimizes for current execution paths
- Self-corrects when code paths change
Potential Costs:
- Dependency discovery happens on every reactive update
- Tracking context adds slight overhead to
getState()
calls - Dynamic subscriptions require more complex cleanup logic
Comparison with Manual Dependency Systems
React useEffect:
// Manual dependency declaration - error prone
useEffect(() => {
if (user?.role === 'admin') {
fetchAdminData(user.id);
}
}, [user?.role, user?.id]); // Must manually track all dependencies
Juris Automatic:
// Dependencies discovered automatically
children: () => {
const user = getState('auth.user');
if (user?.role === 'admin') {
const adminData = getState(`admin.data.${user.id}`);
return renderAdminData(adminData);
}
return renderUserData();
} // All dependencies automatically tracked
Edge Cases and Limitations
Async Function Boundaries: Tracking doesn't cross async boundaries:
children: async () => {
const userId = getState('auth.userId'); // Tracked
const userData = await fetchUser(userId);
const settings = getState('user.settings'); // NOT tracked - async boundary crossed
return renderUser(userData, settings);
}
Conditional Tracking: Only tracks paths actually accessed:
children: () => {
const shouldCheck = getState('app.enableAdvancedMode');
// This getState() only tracked if shouldCheck is true
const advancedData = shouldCheck ? getState('advanced.data') : null;
return renderUI(advancedData);
}
Function Identity: Functions are re-executed on every dependency change, so expensive computations should be memoized elsewhere.
Implementation Benefits
This approach enables several powerful patterns:
Self-Healing Dependencies: Components automatically adapt when code changes
Zero-Config Reactivity: No dependency arrays to maintain
Path-Sensitive Updates: Components only update when their actual dependencies change
Deep Integration: Works seamlessly with complex nested function calls
When This Approach Excels
Automatic dependency detection particularly benefits:
Complex Conditional Logic: Applications with many branching code paths
Dynamic User Interfaces: UIs that change behavior based on state
Deeply Nested Components: Components with complex internal logic
Rapid Development: Scenarios where manual dependency management slows development
Conclusion
Juris.js's automatic dependency detection represents a fundamental shift from explicit dependency management to implicit discovery. By tracking getState()
calls across deep call stacks and conditional branches, it eliminates a entire class of reactivity bugs while adapting to changing code paths automatically.
This approach trades some predictability for significant developer experience improvements, particularly in complex interactive applications where manual dependency tracking becomes error-prone and maintenance-heavy.
The system's ability to discover dependencies dynamically based on actual execution paths makes it particularly well-suited for applications with complex state-dependent logic that would be difficult to manage with manual dependency arrays.
Visit https://jurisjs.com to learn more.
Star them on Github: https://github.com/jurisjs/juris
Top comments (0)