Juris Component Patterns
Comprehensive guide to component patterns in JurisJS - from basic display components to advanced composition patterns. Learn proven approaches for building maintainable, reusable, and performant UI components.
🧩 Component Basics
Juris components are pure functions that return UI object definitions. They receive props from parents and context for framework interactions.
// Basic component structure
const ComponentName = (props, context) => {
// Component logic here
return {
tagName: {
// Attributes and properties
className: 'component-class',
// Event handlers
onClick: (event) => {
// Handle interactions
},
// Content
text: 'Component content',
// Child components
children: () => [
{ ChildComponent: { prop: 'value' } }
]
}
};
};
📋 Pattern Categories
🎯 Basic Patterns
Fundamental component patterns for common UI needs
Low ComplexityUse Cases
- Static content display
- Simple user interactions
- Basic conditional rendering
📊 Data Patterns
Components that handle data display, forms, and async operations
Medium ComplexityUse Cases
- Lists and tables
- Form validation
- API integration
🏗️ Composition Patterns
Advanced patterns for component composition and reusability
High ComplexityUse Cases
- Complex UI layouts
- Reusable logic
- Component abstraction
🎯 Simple Display Components
Components that display static or reactive content without complex interactions.
Basic Text Display
// Static text component
const Heading = (props) => ({
h1: {
className: `heading ${props.size || 'medium'}`,
text: props.title,
style: {
color: props.color || '#ffffff'
}
}
});
// Reactive text component
const UserGreeting = (props, { getState }) => ({
div: {
className: 'greeting',
text: () => {
const userName = getState('user.name', 'Guest');
const timeOfDay = new Date().getHours() < 12 ? 'morning' : 'evening';
return `Good ${timeOfDay}, ${userName}!`;
}
}
});
// Status badge component
const StatusBadge = (props, { getState }) => ({
span: {
className: () => {
const status = props.status || getState('system.status', 'unknown');
return `badge badge-${status}`;
},
text: () => (props.status || getState('system.status', 'Unknown')).toUpperCase(),
style: {
backgroundColor: () => {
const status = props.status || getState('system.status');
const colors = {
online: '#22c55e',
offline: '#ef4444',
maintenance: '#f59e0b'
};
return colors[status] || '#6b7280';
}
}
}
});
⚠️ Edge Cases to Handle
- Null/undefined data: Always provide fallback values
- Long text overflow: Handle text truncation and tooltips
- Dynamic styling: Validate color values and CSS properties
Image Display Components
// Responsive image with fallback
const ResponsiveImage = (props) => ({
div: {
className: 'image-container',
children: () => [{
img: {
src: props.src,
alt: props.alt || 'Image',
className: 'responsive-image',
onError: (e) => {
e.target.src = props.fallback || '/images/placeholder.jpg';
},
onLoad: () => {
// Image loaded successfully
if (props.onLoad) props.onLoad();
}
}
}]
}
});
// Avatar component with initials fallback
const Avatar = (props, { getState }) => {
const getInitials = (name) => {
return name.split(' ').map(n => n[0]).join('').toUpperCase();
};
return {
div: {
className: `avatar ${props.size || 'medium'}`,
children: () => {
const user = props.user || getState('user.current');
if (user?.avatar) {
return [{
img: {
src: user.avatar,
alt: user.name,
onError: (e) => e.target.style.display = 'none'
}
}];
}
return [{
span: {
className: 'avatar-initials',
text: user?.name ? getInitials(user.name) : '?'
}
}];
}
}
};
};
// Icon component with loading state
const Icon = (props) => ({
i: {
className: () => {
const baseClass = 'icon';
const iconClass = `icon-${props.name}`;
const sizeClass = props.size ? `icon-${props.size}` : '';
const loadingClass = props.loading ? 'icon-loading' : '';
return [baseClass, iconClass, sizeClass, loadingClass]
.filter(Boolean).join(' ');
},
style: {
color: props.color,
fontSize: props.customSize
},
title: props.tooltip
}
});
⚠️ Edge Cases to Handle
- Image loading failures: Provide meaningful fallbacks
- Missing user data: Show placeholder initials or default avatar
- Icon accessibility: Include proper alt text and ARIA labels
🎛️ Interactive Basic Components
Components that handle user interactions like clicks, form inputs, and state changes.
Button Components
// Basic interactive button
const Button = (props, { getState }) => ({
button: {
className: () => {
const baseClass = 'btn';
const variantClass = `btn-${props.variant || 'primary'}`;
const sizeClass = props.size ? `btn-${props.size}` : '';
const disabledClass = props.disabled || getState('ui.loading') ? 'btn-disabled' : '';
return [baseClass, variantClass, sizeClass, disabledClass]
.filter(Boolean).join(' ');
},
disabled: () => props.disabled || getState('ui.loading'),
onClick: (e) => {
if (props.disabled || getState('ui.loading')) return;
if (props.onClick) {
props.onClick(e);
}
},
children: () => {
if (getState('ui.loading') && props.showLoading) {
return [
{ Icon: { name: 'spinner', className: 'btn-spinner' } },
{ span: { text: 'Loading...' } }
];
}
return [
props.icon ? { Icon: { name: props.icon } } : null,
{ span: { text: props.text || props.children } }
].filter(Boolean);
}
}
});
// Toggle button with state
const ToggleButton = (props, { getState, setState }) => {
const isActive = () => getState(props.statePath, props.defaultValue || false);
return {
button: {
className: () => `toggle-btn ${isActive() ? 'active' : 'inactive'}`,
onClick: () => {
const newValue = !isActive();
setState(props.statePath, newValue);
if (props.onChange) {
props.onChange(newValue);
}
},
children: () => [{
span: {
text: isActive() ? props.activeText : props.inactiveText
}
}]
}
};
};
// Action button with confirmation
const ActionButton = (props, { setState }) => ({
button: {
className: `action-btn ${props.danger ? 'btn-danger' : 'btn-primary'}`,
onClick: async (e) => {
if (props.requireConfirmation) {
const confirmed = confirm(props.confirmMessage || 'Are you sure?');
if (!confirmed) return;
}
if (props.async) {
setState('ui.loading', true);
try {
await props.onClick(e);
} catch (error) {
setState('ui.error', error.message);
} finally {
setState('ui.loading', false);
}
} else {
props.onClick(e);
}
},
text: props.text
}
});
⚠️ Edge Cases to Handle
- Double-click prevention: Disable button during async operations
- Keyboard accessibility: Handle Enter and Space key events
- Loading states: Show visual feedback during async actions
Input Components
// Controlled input with validation
const TextInput = (props, { useState }) => {
const [value, setValue] = useState(props.statePath, props.defaultValue || '');
const [error, setError] = useState(`${props.statePath}.error`, null);
const validate = (newValue) => {
if (props.required && !newValue.trim()) {
return 'This field is required';
}
return null;
};
return {
div: {
className: 'input-group',
children: () => [
{
input: {
value: value(),
onInput: (e) => {
const newValue = e.target.value;
setValue(newValue);
setError(null); // Clear error on input
},
onBlur: (e) => {
const validationError = validate(e.target.value);
setError(validationError);
}
}
}
]
}
};
};
// Select dropdown component
const Select = (props, { getState, setState }) => {
const getValue = () => getState(props.statePath, props.defaultValue || '');
return {
div: {
className: 'select-group',
children: () => [
props.label ? {
label: {
text: props.label,
className: 'select-label'
}
} : null,
{
select: {
value: () => getValue(),
className: 'form-select',
onChange: (e) => {
const value = e.target.value;
setState(props.statePath, value);
if (props.onChange) {
props.onChange(value);
}
},
children: () => [
props.placeholder ? {
option: {
value: '',
text: props.placeholder,
disabled: true
}
} : null,
...props.options.map(option => ({
option: {
value: option.value,
text: option.label || option.value,
selected: () => getValue() === option.value
}
}))
].filter(Boolean)
}
}
].filter(Boolean)
}
};
};
// Checkbox with indeterminate state
const Checkbox = (props, { getState, setState }) => {
const isChecked = () => getState(props.statePath, props.defaultValue || false);
return {
label: {
className: 'checkbox-label',
children: () => [
{
input: {
type: 'checkbox',
checked: () => isChecked(),
indeterminate: () => props.indeterminate,
onChange: (e) => {
const checked = e.target.checked;
setState(props.statePath, checked);
if (props.onChange) {
props.onChange(checked);
}
}
}
},
{
span: {
text: props.label,
className: 'checkbox-text'
}
}
]
}
};
};
⚠️ Edge Cases to Handle
- Input sanitization: Clean and validate user input
- Real-time validation: Balance UX with validation feedback timing
- Accessibility: Proper labeling and ARIA attributes
🔀 Conditional Rendering Patterns
Components that render different content based on state, props, or conditions.
Conditional Display
// Simple conditional rendering
const ConditionalMessage = (props, { getState }) => ({
div: {
children: () => {
const isLoggedIn = getState('auth.isAuthenticated', false);
if (isLoggedIn) {
return [{
div: {
className: 'welcome-message',
text: `Welcome back, ${getState('auth.user.name', 'User')}!`
}
}];
}
return [{
div: {
className: 'login-prompt',
children: [
{ p: { text: 'Please log in to continue' } },
{ Button: { text: 'Login', onClick: props.onLogin } }
]
}
}];
}
}
});
// Multi-state conditional rendering
const LoadingStateComponent = (props, { getState }) => ({
div: {
className: 'state-container',
children: () => {
const loading = getState('ui.loading', false);
const error = getState('ui.error', null);
const data = getState('data.items', null);
// Loading state
if (loading) {
return [{
div: {
className: 'loading-state',
children: [
{ Icon: { name: 'spinner', className: 'spinner' } },
{ p: { text: 'Loading...' } }
]
}
}];
}
// Error state
if (error) {
return [{
div: {
className: 'error-state',
children: [
{ Icon: { name: 'alert-circle', className: 'error-icon' } },
{ p: { text: error } },
{ Button: {
text: 'Retry',
onClick: props.onRetry,
variant: 'secondary'
}}
]
}
}];
}
// Empty state
if (!data || data.length === 0) {
return [{
div: {
className: 'empty-state',
children: [
{ Icon: { name: 'inbox', className: 'empty-icon' } },
{ p: { text: 'No items found' } },
{ Button: {
text: 'Add Item',
onClick: props.onAdd
}}
]
}
}];
}
// Success state with data
return [{
div: {
className: 'data-state',
children: data.map(item => ({
DataItem: { item }
}))
}
}];
}
}
});
// Permission-based rendering
const PermissionGate = (props, { getState }) => {
const hasPermission = () => {
const userRole = getState('auth.user.role', 'guest');
const userPermissions = getState('auth.user.permissions', []);
// Check role-based permissions
if (props.requiredRole && userRole !== props.requiredRole) {
return false;
}
// Check specific permissions
if (props.requiredPermissions) {
return props.requiredPermissions.every(permission =>
userPermissions.includes(permission)
);
}
return true;
};
return {
div: {
children: () => {
if (!hasPermission()) {
if (props.fallback) {
return [props.fallback];
}
return [{
div: {
className: 'permission-denied',
text: 'Access denied'
}
}];
}
return props.children || [];
}
}
};
};
⚠️ Edge Cases to Handle
- Race conditions: Handle rapid state changes gracefully
- Permission checks: Validate permissions on both client and server
- Fallback content: Always provide meaningful fallbacks
📋 List Rendering Patterns
Components for displaying dynamic lists, tables, and collections of data.
Dynamic Lists
// Basic list component
const ItemList = (props, { getState }) => ({
div: {
className: 'item-list',
children: () => {
const items = getState(props.dataPath, []);
if (items.length === 0) {
return [{
div: {
className: 'empty-list',
text: props.emptyMessage || 'No items available'
}
}];
}
return items.map((item, index) => ({
div: {
key: item.id || index,
className: 'list-item',
children: [{
[props.itemComponent]: {
item,
index,
onUpdate: props.onItemUpdate,
onDelete: props.onItemDelete
}
}]
}
}));
}
}
});
// Filterable and sortable list
const FilterableList = (props, { getState, setState }) => {
const getFilteredItems = () => {
let items = getState(props.dataPath, []);
const filter = getState('ui.filter', '');
const sortBy = getState('ui.sortBy', 'name');
const sortOrder = getState('ui.sortOrder', 'asc');
// Apply filter
if (filter) {
items = items.filter(item =>
Object.values(item)
.join(' ')
.toLowerCase()
.includes(filter.toLowerCase())
);
}
// Apply sorting
items.sort((a, b) => {
const aVal = a[sortBy] || '';
const bVal = b[sortBy] || '';
const comparison = aVal.toString().localeCompare(bVal.toString());
return sortOrder === 'desc' ? -comparison : comparison;
});
return items;
};
return {
div: {
className: 'filterable-list',
children: () => [
{
div: {
className: 'list-controls',
children: [
{
TextInput: {
statePath: 'ui.filter',
placeholder: 'Search items...',
className: 'filter-input'
}
},
{
Select: {
statePath: 'ui.sortBy',
options: props.sortOptions || [
{ value: 'name', label: 'Name' },
{ value: 'date', label: 'Date' }
]
}
}
]
}
},
{
div: {
className: 'filtered-items',
children: () => getFilteredItems().map(item => ({
[props.itemComponent]: { item }
}))
}
}
]
}
};
};
// Paginated list component
const PaginatedList = (props, { getState, setState }) => {
const pageSize = props.pageSize || 10;
const currentPage = () => getState('ui.currentPage', 1);
const items = () => getState(props.dataPath, []);
const totalPages = () => Math.ceil(items().length / pageSize);
const startIndex = () => (currentPage() - 1) * pageSize;
const endIndex = () => startIndex() + pageSize;
const currentItems = () => items().slice(startIndex(), endIndex());
return {
div: {
className: 'paginated-list',
children: () => [
{
div: {
className: 'pagination-info',
text: () => {
const total = items().length;
const start = startIndex() + 1;
const end = Math.min(endIndex(), total);
return `Showing ${start}-${end} of ${total} items`;
}
}
},
{
div: {
className: 'paginated-items',
children: () => currentItems().map(item => ({
[props.itemComponent]: { item }
}))
}
},
{
div: {
className: 'pagination-controls',
children: () => [
{
Button: {
text: 'Previous',
disabled: () => currentPage() <= 1,
onClick: () => setState('ui.currentPage', currentPage() - 1)
}
},
{
span: {
className: 'page-info',
text: () => `Page ${currentPage()} of ${totalPages()}`
}
},
{
Button: {
text: 'Next',
disabled: () => currentPage() >= totalPages(),
onClick: () => setState('ui.currentPage', currentPage() + 1)
}
}
]
}
}
]
}
};
};
⚠️ Edge Cases to Handle
- Large datasets: Implement virtual scrolling for performance
- Real-time updates: Handle list changes while user is interacting
- Selection state: Maintain selection across pagination and filtering
📝 Form Handling Patterns
Comprehensive form components with validation, submission, and error handling.
Form Components
// Complete form component with validation
const ContactForm = (props, { getState, setState }) => {
const formData = () => getState('form.contact', {
name: '',
email: '',
message: ''
});
const errors = () => getState('form.contact.errors', {});
const isSubmitting = () => getState('form.contact.submitting', false);
const validateField = (field, value) => {
const validations = {
name: (val) => {
if (!val.trim()) return 'Name is required';
if (val.length < 2) return 'Name must be at least 2 characters';
return null;
},
email: (val) => {
if (!val.trim()) return 'Email is required';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(val)) return 'Please enter a valid email';
return null;
},
message: (val) => {
if (!val.trim()) return 'Message is required';
if (val.length < 10) return 'Message must be at least 10 characters';
return null;
}
};
return validations[field] ? validations[field](value) : null;
};
const validateForm = () => {
const data = formData();
const newErrors = {};
Object.keys(data).forEach(field => {
const error = validateField(field, data[field]);
if (error) newErrors[field] = error;
});
setState('form.contact.errors', newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) return;
setState('form.contact.submitting', true);
try {
await props.onSubmit(formData());
setState('form.contact', { name: '', email: '', message: '' });
setState('form.contact.errors', {});
setState('ui.notification', {
type: 'success',
message: 'Message sent successfully!'
});
} catch (error) {
setState('form.contact.errors', {
submit: 'Failed to send message. Please try again.'
});
} finally {
setState('form.contact.submitting', false);
}
};
return {
form: {
className: 'contact-form',
onSubmit: handleSubmit,
children: () => [
{
div: {
className: 'form-row',
children: [
{
TextInput: {
label: 'Name',
statePath: 'form.contact.name',
required: true,
error: () => errors().name
}
}
]
}
},
{
div: {
className: 'form-row',
children: [
{
TextInput: {
label: 'Email',
type: 'email',
statePath: 'form.contact.email',
required: true,
error: () => errors().email
}
}
]
}
},
{
div: {
className: 'form-row',
children: [
{
textarea: {
placeholder: 'Your message...',
value: () => formData().message,
className: () => errors().message ? 'error' : '',
onInput: (e) => {
setState('form.contact.message', e.target.value);
// Clear error on input
if (errors().message) {
const newErrors = { ...errors() };
delete newErrors.message;
setState('form.contact.errors', newErrors);
}
}
}
}
]
}
},
errors().submit ? {
div: {
className: 'form-error',
text: () => errors().submit
}
} : null,
{
div: {
className: 'form-actions',
children: [
{
Button: {
type: 'submit',
text: () => isSubmitting() ? 'Sending...' : 'Send Message',
disabled: () => isSubmitting(),
variant: 'primary'
}
}
]
}
}
].filter(Boolean)
}
};
};
// Multi-step form component
const MultiStepForm = (props, { getState, setState }) => {
const currentStep = () => getState('form.multiStep.currentStep', 1);
const totalSteps = props.steps.length;
const nextStep = () => {
if (currentStep() < totalSteps) {
setState('form.multiStep.currentStep', currentStep() + 1);
}
};
const prevStep = () => {
if (currentStep() > 1) {
setState('form.multiStep.currentStep', currentStep() - 1);
}
};
const getCurrentStepComponent = () => {
const step = props.steps[currentStep() - 1];
return step ? step.component : null;
};
return {
div: {
className: 'multi-step-form',
children: () => [
{
div: {
className: 'step-indicator',
children: () => props.steps.map((step, index) => ({
div: {
className: () => {
const stepNumber = index + 1;
const classes = ['step'];
if (stepNumber === currentStep()) classes.push('active');
if (stepNumber < currentStep()) classes.push('completed');
return classes.join(' ');
},
children: [
{
span: {
className: 'step-number',
text: index + 1
}
},
{
span: {
className: 'step-title',
text: step.title
}
}
]
}
}))
}
},
{
div: {
className: 'step-content',
children: () => {
const StepComponent = getCurrentStepComponent();
return StepComponent ? [{ [StepComponent]: {} }] : [];
}
}
},
{
div: {
className: 'step-navigation',
children: () => [
{
Button: {
text: 'Previous',
disabled: () => currentStep() <= 1,
onClick: prevStep,
variant: 'secondary'
}
},
{
Button: {
text: () => currentStep() === totalSteps ? 'Submit' : 'Next',
onClick: () => {
if (currentStep() === totalSteps) {
props.onSubmit();
} else {
nextStep();
}
},
variant: 'primary'
}
}
]
}
}
]
}
};
};
// Form field with dynamic validation
const ValidatedField = (props, { getState, setState }) => {
const value = () => getState(props.statePath, '');
const error = () => getState(`${props.statePath}.error`, null);
const touched = () => getState(`${props.statePath}.touched`, false);
const runValidation = async (val) => {
if (props.validators) {
for (const validator of props.validators) {
const result = await validator(val);
if (result) {
setState(`${props.statePath}.error`, result);
return result;
}
}
}
setState(`${props.statePath}.error`, null);
return null;
};
return {
div: {
className: 'validated-field',
children: () => [
{
input: {
type: props.type || 'text',
value: () => value(),
placeholder: props.placeholder,
className: () => {
const classes = ['form-input'];
if (touched() && error()) classes.push('error');
if (touched() && !error()) classes.push('valid');
return classes.join(' ');
},
onInput: (e) => {
setState(props.statePath, e.target.value);
runValidation(e.target.value);
},
onBlur: () => {
setState(`${props.statePath}.touched`, true);
runValidation(value());
}
}
},
touched() && error() ? {
div: {
className: 'field-error',
text: () => error()
}
} : null
].filter(Boolean)
}
};
};
⚠️ Edge Cases to Handle
- Async validation: Handle server-side validation and debouncing
- Form persistence: Save draft data during navigation
- File uploads: Handle progress, validation, and error states
🔄 Async Data Patterns
Components that handle asynchronous operations, API calls, and data loading states.
Data Loading Components
// Async data loader with caching
const DataLoader = (props, { getState, setState }) => {
const cacheKey = props.cacheKey || props.url;
const data = () => getState(`cache.${cacheKey}`, null);
const loading = () => getState(`loading.${cacheKey}`, false);
const error = () => getState(`errors.${cacheKey}`, null);
const lastFetch = () => getState(`lastFetch.${cacheKey}`, 0);
const shouldRefetch = () => {
const maxAge = props.maxAge || 300000; // 5 minutes
return Date.now() - lastFetch() > maxAge;
};
const fetchData = async () => {
if (loading()) return;
setState(`loading.${cacheKey}`, true);
setState(`errors.${cacheKey}`, null);
try {
const response = await fetch(props.url, props.options || {});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
setState(`cache.${cacheKey}`, result);
setState(`lastFetch.${cacheKey}`, Date.now());
if (props.onSuccess) {
props.onSuccess(result);
}
} catch (err) {
setState(`errors.${cacheKey}`, err.message);
if (props.onError) {
props.onError(err);
}
} finally {
setState(`loading.${cacheKey}`, false);
}
};
return {
div: {
className: 'data-loader',
onMount: () => {
if (!data() || shouldRefetch()) {
fetchData();
}
},
children: () => {
if (loading() && !data()) {
return [{
div: {
className: 'loading-state',
children: [
{ Icon: { name: 'spinner' } },
{ span: { text: props.loadingText || 'Loading...' } }
]
}
}];
}
if (error() && !data()) {
return [{
div: {
className: 'error-state',
children: [
{ p: { text: error() } },
{ Button: {
text: 'Retry',
onClick: fetchData
}}
]
}
}];
}
if (data()) {
return [{
div: {
className: 'data-content',
children: [
loading() ? {
div: {
className: 'refresh-indicator',
text: 'Refreshing...'
}
} : null,
{ [props.component]: { data: data() } }
].filter(Boolean)
}
}];
}
return [];
}
}
};
};
// Infinite scroll data loader
const InfiniteLoader = (props, { getState, setState }) => {
const items = () => getState(props.dataPath, []);
const loading = () => getState('ui.infiniteLoading', false);
const hasMore = () => getState('ui.hasMore', true);
const page = () => getState('ui.currentPage', 1);
const loadMore = async () => {
if (loading() || !hasMore()) return;
setState('ui.infiniteLoading', true);
try {
const response = await fetch(`${props.url}?page=${page()}&limit=${props.pageSize || 20}`);
const data = await response.json();
const newItems = [...items(), ...data.items];
setState(props.dataPath, newItems);
setState('ui.currentPage', page() + 1);
setState('ui.hasMore', data.hasMore);
} catch (error) {
setState('ui.error', 'Failed to load more items');
} finally {
setState('ui.infiniteLoading', false);
}
};
return {
div: {
className: 'infinite-loader',
children: () => [
{
div: {
className: 'infinite-content',
children: () => items().map(item => ({
[props.itemComponent]: { item }
}))
}
},
hasMore() ? {
div: {
className: 'load-trigger',
onIntersect: loadMore, // Custom intersection observer
children: [{
Button: {
text: () => loading() ? 'Loading...' : 'Load More',
disabled: () => loading(),
onClick: loadMore
}
}]
}
} : {
div: {
className: 'end-indicator',
text: 'No more items to load'
}
}
]
}
};
};
// Real-time data component with WebSocket
const RealTimeData = (props, context) => {
return {
render: () => ({
div: {
className: 'realtime-data',
children: () => [
{
div: {
className: () => {
const connected = context.getState('ws.connected', false);
return `connection-status ${connected ? 'connected' : 'disconnected'}`;
},
children: () => {
const connected = context.getState('ws.connected', false);
const error = context.getState('ws.error', null);
if (error) {
return [
{ Icon: { name: 'alert-circle', className: 'status-icon' } },
{ span: { text: `Connection Error: ${error}` } }
];
}
return [
{ Icon: {
name: connected ? 'wifi' : 'wifi-off',
className: 'status-icon'
}},
{ span: {
text: connected ? 'Connected' : 'Disconnected',
className: 'status-text'
}}
];
}
}
},
{
div: {
className: 'data-content',
children: () => {
const data = context.getState(props.dataPath);
if (!data) {
return [{ div: {
className: 'no-data',
text: 'Waiting for data...'
}}];
}
// Render data using provided component or default
if (props.component) {
return [{ [props.component]: { data } }];
}
// Default data display
return [{
pre: {
text: JSON.stringify(data, null, 2),
className: 'data-display'
}
}];
}
}
}
]
}
}),
onMount: (element, componentProps, componentContext) => {
let socket = null;
let reconnectTimeout = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = componentProps.maxReconnectAttempts || 5;
const reconnectDelay = componentProps.reconnectDelay || 3000;
const autoReconnect = componentProps.autoReconnect !== false;
const connect = () => {
if (socket && socket.readyState === WebSocket.OPEN) {
return; // Already connected
}
console.log(`🔄 Connecting to WebSocket: ${componentProps.wsUrl}`);
componentContext.setState('ws.connecting', true);
try {
socket = new WebSocket(componentProps.wsUrl);
socket.onopen = () => {
console.log('✅ WebSocket connected');
componentContext.setState('ws.connected', true);
componentContext.setState('ws.connecting', false);
componentContext.setState('ws.error', null);
reconnectAttempts = 0; // Reset on successful connection
// Call onConnect callback if provided
if (componentProps.onConnect) {
componentProps.onConnect(socket);
}
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Call custom message handler if provided
if (componentProps.onMessage) {
componentProps.onMessage(data, componentContext);
} else {
// Default: update the specified data path
componentContext.setState(componentProps.dataPath, data);
}
// Update last received timestamp
componentContext.setState('ws.lastReceived', Date.now());
} catch (error) {
console.error('❌ WebSocket message parse error:', error);
componentContext.setState('ws.error', 'Invalid message format');
}
};
socket.onerror = (error) => {
console.error('❌ WebSocket error:', error);
componentContext.setState('ws.error', 'Connection failed');
componentContext.setState('ws.connecting', false);
if (componentProps.onError) {
componentProps.onError(error, componentContext);
}
};
socket.onclose = (event) => {
console.log(`🔌 WebSocket closed: ${event.code} ${event.reason}`);
componentContext.setState('ws.connected', false);
componentContext.setState('ws.connecting', false);
socket = null;
// Call onDisconnect callback if provided
if (componentProps.onDisconnect) {
componentProps.onDisconnect(event, componentContext);
}
// Auto-reconnect logic
if (autoReconnect && reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = reconnectDelay * Math.pow(1.5, reconnectAttempts - 1); // Exponential backoff
console.log(`🔄 Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
componentContext.setState('ws.reconnecting', true);
reconnectTimeout = setTimeout(() => {
componentContext.setState('ws.reconnecting', false);
connect();
}, delay);
} else if (reconnectAttempts >= maxReconnectAttempts) {
console.error('❌ Max reconnection attempts reached');
componentContext.setState('ws.error', 'Connection lost - max retries exceeded');
}
};
} catch (error) {
console.error('❌ WebSocket connection error:', error);
componentContext.setState('ws.error', error.message);
componentContext.setState('ws.connecting', false);
}
};
const disconnect = () => {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
if (socket) {
socket.close(1000, 'Component unmounting');
socket = null;
}
// Clear all WebSocket state
componentContext.setState('ws.connected', false);
componentContext.setState('ws.connecting', false);
componentContext.setState('ws.reconnecting', false);
componentContext.setState('ws.error', null);
};
// Initial connection
connect();
// Expose methods to component API (optional)
if (componentProps.exposeAPI) {
element._wsAPI = {
connect,
disconnect,
send: (data) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
return true;
}
return false;
},
getStatus: () => ({
connected: componentContext.getState('ws.connected', false),
connecting: componentContext.getState('ws.connecting', false),
reconnecting: componentContext.getState('ws.reconnecting', false),
error: componentContext.getState('ws.error', null),
attempts: reconnectAttempts
})
};
}
// Return cleanup function - this is called on component unmount
return () => {
console.log('🧹 Cleaning up WebSocket connection');
disconnect();
};
},
onError: (error, element, props, context) => {
console.error('❌ RealTimeData component error:', error);
context.setState('ws.error', `Component error: ${error.message}`);
// Attempt to recover by reconnecting
if (props.recoverOnError !== false) {
setTimeout(() => {
context.setState('ws.error', null);
// Force remount by updating a dummy state
context.setState('ws.forceReconnect', Date.now());
}, 2000);
}
}
};
};
const RealTimeDataSimple = (props, context) => {
return {
render: () => {
const [connected, setConnected] = context.useState('ws.connected', false);
const [error, setError] = context.useState('ws.error', null);
const [data, setData] = context.useState(props.dataPath, null);
return {
div: {
className: 'realtime-data-simple',
children: () => [
// Connection status
{
div: {
className: `status ${connected() ? 'connected' : 'disconnected'}`,
text: () => {
if (error()) return `Error: ${error()}`;
return connected() ? '🟢 Connected' : '🔴 Disconnected';
}
}
},
// Data display
{
div: {
className: 'data',
children: () => {
const currentData = data();
if (!currentData) {
return [{ div: { text: 'No data received yet...' } }];
}
return props.renderData
? [props.renderData(currentData)]
: [{ pre: { text: JSON.stringify(currentData, null, 2) } }];
}
}
}
]
}
};
},
onMount: (element, componentProps, componentContext) => {
const [, setConnected] = componentContext.useState('ws.connected', false);
const [, setError] = componentContext.useState('ws.error', null);
const [, setData] = componentContext.useState(componentProps.dataPath, null);
const socket = new WebSocket(componentProps.wsUrl);
socket.onopen = () => {
setConnected(true);
setError(null);
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
setData(data);
} catch (err) {
setError('Invalid data format');
}
};
socket.onerror = () => {
setError('Connection failed');
setConnected(false);
};
socket.onclose = () => {
setConnected(false);
};
// Cleanup function
return () => {
socket.close();
};
}
};
};
// Usage examples:
const examples = {
basic: {
RealTimeData: {
wsUrl: 'wss://api.example.com/live',
dataPath: 'liveData.prices',
component: 'PriceDisplay'
}
},
advanced: {
RealTimeData: {
wsUrl: 'wss://api.example.com/live',
dataPath: 'liveData.messages',
maxReconnectAttempts: 10,
reconnectDelay: 2000,
autoReconnect: true,
exposeAPI: true,
onConnect: (socket) => {
socket.send(JSON.stringify({ action: 'subscribe', channel: 'notifications' }));
},
onMessage: (data, context) => {
// Custom message handling
if (data.type === 'notification') {
context.setState('notifications', data.payload);
} else if (data.type === 'update') {
context.setState('liveData.updates', data.payload);
}
},
onError: (error, context) => {
context.setState('ui.toast', {
type: 'error',
message: 'Real-time connection lost'
});
}
}
},
simple: {
RealTimeDataSimple: {
wsUrl: 'wss://api.example.com/simple',
dataPath: 'simpleData',
renderData: (data) => ({
div: {
className: 'custom-data-display',
text: `Latest: ${data.value} at ${new Date(data.timestamp).toLocaleTimeString()}`
}
})
}
}
};
⚠️ Edge Cases to Handle
- Network failures: Implement retry logic with exponential backoff
- Race conditions: Cancel previous requests when new ones are made
- Memory leaks: Clean up subscriptions and intervals on unmount
👨👧👦 Parent-Child Communication
Patterns for data flow and communication between parent and child components.
Props and Callbacks
// Parent component managing child state
const TodoApp = (props, { getState, setState }) => {
const todos = () => getState('todos', []);
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text: text.trim(),
completed: false,
createdAt: new Date()
};
setState('todos', [...todos(), newTodo]);
};
const updateTodo = (id, updates) => {
const updated = todos().map(todo =>
todo.id === id ? { ...todo, ...updates } : todo
);
setState('todos', updated);
};
const deleteTodo = (id) => {
const filtered = todos().filter(todo => todo.id !== id);
setState('todos', filtered);
};
return {
div: {
className: 'todo-app',
children: () => [
{
TodoForm: {
onSubmit: addTodo,
placeholder: 'Add a new todo...'
}
},
{
TodoList: {
items: todos(),
onUpdate: updateTodo,
onDelete: deleteTodo
}
},
{
TodoStats: {
total: todos().length,
completed: todos().filter(t => t.completed).length
}
}
]
}
};
};
// Child component receiving props and callbacks
const TodoItem = (props) => ({
div: {
className: () => `todo-item ${props.item.completed ? 'completed' : ''}`,
children: () => [
{
Checkbox: {
checked: props.item.completed,
onChange: (checked) => {
props.onUpdate(props.item.id, { completed: checked });
}
}
},
{
span: {
className: 'todo-text',
text: props.item.text,
onDoubleClick: () => {
// Enable inline editing
props.onEdit(props.item.id);
}
}
},
{
Button: {
text: 'Delete',
variant: 'danger',
size: 'small',
onClick: () => {
if (confirm('Delete this todo?')) {
props.onDelete(props.item.id);
}
}
}
}
]
}
});
// Provider pattern for deep prop passing
const ThemeProvider = (props, { getState, setState }) => {
const theme = () => getState('ui.theme', 'light');
const toggleTheme = () => {
const newTheme = theme() === 'light' ? 'dark' : 'light';
setState('ui.theme', newTheme);
};
const themeContext = {
theme: theme(),
toggleTheme,
colors: {
light: { bg: '#ffffff', text: '#000000' },
dark: { bg: '#1a1a1a', text: '#ffffff' }
}
};
return {
div: {
className: () => `theme-provider theme-${theme()}`,
style: () => ({
backgroundColor: themeContext.colors[theme()].bg,
color: themeContext.colors[theme()].text
}),
children: () => props.children.map(child => ({
...child,
themeContext
}))
}
};
};
⚠️ Edge Cases to Handle
- Prop drilling: Use context or state management for deep hierarchies
- Callback stability: Ensure callbacks don't cause unnecessary re-renders
- Data synchronization: Handle conflicts when multiple children update shared state
🎭 Higher-Order Components
Patterns for component composition and cross-cutting concerns.
HOC Patterns
// Authentication HOC
const withAuth = (WrappedComponent) => {
return (props, context) => {
const { getState, navigate } = context;
const isAuthenticated = () => getState('auth.isAuthenticated', false);
return {
div: {
children: () => {
if (!isAuthenticated()) {
return [{
div: {
className: 'auth-required',
text: 'Please log in to access this content'
}
}];
}
// Render wrapped component by returning its definition
return [WrappedComponent(props, context)];
}
}
};
};
};
// Loading HOC
const withLoading = (WrappedComponent, loadingConfig = {}) => {
return (props, context) => {
const { getState } = context;
const isLoading = () => getState(loadingConfig.statePath || 'ui.loading', false);
return {
div: {
className: 'loading-wrapper',
children: () => {
if (isLoading()) {
return [{
div: {
className: 'loading-overlay',
children: [
{ Icon: { name: 'spinner' } },
{ p: { text: loadingConfig.message || 'Loading...' } }
]
}
}];
}
return [{ [WrappedComponent]: props }];
}
}
};
};
};
// Error boundary HOC
const withErrorBoundary = (WrappedComponent, errorConfig = {}) => {
return (props, context) => {
const { getState, setState } = context;
const error = () => getState(`errors.${errorConfig.key || 'component'}`, null);
const clearError = () => {
setState(`errors.${errorConfig.key || 'component'}`, null);
};
return {
div: {
className: 'error-boundary',
children: () => {
if (error()) {
return [{
div: {
className: 'error-fallback',
children: [
{ h3: { text: 'Something went wrong' } },
{ p: { text: error() } },
{ Button: {
text: 'Try Again',
onClick: clearError
}}
]
}
}];
}
try {
return [{ [WrappedComponent]: props }];
} catch (err) {
setState(`errors.${errorConfig.key || 'component'}`, err.message);
return [];
}
}
}
};
};
};
// Data fetching HOC
const withData = (WrappedComponent, dataConfig) => {
return (props, context) => {
const { getState, setState } = context;
const cacheKey = dataConfig.cacheKey || dataConfig.url;
const data = () => getState(`cache.${cacheKey}`, null);
const loading = () => getState(`loading.${cacheKey}`, false);
const error = () => getState(`errors.${cacheKey}`, null);
const fetchData = async () => {
setState(`loading.${cacheKey}`, true);
setState(`errors.${cacheKey}`, null);
try {
const url = typeof dataConfig.url === 'function'
? dataConfig.url(props)
: dataConfig.url;
const response = await fetch(url);
const result = await response.json();
setState(`cache.${cacheKey}`, result);
} catch (err) {
setState(`errors.${cacheKey}`, err.message);
} finally {
setState(`loading.${cacheKey}`, false);
}
};
return {
div: {
onMount: () => {
if (!data() && !loading()) {
fetchData();
}
},
children: () => [{
[WrappedComponent]: {
...props,
data: data(),
loading: loading(),
error: error(),
refetch: fetchData
}
}]
}
};
};
};
// Usage examples
const ProtectedUserProfile = withAuth(withLoading(withData(UserProfile, {
url: (props) => `/api/users/${props.userId}`,
cacheKey: 'currentUser'
})));
const SafeDashboard = withErrorBoundary(withAuth(Dashboard), {
key: 'dashboard'
});
⚠️ Edge Cases to Handle
- HOC composition order: Order matters for data flow and functionality
- Props collision: Ensure HOCs don't override important props
- Performance impact: Consider HOC wrapping depth and re-render frequency
🎨 Render Props Pattern
Flexible component patterns that provide data or functionality through render functions.
Render Props Implementation
// Data provider with render prop
const DataProvider = (props, context) => {
const { getState, setState } = context;
const data = () => getState(props.dataPath, null);
const loading = () => getState(`${props.dataPath}.loading`, false);
const error = () => getState(`${props.dataPath}.error`, null);
const actions = {
refetch: async () => {
setState(`${props.dataPath}.loading`, true);
try {
const response = await fetch(props.url);
const result = await response.json();
setState(props.dataPath, result);
setState(`${props.dataPath}.error`, null);
} catch (err) {
setState(`${props.dataPath}.error`, err.message);
} finally {
setState(`${props.dataPath}.loading`, false);
}
},
update: (newData) => {
setState(props.dataPath, newData);
},
reset: () => {
setState(props.dataPath, null);
setState(`${props.dataPath}.error`, null);
}
};
return {
div: {
children: () => props.render({
data: data(),
loading: loading(),
error: error(),
actions
})
}
};
};
// Mouse tracker render prop
const MouseTracker = (props) => {
let mousePosition = { x: 0, y: 0 };
const updatePosition = (e) => {
mousePosition = { x: e.clientX, y: e.clientY };
// Trigger re-render somehow - in real implementation
// this would use setState or similar
};
return {
div: {
onMouseMove: updatePosition,
style: {
height: '100%',
width: '100%'
},
children: () => props.render(mousePosition)
}
};
};
// Form state management render prop
const FormProvider = (props, { getState, setState }) => {
const formPath = props.formPath || 'form';
const formData = () => getState(formPath, props.initialValues || {});
const errors = () => getState(`${formPath}.errors`, {});
const touched = () => getState(`${formPath}.touched`, {});
const formActions = {
setValue: (field, value) => {
setState(`${formPath}.${field}`, value);
},
setError: (field, error) => {
const currentErrors = errors();
setState(`${formPath}.errors`, { ...currentErrors, [field]: error });
},
setTouched: (field, isTouched = true) => {
const currentTouched = touched();
setState(`${formPath}.touched`, { ...currentTouched, [field]: isTouched });
},
reset: () => {
setState(formPath, props.initialValues || {});
setState(`${formPath}.errors`, {});
setState(`${formPath}.touched`, {});
},
validate: () => {
if (!props.validationSchema) return true;
const data = formData();
const newErrors = {};
Object.keys(props.validationSchema).forEach(field => {
const validator = props.validationSchema[field];
const error = validator(data[field], data);
if (error) newErrors[field] = error;
});
setState(`${formPath}.errors`, newErrors);
return Object.keys(newErrors).length === 0;
},
submit: async (onSubmit) => {
if (formActions.validate()) {
try {
await onSubmit(formData());
} catch (error) {
setState(`${formPath}.submitError`, error.message);
}
}
}
};
return {
div: {
children: () => props.render({
values: formData(),
errors: errors(),
touched: touched(),
actions: formActions
})
}
};
};
// Usage examples
const UserDataDisplay = () => ({
DataProvider: {
url: '/api/user',
dataPath: 'userData',
render: ({ data, loading, error, actions }) => {
if (loading) return [{ div: { text: 'Loading user...' } }];
if (error) return [{ div: { text: `Error: ${error}` } }];
if (!data) return [{ div: { text: 'No user data' } }];
return [
{ h2: { text: data.name } },
{ p: { text: data.email } },
{ Button: {
text: 'Refresh',
onClick: actions.refetch
}}
];
}
}
});
const ContactForm = () => ({
FormProvider: {
formPath: 'contactForm',
initialValues: { name: '', email: '', message: '' },
validationSchema: {
name: (value) => !value ? 'Name is required' : null,
email: (value) => !value || !value.includes('@') ? 'Valid email required' : null,
message: (value) => !value || value.length < 10 ? 'Message must be at least 10 characters' : null
},
render: ({ values, errors, touched, actions }) => [
{
TextInput: {
value: values.name || '',
error: touched.name && errors.name,
onChange: (value) => actions.setValue('name', value),
onBlur: () => actions.setTouched('name')
}
},
{
TextInput: {
type: 'email',
value: values.email || '',
error: touched.email && errors.email,
onChange: (value) => actions.setValue('email', value),
onBlur: () => actions.setTouched('email')
}
},
{
Button: {
text: 'Submit',
onClick: () => actions.submit(async (data) => {
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(data)
});
})
}
}
]
}
});
⚠️ Edge Cases to Handle
- Render function stability: Ensure render functions don't cause infinite loops
- Performance considerations: Optimize render prop components for frequent updates
- Type safety: Document expected render prop parameters clearly
🎯 Pattern Selection Guide
Choose the right component pattern based on your specific use case and requirements.
📊 Simple Display
Use for static content, labels, badges, and basic information display
Low ComplexityBest For
- Headers and text content
- Status indicators
- Image and media display
- Icons and visual elements
🎛️ Interactive Basic
Perfect for buttons, inputs, and simple user interactions
Low ComplexityBest For
- Form controls
- Action buttons
- Toggle switches
- Basic user input
📋 List & Data
Ideal for displaying collections, tables, and data-driven content
Medium ComplexityBest For
- Data tables
- Product catalogs
- Search results
- Infinite scroll lists
📝 Forms
Complex form handling with validation, submission, and error management
High ComplexityBest For
- User registration
- Data entry forms
- Multi-step wizards
- Settings panels
🔄 Async Data
Components that handle API calls, loading states, and real-time data
High ComplexityBest For
- API integration
- Real-time dashboards
- Live notifications
- Data synchronization
🏗️ Composition
Advanced patterns for reusable logic and component composition
High ComplexityBest For
- Cross-cutting concerns
- Reusable behaviors
- Complex layouts
- Plugin architectures
Decision Matrix
Requirement | Recommended Pattern | Alternative Options |
---|---|---|
Display user information | Simple Display | Conditional Rendering |
Handle form submission | Form Handling | Interactive Basic + Validation |
Show list of products | List Rendering | Async Data + List |
Reusable authentication | Higher-Order Components | Render Props |
Real-time notifications | Async Data | WebSocket + Conditional |
Complex modal dialog | Portal + Composition | Conditional + Interactive |
✨ Best Practices
Guidelines for writing maintainable, performant, and reusable Juris components.
Component Design Principles
- Single Responsibility: Each component should have one clear purpose
- Pure Functions: Same props should always produce the same output
- Minimal Props: Keep component interfaces simple and focused
- Composition over Inheritance: Build complex UIs by composing simple components
Performance Guidelines
- Optimize Reactive Functions: Avoid expensive calculations in reactive functions
- Use Keys for Lists: Provide stable keys for list items to optimize updates
- Minimize State Subscriptions: Only subscribe to the state you actually need
- Lazy Load Components: Load heavy components only when needed
Code Organization
- Group Related Components: Organize components by feature or domain
- Extract Common Logic: Use HOCs or render props for shared functionality
- Document Props: Clearly document expected props and their types
- Handle Edge Cases: Always consider null, undefined, and error states
Testing Strategies
- Test Component Logic: Focus on testing component behavior, not implementation
- Mock External Dependencies: Isolate components from external services
- Test User Interactions: Verify that user actions produce expected results
- Test Error Conditions: Ensure components handle errors gracefully
⚠️ Anti-Patterns to Avoid
Common mistakes and patterns that should be avoided in Juris component development.
State Anti-Patterns
// ❌ DON'T: Mutate state directly
const BadComponent = (props, { getState, setState }) => {
const data = getState('data', []);
data.push(newItem); // Direct mutation!
setState('data', data);
};
// ✅ DO: Create new state objects
const GoodComponent = (props, { getState, setState }) => {
const data = getState('data', []);
setState('data', [...data, newItem]); // Immutable update
};
Performance Anti-Patterns
// ❌ DON'T: Expensive operations in reactive functions
const BadComponent = (props, { getState }) => ({
div: {
text: () => {
const items = getState('items', []);
// Expensive calculation on every render!
return items.reduce((sum, item) => sum + complexCalculation(item), 0);
}
}
});
// ✅ DO: Use computed state or memoization
const GoodComponent = (props, { getState }) => ({
div: {
text: () => getState('computedTotal', 0) // Pre-calculated value
}
});
Common Mistakes
- Over-engineering: Don't use complex patterns for simple use cases
- Props drilling: Avoid passing props through many component layers
- Mixed concerns: Don't mix UI logic with business logic in components
- Missing error handling: Always handle potential error states
- Ignoring accessibility: Include proper ARIA labels and keyboard navigation