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); };
Vue Single File Components:
<!-- What you write --> <template> <button @click="handleClick">{{ text }}</button> </template> <!-- Gets compiled to render functions and setup code -->
Svelte:
// What you write let count = 0; $: doubled = count * 2; // Gets compiled to reactive update mechanisms
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 } } ] } }; };
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 } } ] } }; };
Setting a breakpoint in the reactive class
function shows you:
- Exact trigger: The call stack reveals which
setState()
call caused this update - Current state: All variables are your original names with expected values
- 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'; } } }; };
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...' } }; };
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 } } ]; } } }; };
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:
- "Which state change caused this update?"
- Set breakpoint in reactive function
- 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 } })); } } }; };
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)