DEV Community

Professional Joe
Professional Joe

Posted on

Juris.js: Debug What You Wrote

Modern JavaScript frameworks often create a disconnect between the code you write and what actually runs in the browser. JSX transformations, build steps, and runtime abstractions can make debugging feel like archeology - digging through layers of generated code to find your original logic. Juris.js takes a different approach: what you write is what executes.

The Transformation Problem

Most modern frameworks transform your code before it reaches the browser:

React/JSX:

// What you write const Button = ({ onClick, children }) => { return <button onClick={onClick}>{children}</button>; }; // What actually runs (simplified) const Button = ({ onClick, children }) => { return React.createElement('button', { onClick }, children); }; 
Enter fullscreen mode Exit fullscreen mode

Vue Single File Components:

<!-- What you write --> <template> <button @click="handleClick">{{ text }}</button> </template> <!-- Gets compiled to render functions and setup code --> 
Enter fullscreen mode Exit fullscreen mode

Svelte:

// What you write let count = 0; $: doubled = count * 2; // Gets compiled to reactive update mechanisms 
Enter fullscreen mode Exit fullscreen mode

When you set a breakpoint in these transformed environments, you're debugging generated code. Source maps help, but they're an approximation - a best-effort mapping between transformed output and your original source.

Juris: Direct Execution

Juris components run exactly as written:

const UserProfile = (props, { getState, setState }) => { const user = getState('user.profile', null); const isEditing = getState('ui.editing', false); const toggleEdit = () => setState('ui.editing', !isEditing); return { div: { class: 'user-profile', children: [ { h2: { text: user?.name || 'Unknown User' } }, { button: { text: isEditing ? 'Save' : 'Edit', onclick: toggleEdit } } ] } }; }; 
Enter fullscreen mode Exit fullscreen mode

This code executes exactly as written. No JSX transformation, no template compilation, no build step magic. When you set a breakpoint on any line, you're debugging your actual code.

Debugging Reactive Updates

The real power shows when debugging reactive state updates. Consider this interactive table row:

