-
Notifications
You must be signed in to change notification settings - Fork 8
Headless Components ‐ Complete Developer Guide
Traditional Components:
- Tightly coupled to DOM structure
- Mix logic with presentation
- Limited reusability across different UIs
Juris Headless Components:
- Pure logic and state management
- Complete separation of concerns
- Universal reusability across any UI framework
- Cross-platform compatibility
While headless components and services may seem similar, they serve different architectural purposes in Juris:
Definition: Reactive, stateful entities that participate in the component lifecycle and state management system.
Characteristics:
- Lifecycle Management: Full lifecycle hooks (onRegister, onUpdate, onUnregister)
- State Integration: Deep integration with Juris's reactive state system
- Context Injection: Automatic injection into component contexts
- Instance Management: Managed instances with cleanup handling
- Reactive Dependencies: Automatic dependency tracking and updates
// Headless Component Example const userProfileComponent = (props, context) => { const { getState, setState, subscribe } = context; // Reactive state management const profileData = () => getState('user.profile', {}); // Subscribe to changes const unsubscribe = subscribe('user.profile', (newProfile) => { // React to profile changes validateProfile(newProfile); }); return { api: { updateProfile: async (data) => { setState('user.profile.updating', true); try { const updated = await updateUserProfile(data); setState('user.profile', updated); setState('user.profile.lastUpdated', Date.now()); } finally { setState('user.profile.updating', false); } }, getProfile: () => profileData(), isUpdating: () => getState('user.profile.updating', false) }, hooks: { onRegister: () => { // Initialize profile data loadInitialProfile(); }, onUnregister: () => { // Cleanup subscriptions unsubscribe(); } } }; };
Definition: Stateless utility functions or objects that provide specific functionality without lifecycle management.
Characteristics:
- Stateless Operations: Pure functions or utility objects
- Direct Injection: Provided via the services configuration
- No Lifecycle: No managed lifecycle or cleanup
- Immediate Availability: Available from framework initialization
- Functional Approach: Focus on operations rather than state
// Service Example class HttpService { constructor(config) { this.baseURL = config.baseURL; this.defaultHeaders = config.headers || {}; } async get(endpoint, options = {}) { return this.request('GET', endpoint, null, options); } async post(endpoint, data, options = {}) { return this.request('POST', endpoint, data, options); } async request(method, endpoint, data, options) { const url = `${this.baseURL}${endpoint}`; const headers = { ...this.defaultHeaders, ...options.headers }; const response = await fetch(url, { method, headers, body: data ? JSON.stringify(data) : undefined, ...options }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); } } // Usage in Juris configuration const juris = new Juris({ services: { http: new HttpService({ baseURL: '/api', headers: { 'Content-Type': 'application/json' } }), logger: new LoggerService(), validator: new ValidationService() } });
Use Headless Components When:
- You need reactive state management
- Component lifecycle management is required
- You want automatic dependency tracking
- The functionality should be lazily initialized
- You need cleanup on unmount
- The logic is stateful and evolving
- Grouping multi-domain functionality that handles the same data
// Good headless component use case: Multi-domain user management const UserDomainComponent = (props, context) => { const { getState, setState, subscribe, httpClient } = context; // All user-related functionality grouped together return { api: { // Authentication domain login: async (credentials) => { setState('user.auth.loading', true); try { const response = await httpClient.post('/auth/login', credentials); const tokens = response.data; setState('user.auth.tokens', tokens); setState('user.auth.isAuthenticated', true); setState('user.auth.lastLogin', Date.now()); } finally { setState('user.auth.loading', false); } }, logout: () => { setState('user.auth.tokens', null); setState('user.auth.isAuthenticated', false); setState('user.profile', null); setState('user.preferences', {}); }, // Profile management domain updateProfile: async (profileData) => { setState('user.profile.updating', true); try { const response = await httpClient.put('/user/profile', profileData); const updated = response.data; setState('user.profile.data', updated); setState('user.profile.lastUpdated', Date.now()); // Cross-domain effect: update auth user info const currentAuth = getState('user.auth.tokens'); if (currentAuth) { setState('user.auth.userInfo', { name: updated.name, email: updated.email }); } } finally { setState('user.profile.updating', false); } }, // Preferences domain updatePreferences: async (preferences) => { setState('user.preferences.updating', true); try { const response = await httpClient.put('/user/preferences', preferences); const updated = response.data; setState('user.preferences.data', updated); // Cross-domain effect: apply theme preference if (updated.theme) { setState('app.theme', updated.theme); } } finally { setState('user.preferences.updating', false); } }, // Notifications domain markNotificationRead: (notificationId) => { const notifications = getState('user.notifications', []); const updated = notifications.map(n => n.id === notificationId ? { ...n, read: true } : n ); setState('user.notifications', updated); setState('user.notifications.unreadCount', updated.filter(n => !n.read).length ); }, // Cross-domain data access getUserData: () => ({ auth: getState('user.auth', {}), profile: getState('user.profile.data', {}), preferences: getState('user.preferences.data', {}), notifications: getState('user.notifications', []), isAuthenticated: getState('user.auth.isAuthenticated', false), isLoading: getState('user.profile.updating', false) || getState('user.auth.loading', false) }), // Aggregate operations across domains initializeUser: async (userId) => { setState('user.initializing', true); try { // Load data from multiple domains const [profileRes, preferencesRes, notificationsRes] = await Promise.all([ httpClient.get(`/user/${userId}/profile`), httpClient.get(`/user/${userId}/preferences`), httpClient.get(`/user/${userId}/notifications`) ]); // Set all related data atomically setState('user.profile.data', profileRes.data); setState('user.preferences.data', preferencesRes.data); setState('user.notifications', notificationsRes.data); setState('user.notifications.unreadCount', notificationsRes.data.filter(n => !n.read).length ); setState('user.initialized', true); } finally { setState('user.initializing', false); } } }, hooks: { onRegister: () => { // Subscribe to cross-domain changes subscribe('user.auth.isAuthenticated', (isAuth) => { if (!isAuth) { // Clear all user data when logged out setState('user.profile', null); setState('user.preferences', {}); setState('user.notifications', []); } }); } } }; };
Use Services When:
- You need pure utility functions
- No state management is required
- The functionality is stateless
- You want immediate availability
- The operations are computational or transformative
// Good service use case: Data transformation utilities class DataTransformService { normalize(data, schema) { // Pure transformation logic return this.applySchema(data, schema); } validate(data, rules) { // Stateless validation return this.checkRules(data, rules); } sanitize(input, options = {}) { // Pure sanitization return this.cleanInput(input, options); } format(value, type, options = {}) { // Formatting utilities switch (type) { case 'currency': return this.formatCurrency(value, options); case 'date': return this.formatDate(value, options); case 'number': return this.formatNumber(value, options); default: return value; } } }
You can combine both patterns for maximum flexibility:
// Service for pure operations class UserService { validateEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } hashPassword(password) { // Pure password hashing return bcrypt.hash(password, 10); } generateUserSlug(name) { return name.toLowerCase().replace(/\s+/g, '-'); } } // Headless component for stateful user management const userManagerComponent = (props, context) => { const { getState, setState, userService } = context; return { api: { createUser: async (userData) => { // Use service for validation if (!userService.validateEmail(userData.email)) { throw new Error('Invalid email format'); } // Use component for state management setState('user.creating', true); try { const hashedPassword = await userService.hashPassword(userData.password); const slug = userService.generateUserSlug(userData.name); const newUser = { ...userData, password: hashedPassword, slug, createdAt: Date.now() }; const created = await createUserAPI(newUser); setState('user.current', created); setState('user.lastCreated', Date.now()); return created; } finally { setState('user.creating', false); } }, getCurrentUser: () => getState('user.current', null), isCreating: () => getState('user.creating', false) } }; }; // Configuration combining both const juris = new Juris({ services: { userService: new UserService() }, headlessComponents: { userManager: userManagerComponent } });
| Criteria | Headless Component | Service |
|---|---|---|
| State Management | ✅ Reactive state | ❌ Stateless |
| Lifecycle Hooks | ✅ Full lifecycle | ❌ No lifecycle |
| Dependency Tracking | ✅ Automatic | ❌ Manual |
| Lazy Loading | ✅ On-demand init | ❌ Immediate |
| Cleanup Required | ✅ Automatic | ❌ Not applicable |
| Pure Functions | ❌ Can be stateful | ✅ Encouraged |
| Performance | 🔶 Moderate overhead | ✅ Minimal overhead |
| Testability | ✅ Easy to test | ✅ Easy to test |
| Reusability | 🔶 Context dependent | ✅ Highly reusable |
| Multi-Domain Coordination | ✅ Excellent | ❌ Poor |
| Shared Data Management | ✅ Centralized | ❌ Fragmented |
| Cross-Domain Effects | ✅ Automatic | ❌ Manual coordination |
- Standard Components: DOM-rendering components with lifecycle hooks
- Headless Components: Logic-only components with exposed APIs and state management
- Services: Stateless utility functions and classes
- Hybrid Components: Components that can operate in both headless and rendering modes
The HeadlessManager is the core orchestrator for headless components:
class HeadlessManager { constructor(juris) { this.juris = juris; this.components = new Map(); // Component definitions this.instances = new Map(); // Active instances this.context = {}; // Shared context this.initQueue = new Set(); // Auto-initialization queue this.lifecycleHooks = new Map(); // Lifecycle management } }
Components can be registered in multiple ways:
Method 1: Direct Registration
headlessManager.register(name, componentFn, options = {})
Method 2: Via Juris Instance
juris.registerHeadlessComponent(name, componentFn, options = {})
Method 3: During Juris Initialization
const juris = new Juris({ headlessComponents: { componentName: { fn: componentFn, options: { autoInit: true } } } });
Parameters:
-
name: Unique identifier for the component -
componentFn: Factory function that returns component instance -
options: Configuration object with optionalautoInitflag
Every headless component is defined as a factory function:
(props, context) => { // Component logic here return { api: {}, // Public interface (required) hooks: {} // Lifecycle hooks (optional) }; }
const HeadlessComponent = (props, context) => { // Use context for state management and services const { getState, setState } = context; return { // Required: Public API api: { doSomething: () => { // Component logic here } }, // Optional: Lifecycle hooks hooks: { onRegister: () => { console.log('Component registered'); }, onUnregister: () => { console.log('Component cleanup'); } } }; };
// Simplest form - just API, no hooks const simpleHeadlessComponent = (props, context) => { const { getState, setState } = context; return { api: { getValue: () => getState('simple.value', props.defaultValue || ''), setValue: (value) => setState('simple.value', value) } // No hooks needed for simple components }; };
// Component that only provides lifecycle behavior const lifecycleOnlyComponent = (props, context) => { return { api: {}, // Empty API - component doesn't expose methods hooks: { onRegister: () => { // Set up global listeners, initialize background processes window.addEventListener('beforeunload', handleBeforeUnload); startBackgroundSync(); }, onUnregister: () => { // Cleanup global listeners window.removeEventListener('beforeunload', handleBeforeUnload); stopBackgroundSync(); } } }; };
1. Service Pattern (Stateless utilities)
const UtilityService = (props, context) => { return { api: { formatDate: (date) => new Intl.DateTimeFormat().format(date), validateEmail: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), generateId: () => Math.random().toString(36).substr(2, 9) } // No hooks needed for stateless utilities }; };
2. Manager Pattern (Stateful coordination)
const DataManager = (props, context) => { const { getState, setState, subscribe } = context; return { api: { // State accessors getData: (key) => getState(`data.${key}`), setData: (key, value) => setState(`data.${key}`, value), // Operations clearAll: () => setState('data', {}), export: () => ({ ...getState('data', {}) }) }, hooks: { onRegister: () => { // Initialize default data structure setState('data', props.initialData || {}); } } }; };
3. Controller Pattern (Complex logic)
const WorkflowController = (props, context) => { const { getState, setState, executeBatch } = context; return { api: { startWorkflow: async (workflowId) => { executeBatch(() => { setState('workflow.current', workflowId); setState('workflow.status', 'running'); setState('workflow.startTime', Date.now()); }); // Execute workflow steps... }, getStatus: () => getState('workflow.status', 'idle'), getCurrentStep: () => getState('workflow.currentStep', 0) }, hooks: { onRegister: () => { setState('workflow.status', 'idle'); setState('workflow.steps', []); }, onUnregister: () => { // Cancel any running workflows if (getState('workflow.status') === 'running') { setState('workflow.status', 'cancelled'); } } } }; };
Required Elements:
- Factory function accepting
(props, context) - Return object with
apiproperty (even if empty:api: {})
Optional Elements:
-
hooksobject with lifecycle methods -
hooks.onRegister- called immediately after initialization -
hooks.onUnregister- called during cleanup/reinitialization
Best Practices:
- Always return consistent structure (
{ api, hooks }) - Keep
apifocused on public interface only - Use
hooks.onRegisterfor initialization that needs lifecycle management - Use
hooks.onUnregisterfor cleanup (subscriptions, timers, connections) - Make
hooksoptional for simple components - Destructure context for cleaner code
juris.registerHeadlessComponent('dataManager', (props, context) => { const { getState, setState, subscribe } = context; return { api: { loadData: async (endpoint) => { /* implementation */ }, getData: (key) => getState(`data.${key}`), clearCache: () => setState('data', {}) }, hooks: { onRegister: () => console.log('DataManager registered'), onUnregister: () => console.log('DataManager cleaned up') } }; });
Components can be initialized in several ways:
Manual Initialization:
const dataManager = juris.initializeHeadlessComponent('dataManager', { cacheTimeout: 5000, endpoint: '/api/data' });
While autoInit components start automatically, manual initialization gives you complete control over when and how components are created. Here are the key patterns:
// 1. Register the component (without autoInit) juris.registerHeadlessComponent('userManager', userManagerComponent); // 2. Initialize manually when needed const userManager = juris.initializeHeadlessComponent('userManager'); // 3. Use the API userManager.api.loadUser(123);
// Initialize with custom configuration const dataAPI = juris.initializeHeadlessComponent('dataService', { apiEndpoint: 'https://api.example.com', timeout: 5000, retries: 3 }); console.log(dataAPI.api.getEndpoint()); // 'https://api.example.com'
// Initialize different components based on conditions const initializeUserFeatures = async (userId) => { const user = await fetchUser(userId); if (user.subscription === 'premium') { const premium = juris.initializeHeadlessComponent('premiumFeatures', { userId: user.id, features: user.enabledFeatures }); return premium.api; } else { const basic = juris.initializeHeadlessComponent('basicFeatures', { userId: user.id }); return basic.api; } }; // Usage const userFeatures = await initializeUserFeatures(123); userFeatures.getAvailableFeatures();
// Create a lazy loader for expensive components class LazyComponentManager { constructor(juris) { this.juris = juris; this.cache = new Map(); } async getComponent(name, props = {}) { if (this.cache.has(name)) { return this.cache.get(name); } console.log(`Lazy loading component: ${name}`); const component = this.juris.initializeHeadlessComponent(name, props); this.cache.set(name, component); return component; } } // Setup const lazyLoader = new LazyComponentManager(juris); // Initialize only when needed const handleAnalyticsRequest = async () => { const analytics = await lazyLoader.getComponent('heavyAnalytics', { dataset: 'user-behavior' }); return analytics.api.generateReport(); };
const safeInitializeComponent = (componentName, props = {}) => { try { const component = juris.initializeHeadlessComponent(componentName, props); if (!component || !component.api) { throw new Error(`Component '${componentName}' failed to initialize properly`); } console.log(`✓ ${componentName} initialized successfully`); return component.api; } catch (error) { console.error(`✗ Failed to initialize ${componentName}:`, error.message); // Return fallback API return createFallbackAPI(componentName); } }; // Usage with error handling const analytics = safeInitializeComponent('analytics', { trackingId: 'GA-123' });
const initializeIfNeeded = (componentName, props = {}) => { const status = juris.getHeadlessStatus(); // Check if already initialized if (status.initialized.includes(componentName)) { console.log(`${componentName} already initialized`); return juris.headlessManager.getInstance(componentName); } // Check if registered if (!status.registered.includes(componentName)) { throw new Error(`Component '${componentName}' is not registered`); } // Initialize if needed return juris.initializeHeadlessComponent(componentName, props); };
// Initialize component const component = juris.initializeHeadlessComponent('myService', props); // Access pattern 1: Direct from initialization component.api.doSomething(); // Access pattern 2: From headlessAPIs const { myService } = juris.headlessAPIs; myService.doSomething(); // Access pattern 3: From context (in other components) const otherComponent = (props, context) => { const { myService } = context; return { api: { useService: () => myService.doSomething() } }; }; // Access pattern 4: Direct getter const api = juris.headlessManager.getAPI('myService'); api.doSomething();
Once headless components are initialized, their APIs can be consumed in multiple ways depending on your use case:
// After initialization, APIs are immediately available const userService = juris.initializeHeadlessComponent('userService'); // Direct method calls const user = await userService.api.fetchUser(123); const isValid = userService.api.validateEmail('user@example.com'); // Access state getters const currentUser = userService.api.getCurrentUser(); const isLoading = userService.api.isLoading();
// In standard components - APIs injected automatically const userProfileComponent = (props, context) => { const { userService, dataService } = context; // Auto-injected APIs return { render: () => ({ div: { children: [ { h1: { text: () => `Welcome ${userService.getCurrentUser()?.name || 'Guest'}` } }, { button: { text: 'Load Profile', onclick: async () => { const data = await dataService.fetch('/user/profile'); userService.updateProfile(data); } } } ] } }) }; }; // In headless components - access via context const notificationComponent = (props, context) => { const { userService, emailService } = context; return { api: { sendWelcomeEmail: async (userId) => { const user = await userService.fetchUser(userId); return emailService.send({ to: user.email, template: 'welcome', data: { name: user.name } }); } } }; };
// Access from anywhere in your application const handleGlobalAction = async () => { // Get all available APIs const apis = juris.headlessManager.getAllAPIs(); // Or access specific APIs const { userService, cartService, paymentService } = juris.headlessAPIs; // Coordinate multiple services const user = userService.getCurrentUser(); const cartItems = cartService.getItems(); if (user && cartItems.length > 0) { await paymentService.processPayment({ userId: user.id, items: cartItems, total: cartService.getTotal() }); } }; // In vanilla JavaScript (outside framework) window.addEventListener('beforeunload', () => { const { analytics } = window.juris.headlessAPIs; analytics.track('page_exit', { timestamp: Date.now() }); });
// React to headless component state changes const dashboardComponent = (props, context) => { const { userService, subscribe } = context; // Subscribe to user state changes const unsubscribe = subscribe('user.profile', (newProfile) => { console.log('Profile updated:', newProfile); // Trigger UI updates automatically }); return { api: { refreshDashboard: () => { // Access current state const user = userService.getCurrentUser(); const isLoading = userService.isLoading(); return { user, isLoading, lastUpdated: Date.now() }; } }, hooks: { onUnregister: () => unsubscribe() } }; };
// React Hook for Juris headless components const useJurisHeadless = (componentName) => { const [api, setApi] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { try { const instance = window.juris.initializeHeadlessComponent(componentName); setApi(instance.api); } catch (error) { console.error(`Failed to initialize ${componentName}:`, error); } finally { setLoading(false); } }, [componentName]); return { api, loading }; }; // React component using headless service function UserProfile() { const { api: userService, loading } = useJurisHeadless('userService'); const [user, setUser] = useState(null); useEffect(() => { if (userService) { userService.getCurrentUser().then(setUser); } }, [userService]); if (loading) return <div>Loading...</div>; return ( <div> <h1>Welcome {user?.name}</h1> <button onClick={() => userService.refreshProfile()}> Refresh </button> </div> ); } // Vue.js composition API function useUserService() { const userService = computed(() => window.juris.headlessAPIs.userService); const currentUser = ref(null); watch(userService, async (service) => { if (service) { currentUser.value = await service.getCurrentUser(); } }, { immediate: true }); return { userService, currentUser, refreshUser: () => userService.value?.refreshProfile() }; }
// Chain multiple headless component APIs const checkoutFlow = async () => { const { userService, cartService, paymentService, emailService } = juris.headlessAPIs; try { // 1. Validate user const user = userService.getCurrentUser(); if (!user) throw new Error('User not authenticated'); // 2. Get cart items const items = cartService.getItems(); if (items.length === 0) throw new Error('Cart is empty'); // 3. Calculate total const total = cartService.getTotal(); // 4. Process payment const payment = await paymentService.charge({ amount: total, customer: user.id, description: `Order for ${items.length} items` }); // 5. Clear cart cartService.clear(); // 6. Send confirmation email await emailService.sendOrderConfirmation({ user, payment, items }); return { success: true, orderId: payment.id }; } catch (error) { // Handle errors across multiple services console.error('Checkout failed:', error); return { success: false, error: error.message }; } };
// Use APIs conditionally based on availability const handleFeatureRequest = async (featureName) => { const apis = juris.headlessAPIs; // Check if premium features are available if (apis.premiumFeatures && apis.userService.isPremiumUser()) { return apis.premiumFeatures.executeFeature(featureName); } // Fall back to basic features if (apis.basicFeatures) { return apis.basicFeatures.executeFeature(featureName); } // No feature service available throw new Error('Feature service not available'); }; // Feature detection pattern const getAvailableFeatures = () => { const features = []; const apis = juris.headlessAPIs; if (apis.userService) features.push('user-management'); if (apis.paymentService) features.push('payments'); if (apis.analyticsService) features.push('analytics'); if (apis.chatService) features.push('real-time-chat'); return features; };
Comprehensive testing strategies for headless components ensure reliability and maintainability:
describe('UserService Headless Component', () => { let userService; let mockContext; beforeEach(() => { // Create mock context mockContext = { getState: jest.fn(), setState: jest.fn(), subscribe: jest.fn(() => jest.fn()), // returns unsubscribe function executeBatch: jest.fn((callback) => callback()), httpClient: { get: jest.fn(), post: jest.fn(), put: jest.fn() }, logger: { info: jest.fn(), error: jest.fn() } }; // Initialize component with mock context const component = UserServiceComponent({}, mockContext); userService = component.api; }); test('should fetch user data successfully', async () => { // Setup mocks const userData = { id: 1, name: 'John Doe', email: 'john@example.com' }; mockContext.httpClient.get.mockResolvedValue(userData); // Execute const result = await userService.fetchUser(1); // Assert expect(result).toEqual(userData); expect(mockContext.httpClient.get).toHaveBeenCalledWith('/users/1'); expect(mockContext.setState).toHaveBeenCalledWith('user.profile', userData); }); test('should handle fetch errors gracefully', async () => { // Setup error mock const error = new Error('Network error'); mockContext.httpClient.get.mockRejectedValue(error); // Execute and assert await expect(userService.fetchUser(1)).rejects.toThrow('Network error'); expect(mockContext.setState).toHaveBeenCalledWith('user.error', error.message); }); test('should validate email correctly', () => { expect(userService.validateEmail('valid@example.com')).toBe(true); expect(userService.validateEmail('invalid-email')).toBe(false); expect(userService.validateEmail('')).toBe(false); }); test('should manage user state correctly', () => { mockContext.getState.mockReturnValue({ id: 1, name: 'John' }); const user = userService.getCurrentUser(); expect(mockContext.getState).toHaveBeenCalledWith('user.profile', null); expect(user).toEqual({ id: 1, name: 'John' }); }); });
describe('E-commerce Service Integration', () => { let juris; let cartService; let userService; let paymentService; beforeEach(() => { // Setup Juris with mock services juris = new Juris({ services: { httpClient: new MockHttpClient(), paymentGateway: new MockPaymentGateway() } }); // Register components juris.registerHeadlessComponent('cart', CartServiceComponent); juris.registerHeadlessComponent('user', UserServiceComponent); juris.registerHeadlessComponent('payment', PaymentServiceComponent); // Initialize all services cartService = juris.initializeHeadlessComponent('cart').api; userService = juris.initializeHeadlessComponent('user').api; paymentService = juris.initializeHeadlessComponent('payment').api; }); test('should complete checkout flow', async () => { // Setup user await userService.login({ email: 'test@example.com', password: 'pass' }); expect(userService.isAuthenticated()).toBe(true); // Add items to cart cartService.addItem({ id: 1, price: 10.99, name: 'Product 1' }); cartService.addItem({ id: 2, price: 15.99, name: 'Product 2' }); expect(cartService.getItemCount()).toBe(2); expect(cartService.getTotal()).toBe(26.98); // Process payment const paymentResult = await paymentService.processPayment({ amount: cartService.getTotal(), items: cartService.getItems() }); expect(paymentResult.success).toBe(true); expect(paymentResult.transactionId).toBeDefined(); // Verify cart is cleared cartService.clear(); expect(cartService.getItemCount()).toBe(0); }); test('should handle authentication required scenarios', async () => { // Try checkout without authentication cartService.addItem({ id: 1, price: 10.99 }); await expect(paymentService.processPayment({ amount: cartService.getTotal() })).rejects.toThrow('Authentication required'); }); });
describe('Headless Component State Management', () => { let juris; let component; beforeEach(() => { juris = new Juris(); juris.registerHeadlessComponent('testComponent', testComponent); component = juris.initializeHeadlessComponent('testComponent').api; }); test('should manage reactive state correctly', async () => { const stateChanges = []; // Subscribe to state changes const unsubscribe = juris.subscribe('test.data', (newValue) => { stateChanges.push(newValue); }); // Trigger state changes await component.updateData('first'); await component.updateData('second'); expect(stateChanges).toEqual(['first', 'second']); expect(component.getData()).toBe('second'); unsubscribe(); }); test('should handle batch state updates', () => { const updateSpy = jest.spyOn(juris.stateManager, 'setState'); component.batchUpdate({ value1: 'a', value2: 'b', value3: 'c' }); // Verify batching reduces setState calls expect(updateSpy).toHaveBeenCalledTimes(1); }); });
describe('Headless Component Lifecycle', () => { let juris; let lifecycleEvents; beforeEach(() => { lifecycleEvents = []; juris = new Juris(); const lifecycleComponent = (props, context) => ({ api: { getData: () => 'test' }, hooks: { onRegister: () => lifecycleEvents.push('registered'), onUnregister: () => lifecycleEvents.push('unregistered') } }); juris.registerHeadlessComponent('lifecycle', lifecycleComponent); }); test('should trigger lifecycle hooks correctly', () => { // Initialize component const component = juris.initializeHeadlessComponent('lifecycle'); expect(lifecycleEvents).toContain('registered'); // Cleanup component juris.headlessManager.reinitialize('lifecycle'); expect(lifecycleEvents).toContain('unregistered'); expect(lifecycleEvents).toContain('registered'); // reinitialize triggers both }); test('should handle lifecycle errors gracefully', () => { const errorComponent = (props, context) => ({ api: { test: () => 'ok' }, hooks: { onRegister: () => { throw new Error('Registration error'); } } }); juris.registerHeadlessComponent('errorComponent', errorComponent); // Should not throw, but log error const component = juris.initializeHeadlessComponent('errorComponent'); expect(component.api.test()).toBe('ok'); // Component still works }); });
describe('Async Headless Component Operations', () => { let component; let mockTimer; beforeEach(() => { mockTimer = jest.useFakeTimers(); const asyncComponent = (props, context) => ({ api: { delayedOperation: async (delay = 1000) => { return new Promise(resolve => { setTimeout(() => resolve('completed'), delay); }); }, fetchWithTimeout: async (url, timeout = 5000) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); return response.json(); } catch (error) { throw new Error(`Request failed: ${error.message}`); } } } }); const juris = new Juris(); juris.registerHeadlessComponent('async', asyncComponent); component = juris.initializeHeadlessComponent('async').api; }); afterEach(() => { mockTimer.useRealTimers(); }); test('should handle delayed operations', async () => { const promise = component.delayedOperation(1000); // Fast-forward time mockTimer.advanceTimersByTime(1000); const result = await promise; expect(result).toBe('completed'); }); test('should handle timeout scenarios', async () => { // Mock fetch to never resolve global.fetch = jest.fn(() => new Promise(() => {})); const promise = component.fetchWithTimeout('/api/data', 1000); // Fast-forward past timeout mockTimer.advanceTimersByTime(1000); await expect(promise).rejects.toThrow('Request failed'); }); });
describe('Headless Component Error Handling', () => { test('should handle API errors gracefully', async () => { const errorComponent = (props, context) => ({ api: { riskyOperation: async () => { throw new Error('Something went wrong'); }, safeOperation: async () => { try { await this.riskyOperation(); } catch (error) { context.setState('error', error.message); return { success: false, error: error.message }; } } } }); const juris = new Juris(); juris.registerHeadlessComponent('error', errorComponent); const component = juris.initializeHeadlessComponent('error').api; const result = await component.safeOperation(); expect(result.success).toBe(false); expect(result.error).toBe('Something went wrong'); expect(juris.getState('error')).toBe('Something went wrong'); }); });
// Test utilities for headless components class HeadlessTestUtils { static createMockContext(overrides = {}) { return { getState: jest.fn((path, defaultValue) => defaultValue), setState: jest.fn(), subscribe: jest.fn(() => jest.fn()), executeBatch: jest.fn((callback) => callback()), logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn() }, ...overrides }; } static createMockJuris(services = {}) { return new Juris({ services: { httpClient: new MockHttpClient(), ...services } }); } static async waitForStateChange(juris, path, expectedValue, timeout = 1000) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`State change timeout for ${path}`)); }, timeout); const unsubscribe = juris.subscribe(path, (newValue) => { if (newValue === expectedValue) { clearTimeout(timeoutId); unsubscribe(); resolve(newValue); } }); }); } } // Usage in tests test('should update state reactively', async () => { const juris = HeadlessTestUtils.createMockJuris(); const component = juris.initializeHeadlessComponent('test').api; component.updateValue('new-value'); const stateValue = await HeadlessTestUtils.waitForStateChange( juris, 'test.value', 'new-value' ); expect(stateValue).toBe('new-value'); });
These testing approaches ensure your headless components are reliable, maintainable, and work correctly in isolation and integration scenarios.
Auto-Initialization:
// Essential system services that should start immediately juris.registerHeadlessComponent('logger', LoggerComponent, { autoInit: true }); juris.registerHeadlessComponent('errorHandler', ErrorHandlerComponent, { autoInit: true }); juris.registerHeadlessComponent('analytics', AnalyticsComponent, { autoInit: true }); // Auto-initialization example: Global error handler const ErrorHandlerComponent = (props, context) => { const { setState, getState } = context; return { api: { handleError: (error, context = {}) => { const errorLog = getState('errors.log', []); const errorEntry = { id: Date.now(), message: error.message, stack: error.stack, timestamp: Date.now(), context, userAgent: navigator.userAgent, url: window.location.href }; setState('errors.log', [...errorLog.slice(-99), errorEntry]); setState('errors.lastError', errorEntry); // Send to monitoring service if (props.reportToService) { sendErrorToService(errorEntry); } }, getErrorLog: () => getState('errors.log', []), getLastError: () => getState('errors.lastError', null), clearErrors: () => setState('errors.log', []) }, hooks: { onRegister: () => { // Set up global error handlers immediately window.addEventListener('error', (event) => { this.handleError(event.error, { type: 'unhandled' }); }); window.addEventListener('unhandledrejection', (event) => { this.handleError(new Error(event.reason), { type: 'promise' }); }); console.log('Global error handler initialized'); } } }; }; // Framework initialization with autoInit components const juris = new Juris({ headlessComponents: { errorHandler: { fn: ErrorHandlerComponent, options: { autoInit: true, reportToService: true } }, logger: { fn: LoggerComponent, options: { autoInit: true } } } }); // Auto-initialized components are immediately available // No need to manually initialize - they start with the framework setTimeout(() => { const { errorHandler, logger } = juris.headlessAPIs; logger.info('Application started'); console.log('Error log:', errorHandler.getErrorLog()); }, 100);
Context-Based Access:
// Within any component context const { dataManager } = context; // Auto-injected if initialized
Headless components support three lifecycle hooks:
- onRegister: Called when component instance is created
- onUpdate: Called when component props change (if supported)
- onUnregister: Called during cleanup
Once initialized, component APIs are automatically injected into:
- The global headless context
- Component creation contexts
- The main Juris instance
// All these provide access to the same API const api1 = juris.headlessAPIs.dataManager; const api2 = context.dataManager; const api3 = juris.getHeadlessComponent('dataManager').api;
Headless components have full access to Juris's reactive state system:
const searchComponent = (props, context) => { const { getState, setState, subscribe } = context; // Reactive state access with automatic dependency tracking const searchTerm = () => getState('search.term', ''); const results = () => getState('search.results', []); return { api: { search: async (term) => { setState('search.term', term); setState('search.loading', true); try { const data = await fetchSearchResults(term); setState('search.results', data); } catch (error) { setState('search.error', error.message); } finally { setState('search.loading', false); } }, getSearchState: () => ({ term: searchTerm(), results: results(), loading: getState('search.loading', false), error: getState('search.error', null) }) } }; };
Components can subscribe to state changes for reactive behavior:
const notificationComponent = (props, context) => { const { subscribe, setState } = context; // Subscribe to user state changes const unsubscribe = subscribe('user', (newValue, oldValue, path) => { if (path === 'user.notifications.unread') { // Trigger notification logic if (newValue > oldValue) { showNotificationBadge(newValue); } } }); return { api: { getUnreadCount: () => getState('user.notifications.unread', 0) }, hooks: { onUnregister: () => unsubscribe() } }; };
For performance optimization, use batch updates:
const formComponent = (props, context) => { const { executeBatch, setState } = context; return { api: { submitForm: async (formData) => { // Batch multiple state updates executeBatch(() => { setState('form.submitting', true); setState('form.errors', {}); setState('form.lastSubmitted', Date.now()); }); try { await submitToAPI(formData); executeBatch(() => { setState('form.success', true); setState('form.submitting', false); }); } catch (error) { setState('form.errors', parseErrors(error)); setState('form.submitting', false); } } } }; };
Design headless components as services with clear responsibilities:
const authService = (props, context) => { const { getState, setState, services } = context; return { api: { // Authentication methods login: async (credentials) => { /* implementation */ }, logout: () => { /* implementation */ }, refresh: async () => { /* implementation */ }, // State accessors isAuthenticated: () => getState('auth.isLoggedIn', false), getCurrentUser: () => getState('auth.user', null), getToken: () => getState('auth.token', null), // Permission helpers hasPermission: (permission) => { const user = getState('auth.user'); return user?.permissions?.includes(permission) || false; }, hasRole: (role) => { const user = getState('auth.user'); return user?.roles?.includes(role) || false; } } }; };
Implement robust data management with caching and synchronization:
const dataService = (props, context) => { const { getState, setState, subscribe } = context; const cache = new Map(); return { api: { // CRUD operations fetch: async (endpoint, options = {}) => { const cacheKey = `${endpoint}:${JSON.stringify(options)}`; if (cache.has(cacheKey) && !options.force) { return cache.get(cacheKey); } setState(`loading.${endpoint}`, true); try { const data = await fetchData(endpoint, options); cache.set(cacheKey, data); setState(`data.${endpoint}`, data); return data; } catch (error) { setState(`errors.${endpoint}`, error.message); throw error; } finally { setState(`loading.${endpoint}`, false); } }, // Cache management invalidateCache: (pattern) => { for (const key of cache.keys()) { if (key.includes(pattern)) { cache.delete(key); } } }, // State helpers isLoading: (endpoint) => getState(`loading.${endpoint}`, false), getError: (endpoint) => getState(`errors.${endpoint}`, null), getData: (endpoint) => getState(`data.${endpoint}`, null) } }; };
Implement event systems for loose coupling:
const eventBus = (props, context) => { const listeners = new Map(); return { api: { emit: (eventType, payload) => { const eventListeners = listeners.get(eventType) || []; eventListeners.forEach(listener => { try { listener(payload); } catch (error) { console.error(`Event listener error for ${eventType}:`, error); } }); }, on: (eventType, listener) => { if (!listeners.has(eventType)) { listeners.set(eventType, []); } listeners.get(eventType).push(listener); // Return unsubscribe function return () => { const eventListeners = listeners.get(eventType); const index = eventListeners.indexOf(listener); if (index > -1) { eventListeners.splice(index, 1); } }; }, off: (eventType, listener) => { const eventListeners = listeners.get(eventType); if (eventListeners) { const index = eventListeners.indexOf(listener); if (index > -1) { eventListeners.splice(index, 1); } } }, once: (eventType, listener) => { const wrapper = (payload) => { listener(payload); this.off(eventType, wrapper); }; return this.on(eventType, wrapper); } } }; };
The context object provided to headless components contains:
const context = { // State management getState: (path, defaultValue, track) => {}, setState: (path, value, context) => {}, executeBatch: (callback) => {}, subscribe: (path, callback) => {}, // Services services: {}, ...services, // Spread services as direct properties // Headless APIs headless: {}, ...headlessAPIs, // Spread APIs as direct properties // Component management components: { register: (name, component) => {}, registerHeadless: (name, component, options) => {}, get: (name) => {}, getHeadless: (name) => {}, initHeadless: (name, props) => {}, reinitHeadless: (name, props) => {}, getHeadlessAPI: (name) => {}, getAllHeadlessAPIs: () => {} }, // Utilities utils: { render: (container) => {}, cleanup: () => {}, forceRender: () => {}, getHeadlessStatus: () => {} }, // Framework instance juris: jurisInstance, // Logging logger: { log, warn, error, info, debug, subscribe, unsubscribe }, // Environment isSSR: boolean };
Services are automatically injected into the context:
const juris = new Juris({ services: { httpClient: new HttpClient(), storage: new StorageService(), analytics: new AnalyticsService() } }); // Services available in headless components const apiComponent = (props, context) => { const { httpClient, storage, analytics } = context; return { api: { trackEvent: (event) => analytics.track(event), saveToStorage: (key, value) => storage.set(key, value), apiCall: (endpoint) => httpClient.get(endpoint) } }; };
Headless components can perform async initialization:
const databaseComponent = (props, context) => { const { setState } = context; // Async initialization const initPromise = (async () => { setState('db.connecting', true); try { const connection = await connectToDatabase(props.connectionString); setState('db.connected', true); setState('db.connection', connection); return connection; } catch (error) { setState('db.error', error.message); throw error; } finally { setState('db.connecting', false); } })(); return { api: { query: async (sql, params) => { const connection = await initPromise; return connection.query(sql, params); }, isConnected: () => getState('db.connected', false), getError: () => getState('db.error', null) }, hooks: { onUnregister: async () => { try { const connection = await initPromise; await connection.close(); } catch (error) { // Handle cleanup error } } } }; };
Enable communication between headless components:
const messagingHub = (props, context) => { const channels = new Map(); return { api: { createChannel: (channelName) => { if (!channels.has(channelName)) { channels.set(channelName, new Set()); } return { subscribe: (callback) => { channels.get(channelName).add(callback); return () => channels.get(channelName).delete(callback); }, publish: (message) => { channels.get(channelName).forEach(callback => { try { callback(message); } catch (error) { console.error(`Channel ${channelName} callback error:`, error); } }); } }; }, destroyChannel: (channelName) => { channels.delete(channelName); } } }; }; // Usage in other components const userComponent = (props, context) => { const { messagingHub } = context; const userChannel = messagingHub.createChannel('user-updates'); return { api: { updateUser: (userData) => { // Update user data setState('user.data', userData); // Notify other components userChannel.publish({ type: 'USER_UPDATED', data: userData, timestamp: Date.now() }); } } }; };
Create extensible components with plugin support:
const pluginManager = (props, context) => { const plugins = new Map(); const hooks = new Map(); return { api: { registerPlugin: (name, plugin) => { plugins.set(name, plugin); // Initialize plugin hooks if (plugin.hooks) { Object.entries(plugin.hooks).forEach(([hookName, handler]) => { if (!hooks.has(hookName)) { hooks.set(hookName, []); } hooks.get(hookName).push(handler); }); } }, executeHook: async (hookName, ...args) => { const handlers = hooks.get(hookName) || []; const results = []; for (const handler of handlers) { try { const result = await handler(...args); results.push(result); } catch (error) { console.error(`Hook ${hookName} error:`, error); } } return results; }, getPlugin: (name) => plugins.get(name), getAllPlugins: () => Array.from(plugins.keys()) } }; };
Each headless component should have a single, well-defined responsibility:
// Good: Focused authentication service const authService = (props, context) => { /* auth logic only */ }; // Good: Focused data caching service const cacheService = (props, context) => { /* caching logic only */ }; // Avoid: Mixed responsibilities const authAndDataService = (props, context) => { /* auth + data + UI logic */ };
Use the context system for dependency injection:
const userService = (props, context) => { const { httpClient, authService, cacheService } = context; return { api: { fetchUser: async (id) => { if (!authService.isAuthenticated()) { throw new Error('Not authenticated'); } const cached = cacheService.get(`user:${id}`); if (cached) return cached; const user = await httpClient.get(`/users/${id}`); cacheService.set(`user:${id}`, user); return user; } } }; };
Implement comprehensive error handling:
const resilientService = (props, context) => { const { setState, logger } = context; return { api: { performOperation: async (data) => { try { setState('operation.loading', true); setState('operation.error', null); const result = await riskOperation(data); setState('operation.result', result); return result; } catch (error) { logger.error('Operation failed:', error); setState('operation.error', { message: error.message, code: error.code, timestamp: Date.now() }); throw error; } finally { setState('operation.loading', false); } } } }; };
Always clean up resources in onUnregister:
const resourceManager = (props, context) => { const intervals = []; const subscriptions = []; const connections = []; return { api: { startPolling: (callback, interval) => { const id = setInterval(callback, interval); intervals.push(id); return id; } }, hooks: { onUnregister: () => { // Clean up intervals intervals.forEach(id => clearInterval(id)); // Clean up subscriptions subscriptions.forEach(unsub => unsub()); // Close connections connections.forEach(conn => conn.close()); } } }; };
Use consistent API patterns for better maintainability:
const typedApiComponent = (props, context) => { // Define clear interface const api = { // Data operations create: async (data) => { /* implementation */ }, read: async (id) => { /* implementation */ }, update: async (id, data) => { /* implementation */ }, delete: async (id) => { /* implementation */ }, // Query operations list: async (options = {}) => { /* implementation */ }, search: async (query) => { /* implementation */ }, filter: async (criteria) => { /* implementation */ }, // State accessors getItems: () => getState('items', []), getLoading: () => getState('loading', false), getError: () => getState('error', null) }; return { api }; };
❌ Bad Practice: Accessing global functions directly
// Global functions scattered throughout codebase window.formatCurrency = (amount, currency) => { /* implementation */ }; window.validateEmail = (email) => { /* implementation */ }; window.debounce = (func, delay) => { /* implementation */ }; // Components accessing globals directly (creates spaghetti code) const badComponent = (props, context) => { return { api: { processUser: (userData) => { // Direct global access - hard to test, track, and maintain if (!window.validateEmail(userData.email)) { throw new Error('Invalid email'); } const formatted = window.formatCurrency(userData.salary, 'USD'); // More spaghetti code... } } }; };
✅ Good Practice: Register utilities as services and group related functions
// Group related utility functions into cohesive services class ValidationService { validateEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } validatePhone(phone) { return /^\+?[\d\s-()]+$/.test(phone); } validateRequired(value) { return value != null && value !== ''; } validateLength(value, min, max) { const len = value?.length || 0; return len >= min && len <= max; } } class FormattingService { formatCurrency(amount, currency = 'USD', locale = 'en-US') { return new Intl.NumberFormat(locale, { style: 'currency', currency: currency }).format(amount); } formatDate(date, format = 'short') { return new Intl.DateTimeFormat('en-US', { dateStyle: format }).format(new Date(date)); } formatNumber(number, decimals = 2) { return Number(number).toFixed(decimals); } } class UtilityService { debounce(func, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } throttle(func, limit) { let inThrottle; return (...args) => { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } } // Register services with Juris const juris = new Juris({ services: { validator: new ValidationService(), formatter: new FormattingService(), utils: new UtilityService() } }); // Clean component using injected services const goodComponent = (props, context) => { const { validator, formatter, utils } = context; return { api: { processUser: (userData) => { // Clean, testable, traceable dependencies if (!validator.validateEmail(userData.email)) { throw new Error('Invalid email'); } if (!validator.validateRequired(userData.name)) { throw new Error('Name is required'); } const formatted = formatter.formatCurrency(userData.salary, 'USD'); const debouncedSave = utils.debounce(saveUser, 500); return { ...userData, formattedSalary: formatted, save: debouncedSave }; } } }; };
Benefits of Service Registration:
- Dependency Injection: Clear, testable dependencies
- Namespace Organization: Related functions grouped logically
- Easy Mocking: Services can be easily mocked for testing
- Consistent Access: All components use the same service interface
- Maintainability: Changes to utilities are centralized
- Type Safety: Services can be typed for better IDE support
- Performance: Services are instantiated once and reused
- Testability: Each service can be unit tested independently
Service Organization Guidelines:
// Group by domain/responsibility const juris = new Juris({ services: { // Data validation validator: new ValidationService(), // UI formatting formatter: new FormattingService(), // HTTP communication http: new HttpService(), // Local storage storage: new StorageService(), // Analytics tracking analytics: new AnalyticsService(), // Utility functions utils: new UtilityService(), // Business logic helpers business: new BusinessLogicService() } });
Use selective state tracking to avoid unnecessary re-renders:
const optimizedComponent = (props, context) => { const { getState } = context; return { api: { // Track specific paths only getSpecificData: () => getState('specific.path.only'), // Skip tracking when just reading getDataForComparison: () => getState('data', null, false), // Batch state updates updateMultipleValues: () => { context.executeBatch(() => { setState('value1', data1); setState('value2', data2); setState('value3', data3); }); } } }; };
Implement proper cleanup to prevent memory leaks:
const memoryEfficientComponent = (props, context) => { const cache = new WeakMap(); const timers = []; return { api: { cacheWithWeakRef: (object, data) => { cache.set(object, data); // Automatically cleaned when object is GC'd } }, hooks: { onUnregister: () => { timers.forEach(timer => clearTimeout(timer)); // WeakMap cleans itself up } } }; };
Implement lazy initialization for expensive operations:
const lazyComponent = (props, context) => { let heavyResource = null; const getHeavyResource = async () => { if (!heavyResource) { heavyResource = await initializeExpensiveResource(); } return heavyResource; }; return { api: { useResource: async () => { const resource = await getHeavyResource(); return resource.doSomething(); } } }; };
Test components in isolation:
describe('UserService', () => { let userService; let mockContext; beforeEach(() => { mockContext = { getState: jest.fn(), setState: jest.fn(), httpClient: { get: jest.fn(), post: jest.fn() } }; const component = userServiceComponent({}, mockContext); userService = component.api; }); test('should fetch user data', async () => { const userData = { id: 1, name: 'Test User' }; mockContext.httpClient.get.mockResolvedValue(userData); const result = await userService.fetchUser(1); expect(result).toEqual(userData); expect(mockContext.httpClient.get).toHaveBeenCalledWith('/users/1'); }); });
Test component interactions:
describe('Service Integration', () => { let juris; beforeEach(() => { juris = new Juris({ services: { httpClient: new MockHttpClient() } }); juris.registerHeadlessComponent('auth', authComponent); juris.registerHeadlessComponent('user', userComponent); }); test('should coordinate authentication and user data', async () => { const auth = juris.initializeHeadlessComponent('auth'); const user = juris.initializeHeadlessComponent('user'); await auth.api.login({ username: 'test', password: 'pass' }); const userData = await user.api.fetchCurrentUser(); expect(userData).toBeDefined(); }); });
Test reactive state behavior:
describe('State Reactivity', () => { test('should react to state changes', async () => { const juris = new Juris(); const notifications = []; const testComponent = (props, context) => { const { subscribe } = context; subscribe('test.value', (newValue) => { notifications.push(newValue); }); return { api: {} }; }; juris.registerHeadlessComponent('test', testComponent); juris.initializeHeadlessComponent('test'); juris.setState('test.value', 'hello'); juris.setState('test.value', 'world'); expect(notifications).toEqual(['hello', 'world']); }); });
Start by creating headless services for specific functionality:
// Step 1: Extract existing logic into headless service const existingFeatureService = (props, context) => { return { api: { // Migrate existing functions doSomething: existingDoSomething, processData: existingProcessData } }; }; // Step 2: Register and use juris.registerHeadlessComponent('feature', existingFeatureService); // Step 3: Update existing code to use service const { feature } = context; feature.doSomething();
Use headless components with other frameworks:
// React integration const useJurisHeadless = (componentName) => { const [api, setApi] = useState(null); useEffect(() => { const instance = window.juris.initializeHeadlessComponent(componentName); setApi(instance.api); return () => { window.juris.headlessManager.cleanup(); }; }, [componentName]); return api; }; // Vue integration const jurisPlugin = { install(app, options) { app.config.globalProperties.$juris = window.juris; app.mixin({ created() { this.headlessServices = {}; options.services?.forEach(serviceName => { this.headlessServices[serviceName] = window.juris.initializeHeadlessComponent(serviceName); }); } }); } };
Headless components work seamlessly with SSR:
// Node.js SSR setup const Juris = require('juris'); const juris = new Juris({ services: { database: databaseService, cache: redisCache } }); // Register headless components juris.registerHeadlessComponent('user', userService); juris.registerHeadlessComponent('content', contentService); // Initialize for request const userAPI = juris.initializeHeadlessComponent('user'); const contentAPI = juris.initializeHeadlessComponent('content'); // Use during rendering const userData = await userAPI.fetchUser(userId); const pageContent = await contentAPI.getPageContent(pageId);
Juris's headless component system provides a powerful foundation for building scalable, maintainable applications. By separating logic from presentation, you can create reusable services that work across different UI frameworks and environments.
The key to success with Juris headless components is:
- Clear separation of concerns - Keep logic separate from UI
- Consistent API design - Use standard patterns for predictability
- Proper lifecycle management - Handle initialization and cleanup correctly
- Performance optimization - Use reactive state efficiently
- Comprehensive testing - Test components in isolation and integration
This architecture enables you to build robust applications that can evolve and scale while maintaining clean, testable code.