DEV Community

Professional Joe
Professional Joe

Posted on

Juris.js: Deep Call Stack Branch Aware Automatic Dependency Detection

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
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

When a reactive function executes, Juris:

  1. Sets up a tracking context (this.deps = new Set())
  2. Executes the function and all its nested calls
  3. Any getState() call automatically adds its path to the tracking set
  4. 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();
};
Enter fullscreen mode Exit fullscreen mode

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);
      }
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

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' } };
        }
      }
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

When app.mode changes from 'chart' to 'table', the component:

  1. Unsubscribes from chart-related paths
  2. Re-executes the function with tracking
  3. Discovers new table-related dependencies
  4. 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);
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

On each reactive update:

  1. Function re-executes with fresh dependency tracking
  2. New dependencies are automatically subscribed
  3. Old dependencies naturally fade away when no longer accessed
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)