const TableRow = (props, { getState, setState }) => { const { rowIndex, rowData } = props; const selectedRows = getState('ui.table.selectedRows', []); const isSelected = selectedRows.includes(rowIndex); const handleClick = (e) => { if (e.ctrlKey) { const newSelection = isSelected ? selectedRows.filter(id => id !== rowIndex) : [...selectedRows, rowIndex]; setState('ui.table.selectedRows', newSelection); } else { setState('ui.table.selectedRows', [rowIndex]); } }; return { tr: { class: () => { const selected = getState('ui.table.selectedRows', []).includes(rowIndex); const highlighted = getState('ui.table.highlightedRow') === rowIndex; let classes = 'table-row'; if (selected) classes += ' selected'; if (highlighted) classes += ' highlighted'; return classes; // <- Set breakpoint here }, onclick: handleClick, children: [ { td: { text: rowData.name } }, { td: { text: rowData.email } }, { td: { text: rowData.status } } ] } }; }; 
Enter fullscreen mode Exit fullscreen mode

Setting a breakpoint in the reactive class function shows you:

  1. Exact trigger: The call stack reveals which setState() call caused this update
  2. Current state: All variables are your original names with expected values
  3. Execution path: Step through your logic exactly as written

No framework internals, no synthetic stack frames, no transformed variable names.

Real-World Debugging Scenarios

Tracing State Changes

When debugging why a component updated unexpectedly:

const StatusIndicator = (props, { getState }) => { return { div: { class: () => { // Breakpoint here shows exact setState() that triggered update const status = getState('app.connectionStatus', 'disconnected'); const isLoading = getState('app.isLoading', false); return `status ${status} ${isLoading ? 'loading' : ''}`; }, text: () => { const status = getState('app.connectionStatus', 'disconnected'); return status === 'connected' ? '🟢 Online' : '🔴 Offline'; } } }; }; 
Enter fullscreen mode Exit fullscreen mode

The browser's call stack immediately shows whether the update came from a WebSocket handler, a retry mechanism, or user interaction. No detective work required.

Debugging Event Handlers

Event handlers are your original functions:

const SearchInput = (props, { getState, setState }) => { const handleInput = (e) => { const query = e.target.value; setState('search.query', query); // <- Breakpoint here if (query.length > 2) { setState('search.isSearching', true); // Trigger search logic } }; return { input: { type: 'text', value: () => getState('search.query', ''), oninput: handleInput, placeholder: 'Search users...' } }; }; 
Enter fullscreen mode Exit fullscreen mode

Setting a breakpoint in handleInput lets you inspect the exact event object, step through your logic, and watch state changes propagate - all in the code you actually wrote.

Complex State Dependencies

For components with multiple reactive dependencies:

const Dashboard = (props, { getState }) => { return { div: { class: 'dashboard', children: () => { // Breakpoint here reveals which dependency changed const user = getState('auth.user', null); const notifications = getState('notifications.items', []); const theme = getState('ui.theme', 'light'); const isOnline = getState('app.connectionStatus') === 'connected'; if (!user) return [{ div: { text: 'Please log in' } }]; if (!isOnline) return [{ div: { text: 'Connecting...' } }]; return [ { UserHeader: { user, theme } }, { NotificationsList: { notifications } }, { MainContent: { user } } ]; } } }; }; 
Enter fullscreen mode Exit fullscreen mode

When this component re-renders, your breakpoint immediately shows which of the four state dependencies triggered the update. The call stack traces back to the exact setState() call that started the cascade.

Browser DevTools Integration

Juris works seamlessly with standard browser debugging tools:

Console Inspection: All variables maintain their original names and values
Step Debugging: Step through your exact code logic
Watch Expressions: Watch your state paths directly: getState('user.profile.name')
Call Stack: Clear path from state change to reactive update
Performance Profiling: Profile your actual functions, not framework internals

The Mental Model Advantage

This direct execution creates a simpler mental model. When debugging, you think:

  1. "Which state change caused this update?"
  2. Set breakpoint in reactive function
  3. Call stack shows the answer

No need to understand:

  • Framework reconciliation algorithms
  • Build tool source map accuracy
  • Runtime transformation side effects
  • Synthetic event systems

Performance Debugging

Since Juris doesn't transform your code, performance debugging is straightforward:

const ExpensiveComponent = (props, { getState }) => { return { div: { children: () => { console.time('expensive-calculation'); // <- Your timing code works as expected const data = getState('app.largeDataSet', []); const processed = data.map(item => processItem(item)); // <- Breakpoint shows actual performance console.timeEnd('expensive-calculation'); return processed.map(item => ({ div: { text: item.name } })); } } }; }; 
Enter fullscreen mode Exit fullscreen mode

Performance bottlenecks appear in your code, not hidden behind framework abstractions.

Limitations and Trade-offs

This direct execution approach has constraints:

No Compile-Time Optimization: Frameworks like Svelte can optimize at compile time
Verbose Syntax: Object VDOM syntax is more verbose than JSX
Learning Curve: Different paradigm from mainstream frameworks
Ecosystem: Smaller ecosystem compared to React/Vue

However, for debugging complex interactive applications, the ability to debug exactly what you wrote provides significant advantages.

When Direct Debugging Matters

This approach particularly shines in:

Complex State Interactions: Applications with intricate state dependencies
Performance-Critical Code: When you need to optimize exact execution paths
Large Teams: When debugging others' code without framework expertise
Learning Environments: Understanding reactive patterns without abstraction layers

Conclusion

Juris.js prioritizes debugging clarity by eliminating the transformation layer between your code and browser execution. While this comes with trade-offs in syntax and ecosystem, it provides unparalleled debugging transparency.

When you set a breakpoint in a Juris application, you're debugging the code you wrote, with the variable names you chose, executing the logic you designed. No transformations, no approximations, no framework archaeology required.

For developers who value debugging clarity and direct control over their code execution, Juris offers a refreshing alternative to the transformation-heavy landscape of modern frontend development.

Juris.JS is open source and actively evaluated by a growing community of developers, entrepreneurs, and enterprises who believe the future of software development is visual, collaborative, and powered by pure data structures that render as functional applications.

Visit https://jurisjs.com to learn more.
Star them on Github: https://github.com/jurisjs/juris

Top comments (0)