Coherent.js Migration Guide
This guide helps developers migrate from traditional frameworks (React, Vue, etc.) or template engines (Handlebars, EJS, etc.) to Coherent.js.
From React
Component Structure
React JSX:
function Greeting({ name }) {
return (
<div className="greeting">
<h1>Hello, {name}!</h1>
</div>
);
}
Coherent.js Object:
function Greeting({ name }) {
return {
div: {
className: 'greeting',
children: [
{ h1: { text: `Hello, ${name}!` } }
]
}
};
}
State Management
React with useState:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Coherent.js with withState:
import { withState } from 'coherent-js';
const Counter = withState({ count: 0 })(({ state, setState }) => ({
div: {
children: [
{ p: { text: `Count: ${state.count}` } },
{
button: {
text: 'Increment',
onclick: () => setState({ count: state.count + 1 })
}
}
]
}
}));
Conditional Rendering
React:
function UserProfile({ user }) {
return (
<div>
{user ? (
<p>Welcome, {user.name}!</p>
) : (
<p>Please log in</p>
)}
</div>
);
}
Coherent.js:
import { when } from 'coherent-js';
function UserProfile({ user }) {
return {
div: {
children: [
when(user,
{ p: { text: `Welcome, ${user.name}!` } },
{ p: { text: 'Please log in' } }
)
]
}
};
}
List Rendering
React:
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : 'pending'}>
{todo.text}
</li>
))}
</ul>
);
}
Coherent.js:
import { forEach } from 'coherent-js';
function TodoList({ todos }) {
return {
ul: {
children: forEach(todos, (todo) => ({
li: {
text: todo.text,
className: todo.completed ? 'completed' : 'pending'
}
}))
}
};
}
From Template Engines (Handlebars, EJS, etc.)
Basic Template
Handlebars:
<div class="greeting">
<h1>Hello, {{name}}!</h1>
<p>You have {{notifications}} notifications</p>
</div>
Coherent.js:
function Greeting({ name, notifications }) {
return {
div: {
className: 'greeting',
children: [
{ h1: { text: `Hello, ${name}!` } },
{ p: { text: `You have ${notifications} notifications` } }
]
}
};
}
Conditional Blocks
Handlebars:
{{#if user}}
<p>Welcome, {{user.name}}!</p>
{{else}}
<p>Please log in</p>
{{/if}}
Coherent.js:
import { when } from 'coherent-js';
function UserProfile({ user }) {
return {
div: {
children: [
when(user,
{ p: { text: `Welcome, ${user.name}!` } },
{ p: { text: 'Please log in' } }
)
]
}
};
}
Loops
Handlebars:
<ul>
{{#each todos}}
<li class="{{#if completed}}completed{{else}}pending{{/if}}">
{{text}}
</li>
{{/each}}
</ul>
Coherent.js:
import { forEach } from 'coherent-js';
function TodoList({ todos }) {
return {
ul: {
children: forEach(todos, (todo) => ({
li: {
text: todo.text,
className: todo.completed ? 'completed' : 'pending'
}
}))
}
};
}
From String Concatenation
Basic HTML Generation
String Concatenation:
function createGreeting(name) {
return `<div class="greeting">
<h1>Hello, ${name}!</h1>
</div>`;
}
Coherent.js:
function Greeting({ name }) {
return {
div: {
className: 'greeting',
children: [
{ h1: { text: `Hello, ${name}!` } }
]
}
};
}
// Render with:
import { renderToString } from 'coherent-js';
const html = renderToString(Greeting({ name: 'World' }));
Client-Side Hydration for Interactive Components
One of the most critical aspects when migrating from client-side frameworks is understanding how to make server-rendered components interactive in the browser.
From React Hydration to Coherent.js Hydration
React with Next.js:
// pages/counter.js
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// Client-side hydration happens automatically
Coherent.js with Manual Hydration:
// Server-side component (components/Counter.js)
import { withState } from '../../../src/coherent.js';
const CounterComponent = withState({ count: 0 }, { debug: true });
const CounterView = (props) => {
const { state, stateUtils } = props;
const { setState } = stateUtils;
const increment = () => {
setState({ count: state.count + 1 });
};
return {
div: {
'data-coherent-component': 'counter',
children: [
{ p: { text: `Count: ${state.count}` } },
{
button: {
id: 'increment-btn',
text: 'Increment',
onclick: increment // Becomes data-action attribute
}
}
]
}
};
};
export const Counter = CounterComponent(CounterView);
// Client-side hydration (hydration.js)
import { hydrate, autoHydrate } from '@coherentjs/client';
import { Counter } from './components/Counter.js';
document.addEventListener('DOMContentLoaded', () => {
autoHydrate({
counter: Counter
});
});
Key Hydration Concepts for Migrants
1. Event Handler Serialization
In React: Event handlers stay as functions, hydration reconnects them automatically.
In Coherent.js: Event handlers become data-action
attributes during SSR:
<!-- Server renders: -->
<button data-action="__coherent_action_123_abc" data-event="click">Increment</button>
<!-- Client hydration reconnects the original function -->
2. Component Identification
React: Uses component names and React's reconciliation algorithm.
Coherent.js: Uses explicit data-coherent-component
attributes:
{
div: {
'data-coherent-component': 'my-component', // Required for hydration
children: [...]
}
}
3. State Initialization
React with Next.js:
function MyComponent({ initialData }) {
const [state, setState] = useState(initialData);
// Hydration matches server state automatically
}
Coherent.js:
// Method 1: Extract from DOM
const initialState = extractStateFromDOM(element);
hydrate(element, Component, props, { initialState });
// Method 2: Pass through props
const Component = withState(serverState)(View);
Migration Pattern: Interactive Forms
From React Form:
function ContactForm() {
const [formData, setFormData] = useState({ name: '', email: '' });
const handleSubmit = async (e) => {
e.preventDefault();
await submitForm(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
/>
<button type="submit">Submit</button>
</form>
);
}
To Coherent.js with Hydration:
// Server-side component
const ContactFormComponent = withState({
name: '',
email: '',
isSubmitting: false
});
const ContactFormView = ({ state, stateUtils }) => {
const { setState } = stateUtils;
const handleSubmit = async (event) => {
event.preventDefault();
setState({ isSubmitting: true });
const formData = new FormData(event.target);
await submitForm(Object.fromEntries(formData));
setState({ isSubmitting: false, name: '', email: '' });
};
const updateName = (event) => {
setState({ name: event.target.value });
};
return {
form: {
'data-coherent-component': 'contact-form',
onsubmit: handleSubmit, // Becomes data-action
children: [
{
input: {
name: 'name',
value: state.name,
oninput: updateName, // Becomes data-action
placeholder: 'Your name'
}
},
{
button: {
type: 'submit',
text: state.isSubmitting ? 'Submitting...' : 'Submit',
disabled: state.isSubmitting
}
}
]
}
};
};
export const ContactForm = ContactFormComponent(ContactFormView);
// Client-side hydration
document.addEventListener('DOMContentLoaded', () => {
autoHydrate({
'contact-form': ContactForm
});
});
Common Hydration Migration Issues
Issue 1: "My buttons don't work after migration"
Cause: Missing hydration setup or timing issues.
Solution:
// Ensure proper timing
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
// Small delay ensures all scripts are loaded
autoHydrate(componentRegistry);
}, 100);
});
Issue 2: "State changes don't update the UI"
Cause: Not using setState
properly or missing component wrapper.
React pattern:
const [count, setCount] = useState(0);
setCount(count + 1); // React handles re-rendering
Coherent.js pattern:
const { setState } = stateUtils;
setState({ count: state.count + 1 }); // Must use setState for reactivity
Issue 3: "Hydration mismatch warnings"
Cause: Server and client render differently.
Solution: Ensure identical props and handle client-only content:
const isClient = typeof window !== 'undefined';
return {
div: {
children: [
{ p: { text: 'Always rendered' } },
// Client-only content after hydration
isClient ? { p: { text: new Date().toISOString() } } : null
].filter(Boolean)
}
};
Advanced Hydration Patterns
Selective Hydration (Performance Optimization)
// Only hydrate interactive components
document.addEventListener('DOMContentLoaded', () => {
const interactiveComponents = document.querySelectorAll('[data-interactive="true"]');
if (interactiveComponents.length > 0) {
// Lazy load hydration code
import('./full-hydration.js').then(({ initializeHydration }) => {
initializeHydration();
});
}
});
Progressive Enhancement Pattern
// Server-side: Works without JavaScript
{
form: {
action: '/api/submit', // Fallback for no-JS
method: 'POST',
onsubmit: enhancedSubmit, // Enhanced with hydration
children: [
{ input: { name: 'email', required: true } },
{ button: { type: 'submit', text: 'Submit' } }
]
}
}
// Client-side: Enhanced functionality
const enhancedSubmit = async (event) => {
event.preventDefault(); // Override default form submission
// Add loading states, validation, etc.
const formData = new FormData(event.target);
await submitWithEnhancements(formData);
};
Key Benefits of Migrating to Coherent.js
- Universal Rendering: Same components work on server and client
- Type Safety: Full TypeScript support with built-in type definitions
- Performance: Built-in performance monitoring and optimization
- Security: Automatic HTML escaping and XSS protection
- No Build Step: Pure JavaScript with no compilation required
- Progressive Enhancement: Forms and interactions work without JavaScript
- Streaming: Native support for streaming large documents
- Memory Efficiency: Smart caching and object pooling
Migration Checklist
Server-Side Migration
- Identify components that need to be converted
- Convert JSX/components to Coherent.js object structure
- Replace state management with
withState
- Replace conditional rendering with
when
- Replace list rendering with
forEach
- Update event handling (onclick, etc.)
- Add
data-coherent-component
attributes for interactive components - Test server-side rendering output matches original
Client-Side Hydration Setup
- Install
@coherentjs/client
package (when available) - Create hydration entry point (
hydration.js
) - Set up component registry for
autoHydrate
- Add hydration script to HTML pages
- Handle timing with
DOMContentLoaded
events - Test that interactive features work after hydration
- Verify no hydration mismatch warnings
- Add error handling for hydration failures
Testing and Optimization
- Verify performance improvements
- Test progressive enhancement (works without JS)
- Implement selective hydration for performance
- Update build/deployment processes
- Add debugging for development environment
- Document hydration patterns for team
Common Patterns
Event Handling
Before (React):
<button onClick={handleClick}>Click me</button>
After (Coherent.js):
{ button: { text: 'Click me', onclick: handleClick } }
Styling
Before (React with className):
<div className="container highlighted">Content</div>
After (Coherent.js):
{ div: { className: 'container highlighted', text: 'Content' } }
Data Attributes
Before (React):
<div data-id="123" data-role="button">Content</div>
After (Coherent.js):
{ div: { 'data-id': '123', 'data-role': 'button', text: 'Content' } }
This migration guide should help you transition your existing codebase to Coherent.js while maintaining functionality and improving performance and security.