Showing posts with label Javascript. Show all posts
Showing posts with label Javascript. Show all posts

Tuesday, 4 November 2025

Mastering React Performance: A Deep Dive into Concurrency, useTransition, and useDeferredValue (2025 Guide)

November 04, 2025 0

Mastering React Performance: A Deep Dive into Concurrency, useTransition, and useDeferredValue

React concurrent performance optimization diagram showing useTransition and useDeferredValue workflows

In 2025, React's concurrent features have transformed from experimental concepts to essential tools for building high-performance applications. As users demand faster, more responsive interfaces, understanding React's concurrent rendering model and its powerful hooks—useTransition and useDeferredValue—has become crucial for every React developer. This comprehensive guide explores how to leverage these advanced features to eliminate UI freezes, prioritize critical updates, and deliver buttery-smooth user experiences. Whether you're building a data-intensive dashboard, complex forms, or real-time applications, mastering these performance patterns will elevate your React skills to the next level.

🚀 Why Concurrent React is a Game-Changer in 2025

Traditional React rendering follows a synchronous, all-or-nothing approach that can lead to UI freezes during heavy updates. Concurrent React introduces interruptible rendering, allowing React to work on multiple state updates simultaneously and prioritize urgent UI interactions.

  • Interruptible Rendering: React can pause, resume, or abandon renders based on priority
  • Automatic Batching: Multiple state updates are batched into single renders
  • Selective Hydration: Critical components hydrate first, non-critical ones later
  • Suspense Integration: Seamless loading states without blocking the UI
  • Improved User Perception: Immediate feedback even during heavy computations

🔧 Understanding the Concurrent Rendering Model

Concurrent React introduces a new mental model for thinking about rendering priorities and user interactions. Understanding these concepts is crucial for effective performance optimization.

  • Urgent Updates: User interactions like clicks, typing, and animations
  • Transition Updates: Non-urgent UI changes like search results or data fetching
  • Deferred Updates: Computationally expensive operations that can be delayed
  • Render Interruption: Ability to pause low-priority renders for high-priority ones
  • Time Slicing: Breaking work into chunks to maintain responsiveness

💻 useTransition: Prioritizing User Interactions

useTransition allows you to mark non-urgent state updates as transitions, keeping the UI responsive during expensive operations.


import { useState, useTransition } from 'react';
import { searchProducts } from './api';
import { ProductList } from './ProductList';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  // Handle search input with transition
  const handleSearch = (searchQuery) => {
    setQuery(searchQuery); // Urgent update - input reflects immediately
    
    // Mark search as non-urgent transition
    startTransition(() => {
      // This update can be interrupted if more urgent work comes in
      searchProducts(searchQuery).then(newResults => {
        setResults(newResults);
      });
    });
  };

  return (
    <div className="search-container">
      <input
        type="text"
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search products..."
        className="search-input"
      />
      
      {/* Show loading indicator during transition */}
      {isPending && (
        <div className="loading-indicator">
          Searching...
        </div>
      )}
      
      {/* Results show with smooth transition */}
      <ProductList 
        products={results} 
        isLoading={isPending}
      />
    </div>
  );
}

// Advanced useTransition with multiple states
function AdvancedSearch() {
  const [filters, setFilters] = useState({
    category: '',
    priceRange: [0, 1000],
    sortBy: 'name'
  });
  const [searchResults, setSearchResults] = useState([]);
  const [isSearching, startSearchTransition] = useTransition();
  const [searchStats, setSearchStats] = useState(null);

  const updateFilters = (newFilters) => {
    // Urgent update - filters change immediately
    setFilters(newFilters);
    
    // Non-urgent search operation
    startSearchTransition(async () => {
      const { results, stats } = await performSearch(newFilters);
      setSearchResults(results);
      setSearchStats(stats);
    });
  };

  // Handle individual filter changes
  const handleCategoryChange = (category) => {
    updateFilters({ ...filters, category });
  };

  const handlePriceChange = (priceRange) => {
    updateFilters({ ...filters, priceRange });
  };

  return (
    <div>
      <Filters 
        filters={filters}
        onCategoryChange={handleCategoryChange}
        onPriceChange={handlePriceChange}
      />
      
      {isSearching && <SearchProgress />}
      
      <SearchResults 
        results={searchResults}
        stats={searchStats}
      />
    </div>
  );
}

// useTransition with error handling
function SearchWithErrorHandling() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (searchQuery) => {
    setQuery(searchQuery);
    setError(null);
    
    startTransition(() => {
      searchProducts(searchQuery)
        .then(newResults => {
          setResults(newResults);
        })
        .catch(err => {
          setError(err.message);
        });
    });
  };

  return (
    <div>
      <SearchInput 
        value={query}
        onChange={handleSearch}
      />
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}
      
      {isPending ? (
        <LoadingSkeleton />
      ) : (
        <ProductGrid products={results} />
      )}
    </div>
  );
}

  

🎯 useDeferredValue: Optimizing Expensive Computations

useDeferredValue lets you defer updating non-critical parts of the UI, perfect for expensive computations or slow-rendering components.


import { useState, useDeferredValue, useMemo } from 'react';

function DataVisualization() {
  const [data, setData] = useState(largeDataset);
  const [filter, setFilter] = useState('');
  
  // Defer the expensive filtered data computation
  const deferredFilter = useDeferredValue(filter);
  
  // Memoize the expensive computation
  const filteredData = useMemo(() => {
    console.log('Filtering data...');
    
    // Simulate expensive computation
    return data.filter(item => 
      item.name.toLowerCase().includes(deferredFilter.toLowerCase())
    );
  }, [data, deferredFilter]);
  
  const handleFilterChange = (newFilter) => {
    setFilter(newFilter); // Input updates immediately
    // filteredData will update "lagging behind" with lower priority
  };

  return (
    <div className="dashboard">
      <input
        value={filter}
        onChange={(e) => handleFilterChange(e.target.value)}
        placeholder="Filter data..."
      />
      
      {/* This expensive component updates with lower priority */}
      <ExpensiveChart data={filteredData} />
    </div>
  );
}

// Combined useDeferredValue with useTransition
function AdvancedDataTable() {
  const [rows, setRows] = useState(initialRows);
  const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });
  const [globalFilter, setGlobalFilter] = useState('');
  
  const [isSorting, startSortTransition] = useTransition();
  const deferredFilter = useDeferredValue(globalFilter);
  
  // Memoize filtered and sorted data
  const processedData = useMemo(() => {
    console.log('Processing data...');
    
    let filtered = rows;
    
    // Apply global filter
    if (deferredFilter) {
      filtered = rows.filter(row =>
        Object.values(row).some(value =>
          String(value).toLowerCase().includes(deferredFilter.toLowerCase())
        )
      );
    }
    
    // Apply sorting
    return [...filtered].sort((a, b) => {
      const aValue = a[sortConfig.key];
      const bValue = b[sortConfig.key];
      
      if (sortConfig.direction === 'asc') {
        return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
      } else {
        return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
      }
    });
  }, [rows, deferredFilter, sortConfig]);

  const handleSort = (key) => {
    const direction = 
      sortConfig.key === key && sortConfig.direction === 'asc' 
        ? 'desc' 
        : 'asc';
    
    // Mark sorting as non-urgent transition
    startSortTransition(() => {
      setSortConfig({ key, direction });
    });
  };

  const handleGlobalFilter = (filter) => {
    setGlobalFilter(filter); // Input updates immediately
    // Data processing happens with lower priority
  };

  return (
    <div className="data-table-container">
      <div className="table-controls">
        <input
          value={globalFilter}
          onChange={(e) => handleGlobalFilter(e.target.value)}
          placeholder="Search all columns..."
        />
        
        {isSorting && <span className="sorting-indicator">Sorting...</span>}
      </div>
      
      <DataTable
        data={processedData}
        sortConfig={sortConfig}
        onSort={handleSort}
      />
    </div>
  );
}

// useDeferredValue for real-time data streams
function RealTimeDashboard() {
  const [sensorData, setSensorData] = useState(initialSensorData);
  const [visualizationComplexity, setVisualizationComplexity] = useState('medium');
  
  // Defer visualization updates to prevent jank during rapid data updates
  const deferredSensorData = useDeferredValue(sensorData);
  
  // Handle real-time data stream
  useEffect(() => {
    const ws = new WebSocket('ws://sensors.example.com/data');
    
    ws.onmessage = (event) => {
      const newData = JSON.parse(event.data);
      setSensorData(prev => [...prev, newData].slice(-1000)); // Keep last 1000 points
    };
    
    return () => ws.close();
  }, []);
  
  return (
    <div className="dashboard">
      <RealTimeControls 
        complexity={visualizationComplexity}
        onComplexityChange={setVisualizationComplexity}
      />
      
      {/* This expensive visualization updates with lower priority */}
      <SensorVisualization 
        data={deferredSensorData}
        complexity={visualizationComplexity}
      />
      
      <LiveMetrics data={sensorData} /> {/* Always shows latest data */}
    </div>
  );
}

  

🚀 Advanced Concurrent Patterns

Combine concurrent features with other React patterns for maximum performance impact in complex applications.


// Pattern 1: Nested Transitions for Complex UI Updates
function MultiStepForm() {
  const [formData, setFormData] = useState(initialData);
  const [validationErrors, setValidationErrors] = useState({});
  const [saveStatus, setSaveStatus] = useState('idle');
  
  const [isValidating, startValidationTransition] = useTransition();
  const [isSaving, startSaveTransition] = useTransition();

  const validateField = (field, value) => {
    startValidationTransition(() => {
      const errors = validateFieldLogic(field, value);
      setValidationErrors(prev => ({
        ...prev,
        [field]: errors
      }));
    });
  };

  const saveForm = async (data) => {
    startSaveTransition(async () => {
      setSaveStatus('saving');
      try {
        await api.saveForm(data);
        setSaveStatus('success');
      } catch (error) {
        setSaveStatus('error');
      }
    });
  };

  const handleFieldChange = (field, value) => {
    // Immediate UI update
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
    
    // Non-urgent validation
    validateField(field, value);
  };

  return (
    <form>
      {Object.entries(formData).map(([field, value]) => (
        <FormField
          key={field}
          field={field}
          value={value}
          error={validationErrors[field]}
          onChange={handleFieldChange}
          isValidationPending={isValidating}
        />
      ))}
      
      <button 
        onClick={() => saveForm(formData)}
        disabled={isSaving}
      >
        {isSaving ? 'Saving...' : 'Save Form'}
      </button>
    </form>
  );
}

// Pattern 2: Concurrent Data Fetching with Suspense
function UserDashboard() {
  const [selectedUserId, setSelectedUserId] = useState(null);
  const [isTransitioning, startTransition] = useTransition();

  const handleUserSelect = (userId) => {
    startTransition(() => {
      setSelectedUserId(userId);
    });
  };

  return (
    <div className="dashboard">
      <UserList onUserSelect={handleUserSelect} />
      
      <div className="main-content">
        {isTransitioning ? (
          <DashboardSkeleton />
        ) : (
          <Suspense fallback={<UserProfileSkeleton />}>
            {selectedUserId && (
              <UserProfile userId={selectedUserId} />
            )}
          </Suspense>
        )}
      </div>
    </div>
  );
}

// Pattern 3: Optimistic Updates with Concurrent Rendering
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [optimisticTodos, setOptimisticTodos] = useState([]);
  const [isSyncing, startSyncTransition] = useTransition();

  const addTodo = async (text) => {
    const optimisticTodo = {
      id: `temp-${Date.now()}`,
      text,
      completed: false,
      isOptimistic: true
    };

    // Immediate optimistic update
    setOptimisticTodos(prev => [...prev, optimisticTodo]);
    
    // Non-urgent server sync
    startSyncTransition(async () => {
      try {
        const savedTodo = await api.addTodo(text);
        
        // Replace optimistic todo with real one
        setTodos(prev => [...prev, savedTodo]);
        setOptimisticTodos(prev => 
          prev.filter(todo => todo.id !== optimisticTodo.id)
        );
      } catch (error) {
        // Rollback optimistic update
        setOptimisticTodos(prev => 
          prev.filter(todo => todo.id !== optimisticTodo.id)
        );
        // Show error message
      }
    });
  };

  const displayedTodos = [...todos, ...optimisticTodos];

  return (
    <div>
      <AddTodoForm onSubmit={addTodo} />
      
      {isSyncing && <SyncIndicator />}
      
      <TodoListItems 
        todos={displayedTodos}
        isSyncing={isSyncing}
      />
    </div>
  );
}

// Pattern 4: Concurrent Pagination and Infinite Scroll
function ProductCatalog() {
  const [products, setProducts] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [isLoadingNextPage, startLoadTransition] = useTransition();

  const loadMoreProducts = async () => {
    startLoadTransition(async () => {
      const nextPage = page + 1;
      const newProducts = await api.getProducts(nextPage);
      
      if (newProducts.length === 0) {
        setHasMore(false);
      } else {
        setProducts(prev => [...prev, ...newProducts]);
        setPage(nextPage);
      }
    });
  };

  return (
    <div className="catalog">
      <ProductGrid products={products} />
      
      {hasMore && (
        <button 
          onClick={loadMoreProducts}
          disabled={isLoadingNextPage}
          className="load-more-btn"
        >
          {isLoadingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

// Pattern 5: Concurrent Image Loading and Transitions
function ImageGallery() {
  const [images, setImages] = useState([]);
  const [selectedImage, setSelectedImage] = useState(null);
  const [isTransitioning, startTransition] = useTransition();

  const handleImageSelect = (image) => {
    startTransition(() => {
      setSelectedImage(image);
    });
  };

  const loadMoreImages = async () => {
    startTransition(async () => {
      const newImages = await api.getImages();
      setImages(prev => [...prev, ...newImages]);
    });
  };

  return (
    <div className="gallery">
      <div className="thumbnails">
        {images.map(image => (
          <img
            key={image.id}
            src={image.thumbnail}
            onClick={() => handleImageSelect(image)}
            className="thumbnail"
          />
        ))}
      </div>
      
      <div className="viewer">
        {isTransitioning ? (
          <ImagePlaceholder />
        ) : selectedImage ? (
          <Suspense fallback={<ImageLoader />}>
            <FullSizeImage image={selectedImage} />
          </Suspense>
        ) : null}
      </div>
    </div>
  );
}

  

📊 Performance Monitoring and Optimization

Measure and optimize your concurrent React applications with these advanced techniques.

  • React DevTools Profiler: Analyze component render times and priorities
  • User Timing API: Measure real-world performance metrics
  • Bundle Analysis: Identify and optimize large dependencies
  • Lighthouse CI: Automated performance regression testing
  • Core Web Vitals: Monitor INP, LCP, and CLS in production

⚡ Key Takeaways

  1. Prioritize User Interactions: Use useTransition to keep the UI responsive during heavy operations
  2. Defer Expensive Work: Leverage useDeferredValue for computationally intensive tasks
  3. Combine with Memoization: Use useMemo and React.memo with concurrent features for maximum performance
  4. Progressive Enhancement: Implement loading states and optimistic updates for better UX
  5. Measure and Iterate: Continuously monitor performance and optimize based on real metrics
  6. Error Boundaries: Implement proper error handling for failed transitions
  7. Accessibility: Ensure loading states and transitions are accessible to all users

❓ Frequently Asked Questions

When should I use useTransition vs useDeferredValue?
Use useTransition when you need to mark state updates as non-urgent and want to show loading states. Use useDeferredValue when you have a value that's expensive to compute and you want it to "lag behind" the latest value. useTransition is about controlling when state updates happen, while useDeferredValue is about controlling when derived values update.
Do concurrent features work with server-side rendering?
Yes, concurrent features work with SSR through React's streaming capabilities. However, useTransition and useDeferredValue are client-side only features. For SSR, focus on Suspense for data fetching and selective hydration to prioritize critical content.
Can I use multiple transitions in the same component?
Absolutely! You can have multiple useTransition hooks in a single component to manage different types of non-urgent updates with separate loading states. This is useful when you have independent async operations that shouldn't block each other.
How do concurrent features affect testing?
Testing concurrent features requires using React's act() utility and potentially adding small delays to account for transition states. Consider using React Testing Library's async utilities and mock timers to properly test the timing and loading states of your concurrent components.
Are there performance overheads to using concurrent features?
There's minimal overhead for the concurrent features themselves, but the main cost comes from the additional renders (showing loading states, then final states). However, this is almost always outweighed by the improved perceived performance and user experience.
How do I handle errors in transitions?
Wrap your transition logic in try-catch blocks and use error states to show appropriate error messages. You can also combine transitions with error boundaries for more robust error handling. Remember to reset error states when starting new transitions.

💬 Have you implemented concurrent features in your React applications? Share your performance improvements, challenges, or best practices in the comments below! If you found this guide helpful, please share it with your team or on social media to help others master React performance optimization.

About LK-TECH Academy — Practical tutorials & explainers on software engineering, AI, and infrastructure. Follow for concise, hands-on guides.

Wednesday, 8 October 2025

Building Real-Time Collaborative Editor with Operational Transforms & Node.js

October 08, 2025 0

Building a Real-Time Collaborative Editor with Operational Transforms (OT) and Node.js

Real-time collaborative code editor built with Operational Transforms and Node.js - LK-TECH Academy tutorial on building Google Docs-like collaborative editing

Imagine multiple users editing the same document simultaneously without conflicts, version control nightmares, or data loss. This isn't magic—it's Operational Transform (OT), the same technology powering Google Docs, Notion, and other collaborative applications. In this comprehensive guide, you'll learn how to build your own real-time collaborative editor from scratch using Node.js, Socket.IO, and the Operational Transform algorithm. By the end, you'll have a fully functional collaborative text editor that handles concurrent edits gracefully.

🚀 Understanding Operational Transforms

Operational Transform is a conflict resolution algorithm designed specifically for real-time collaborative editing. When multiple users edit the same document simultaneously, their operations (insertions, deletions) need to be transformed to maintain consistency across all clients.

The core problem OT solves: if User A inserts text at position 5, and User B deletes text at position 3, how do we ensure both operations are applied correctly without breaking the document state?

📋 Core OT Operations

  • Insert(position, text): Adds text at specified position
  • Delete(position, length): Removes text starting from position
  • Retain(position, length): Keeps text unchanged

💻 Project Architecture

Our collaborative editor will consist of:

  • Node.js backend with Express and Socket.IO
  • Frontend with vanilla JavaScript and Socket.IO client
  • OT algorithm implementation for conflict resolution
  • Document versioning and operation history

🔧 Setting Up the Server

Let's start by setting up our Node.js server with the necessary dependencies.

💻 Server Setup Code


// package.json dependencies
{
  "name": "collaborative-editor",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.2",
    "socket.io": "^4.7.2",
    "uuid": "^9.0.0"
  }
}

// server.js - Basic setup
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const { v4: uuidv4 } = require('uuid');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});

// Store documents and their operations
const documents = new Map();

app.use(express.static('public'));

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  
  socket.on('join-document', (docId) => {
    socket.join(docId);
    
    if (!documents.has(docId)) {
      documents.set(docId, {
        content: '',
        operations: [],
        version: 0
      });
    }
    
    const doc = documents.get(docId);
    socket.emit('document-state', {
      content: doc.content,
      version: doc.version
    });
  });
  
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

  

🔍 Implementing Operational Transform Algorithm

The heart of our collaborative editor is the OT algorithm. Let's implement the core transformation functions.

💻 OT Algorithm Implementation


// ot.js - Operational Transform implementation
class OperationalTransform {
  static transform(op1, op2) {
    // Transform op1 against op2
    let transformedOp = [...op1];
    
    for (let i = 0; i < op2.length; i++) {
      const component = op2[i];
      
      if (component.insert) {
        transformedOp = this.transformAgainstInsert(transformedOp, component);
      } else if (component.delete) {
        transformedOp = this.transformAgainstDelete(transformedOp, component);
      } else if (component.retain) {
        transformedOp = this.transformAgainstRetain(transformedOp, component);
      }
    }
    
    return transformedOp;
  }
  
  static transformAgainstInsert(op, insertOp) {
    const transformed = [];
    const insertPos = insertOp.insert.position;
    const insertLength = insertOp.insert.text.length;
    
    for (const component of op) {
      if (component.insert) {
        let pos = component.insert.position;
        if (pos >= insertPos) {
          pos += insertLength;
        }
        transformed.push({ insert: { position: pos, text: component.insert.text } });
      } else if (component.delete) {
        let start = component.delete.position;
        let end = start + component.delete.length;
        
        if (start >= insertPos) {
          start += insertLength;
          end += insertLength;
        } else if (end > insertPos) {
          end += insertLength;
        }
        
        transformed.push({ delete: { position: start, length: end - start } });
      } else if (component.retain) {
        let start = component.retain.position;
        let end = start + component.retain.length;
        
        if (start >= insertPos) {
          start += insertLength;
          end += insertLength;
        } else if (end > insertPos) {
          end += insertLength;
        }
        
        transformed.push({ retain: { position: start, length: end - start } });
      }
    }
    
    return transformed;
  }
  
  static transformAgainstDelete(op, deleteOp) {
    const transformed = [];
    const deleteStart = deleteOp.delete.position;
    const deleteEnd = deleteStart + deleteOp.delete.length;
    
    for (const component of op) {
      if (component.insert) {
        let pos = component.insert.position;
        if (pos > deleteEnd) {
          pos -= (deleteEnd - deleteStart);
        } else if (pos > deleteStart) {
          pos = deleteStart;
        }
        transformed.push({ insert: { position: pos, text: component.insert.text } });
      } else if (component.delete) {
        let start = component.delete.position;
        let end = start + component.delete.length;
        
        // Handle overlapping deletions
        if (start >= deleteEnd) {
          start -= (deleteEnd - deleteStart);
          end -= (deleteEnd - deleteStart);
        } else if (end <= deleteStart) {
          // No change needed
        } else if (start < deleteStart && end > deleteEnd) {
          end -= (deleteEnd - deleteStart);
        } else if (start >= deleteStart && end <= deleteEnd) {
          continue; // This deletion is completely covered
        } else if (start < deleteStart) {
          end = deleteStart;
        } else if (end > deleteEnd) {
          start = deleteStart;
          end -= (deleteEnd - deleteStart);
        }
        
        if (end > start) {
          transformed.push({ delete: { position: start, length: end - start } });
        }
      } else if (component.retain) {
        let start = component.retain.position;
        let end = start + component.retain.length;
        
        if (start >= deleteEnd) {
          start -= (deleteEnd - deleteStart);
          end -= (deleteEnd - deleteStart);
        } else if (end <= deleteStart) {
          // No change needed
        } else if (start < deleteStart && end > deleteEnd) {
          end -= (deleteEnd - deleteStart);
        } else if (start >= deleteStart && end <= deleteEnd) {
          continue; // This retain is completely covered by deletion
        } else if (start < deleteStart) {
          end = deleteStart;
        } else if (end > deleteEnd) {
          start = deleteStart;
          end -= (deleteEnd - deleteStart);
        }
        
        if (end > start) {
          transformed.push({ retain: { position: start, length: end - start } });
        }
      }
    }
    
    return transformed;
  }
  
  static transformAgainstRetain(op, retainOp) {
    // Retain operations don't change other operations
    return op;
  }
  
  static applyOperation(content, operation) {
    let result = content;
    
    // Sort operations by position to apply correctly
    const sortedOps = [...operation].sort((a, b) => {
      const posA = a.insert ? a.insert.position : a.delete ? a.delete.position : a.retain.position;
      const posB = b.insert ? b.insert.position : b.delete ? b.delete.position : b.retain.position;
      return posA - posB;
    });
    
    let offset = 0;
    
    for (const component of sortedOps) {
      if (component.insert) {
        const pos = component.insert.position + offset;
        result = result.slice(0, pos) + component.insert.text + result.slice(pos);
        offset += component.insert.text.length;
      } else if (component.delete) {
        const pos = component.delete.position + offset;
        result = result.slice(0, pos) + result.slice(pos + component.delete.length);
        offset -= component.delete.length;
      }
      // Retain operations don't change the content
    }
    
    return result;
  }
}

module.exports = OperationalTransform;

  

🔗 Socket.IO Event Handling

Now let's implement the real-time communication between clients and server.

💻 Enhanced Server with OT Logic


// enhanced-server.js
const OperationalTransform = require('./ot');

// Enhanced socket event handling
io.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  
  socket.on('join-document', (docId) => {
    socket.join(docId);
    
    if (!documents.has(docId)) {
      documents.set(docId, {
        content: '',
        operations: [],
        version: 0,
        clients: new Map()
      });
    }
    
    const doc = documents.get(docId);
    doc.clients.set(socket.id, {
      version: doc.version,
      pendingOps: []
    });
    
    socket.emit('document-state', {
      content: doc.content,
      version: doc.version
    });
  });
  
  socket.on('operation', (data) => {
    const { docId, operation, version } = data;
    const doc = documents.get(docId);
    
    if (!doc) return;
    
    const clientState = doc.clients.get(socket.id);
    if (!clientState) return;
    
    // Check if client is behind
    if (version < doc.version) {
      // Client needs to catch up
      const missingOps = doc.operations.slice(version);
      let transformedOp = operation;
      
      for (const missingOp of missingOps) {
        transformedOp = OperationalTransform.transform(transformedOp, missingOp);
      }
      
      // Apply the transformed operation
      doc.content = OperationalTransform.applyOperation(doc.content, transformedOp);
      doc.operations.push(transformedOp);
      doc.version++;
      
      // Update all clients
      socket.to(docId).emit('remote-operation', {
        operation: transformedOp,
        version: doc.version
      });
      
      // Send acknowledgment to originating client
      socket.emit('operation-ack', {
        version: doc.version
      });
    } else if (version === doc.version) {
      // Client is up to date
      doc.content = OperationalTransform.applyOperation(doc.content, operation);
      doc.operations.push(operation);
      doc.version++;
      
      // Broadcast to other clients
      socket.to(docId).emit('remote-operation', {
        operation: operation,
        version: doc.version
      });
      
      // Acknowledge to originating client
      socket.emit('operation-ack', {
        version: doc.version
      });
    } else {
      // Client is ahead (shouldn't happen) - send sync
      socket.emit('document-state', {
        content: doc.content,
        version: doc.version
      });
    }
    
    // Update client state
    clientState.version = doc.version;
  });
  
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
    
    // Clean up client states from all documents
    for (const [docId, doc] of documents) {
      if (doc.clients.has(socket.id)) {
        doc.clients.delete(socket.id);
        
        // If no clients, consider cleaning up the document
        if (doc.clients.size === 0) {
          // Optional: persist document before cleanup
          // documents.delete(docId);
        }
      }
    }
  });
});

  

⚡ Key Takeaways

  1. Operational Transform is fundamental for conflict-free collaborative editing by mathematically transforming concurrent operations
  2. Version control is crucial for maintaining consistency across multiple clients with different network latencies
  3. Real-time communication using WebSockets (Socket.IO) enables instant updates across all connected clients
  4. Conflict resolution happens automatically through the OT algorithm without user intervention
  5. Advanced features like presence indicators and remote cursors significantly enhance user experience

❓ Frequently Asked Questions

How does Operational Transform differ from Conflict-free Replicated Data Types (CRDT)?
OT requires a central server to transform operations and maintain consistency, while CRDTs are designed for peer-to-peer systems where any replica can merge changes without coordination. OT is generally more efficient for text editing but requires more complex server logic.
What happens when network connectivity is lost?
The client continues to work offline, queueing operations locally. When connectivity is restored, the client sends all pending operations to the server, which transforms them against any missed server operations and updates the document state accordingly.
How scalable is this implementation for large numbers of users?
The basic implementation can handle dozens of concurrent users on a single document. For larger scale, you'd need to implement operation compression, consider using Redis for shared state across multiple server instances, and potentially partition documents across different server nodes.
Can this handle rich text formatting and embedded objects?
Yes, but it requires extending the OT algorithm to handle different operation types. For rich text, you'd need operations for applying formatting ranges, inserting images, creating tables, etc. Each new content type requires defining how its operations transform against other operations.
How do you handle security and access control in a collaborative editor?
You can implement authentication middleware for Socket.IO connections, document-level permissions (read-only, comment-only, edit), operation validation to prevent malicious edits, and audit logging for compliance requirements. Always validate operations server-side before applying them.

💬 Found this article helpful? Please leave a comment below or share it with your network to help others learn! Have you implemented collaborative features in your projects? Share your experiences and challenges!

About LK-TECH Academy — Practical tutorials & explainers on software engineering, AI, and infrastructure. Follow for concise, hands-on guides.

Monday, 28 December 2020

Angular share data between parent child components using @Input and @Output decorators

December 28, 2020 0
Angular share data between parent child components


Last chapter we have discussed how we can share data between two unrelated components using behavioral subjects in Angular 8. Today I am going to explain how you can make an interaction between parent and child component using Angular.

Before moving forward, I will generate one component in my flower store angular app. Flower store is the app we used to demonstrate the angular concept talked in series of chapters in this blog. 

To generate the component, please run below ng CLI command in your command prompt. 

ng g c flower-card

Above command will generate the flower-card.component.ts and corresponding html and CSS files as below.


run cli command ng g c flower-card to generate the component in angular

Angular Related Articles:


In our flower app component landing page view has cards for each flower as below.

landing page of my flower store angular app


Currently your landing.component.html includes all the code to display cards. Below commented lines used to shows cards for each flower. 


<h1>Welcome to my flower store</h1>

    <div *ngFor="let flower of myFlowerList;trackBy:trackFlowers">
        <!-- <p-card header="{{flower.name}}"  [style]="{width: '360px','float':'left','margin-bottom': '2em','margin-right': '2em','background-color':'light-pink'}" styleClass="p-card-shadow">
                <img alt="Card" width="100" height="100" src="assets/stock/{{flower.name}}.jpg">
            <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt
                quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque quas!</p>
                <p-checkbox name="flower" [style]="{'margin-top': '2em'}" value="{{flower.name}}" label="Order Now" [(ngModel)]="selectedFlowers"></p-checkbox>
             
            </p-card> -->
    </div>

    <div>Your search text is: {{searchText}}</div>

    <ul>
        <li *ngFor="let selectedFlower of selectedFlowers">
            <input id="select-user" type="checkbox" (change)="onCheckboxChange(selectedFlower,$event)"> {{selectedFlower}}

        </li>
    </ul>


I am going to replace commented lines by adding selected tags of flower-card component and I am going to move card displaying logic in to flower-card.component.html.

<h1>Welcome to my flower store</h1>

    <div *ngFor="let flower of myFlowerList;trackBy:trackFlowers">
      <flower-card></flower-card>
    </div>

    <div>Your search text is: {{searchText}}</div>

    <ul>
        <li *ngFor="let selectedFlower of selectedFlowers">
            <input id="select-user" type="checkbox" (change)="onCheckboxChange(selectedFlower,$event)"> {{selectedFlower}}

        </li>
    </ul>

<flower-card> is a selector tag in the flower-card component file. Below is the flower-card.component.ts file code for you to understand.

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'flower-card', // selector 
  templateUrl: './flower-card.component.html',
  styleUrls: ['./flower-card.component.scss']
})
export class FlowerCardComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }
}


Now if I run my app you will see below screen because I still did not add any codes to flower-card.component.html file to shows flowers as a card.

flower-card.component.html file to shows flowers as a card

Pass data between parent component to child component in Angular

Now let’s focus on how to pass data from parent component (landing.component.ts) to child component (flower-card.component.ts). 

Now my flower array is defined inside the parent component and I am going to display data in child component. Now I must find a way to transfer data from parent to child component. 

In this case, we send the data from the parent component to the child component using an attribute. This attribute can then be accessed within the child component using the @input decorator.

Properties I need to pass from parent to child are only title or name of the flower since I used dummy text to display description.

I am using the title to read the image also. 

Now let us check how we can bind the attributes to child component from parent component below. There are two ways you can bind data.

<flower-card title="{{flower.name}}"></flower-card>

OR

<flower-card [title]="flower.name"></flower-card>

You can bind another attribute like description as below.

<flower-card [title]="flower.name" [description]="flower.description"></flower-card>

<h1>Welcome to my flower store</h1>

    <div *ngFor="let flower of myFlowerList;trackBy:trackFlowers">
      <!-- <flower-card title="{{flower.name}}"></flower-card> -->
      <flower-card [title]="flower.name"></flower-card>
   </div>

    <div>Your search text is: {{searchText}}</div>

    <ul>
        <li *ngFor="let selectedFlower of selectedFlowers">
            <input id="select-user" type="checkbox" (change)="onCheckboxChange(selectedFlower,$event)"> {{selectedFlower}}

        </li>
    </ul>


Now we have banded data from parent component to child component. Let us see how we can read these data from child component <flower-card-component>

To read data you should defined @Input() decorators. You need to import Input decorator from @angular/core to use @Input.

@Input() title:string =''

flower-card.component.ts file code.

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'flower-card', // selector 
  templateUrl: './flower-card.component.html',
  styleUrls: ['./flower-card.component.scss']
})
export class FlowerCardComponent implements OnInit {

  constructor() { }
  
  @Input() title:string =''
  
  ngOnInit() {
  
  }
}}

Then I will copy paste commented code in parent component html (landing.component.html) file to child component html file and alter the file to display @input type variable value. (flower-card-component.html).

Since I am using PrimeNG component to display card and checkbox I will import PrimeNG modules in flower-card.component.ts file. Then I will move card style related code from landing.component.sccs to flower-card.component.scss file.

flower-card.component.html code:

<p-card header="{{title}}"  [style]="{width: '360px','float':'left','margin-bottom': '2em','margin-right': '2em','background-color':'light-pink'}" styleClass="p-card-shadow">
    <img alt="Card" width="100" height="100" src="assets/stock/{{title}}.jpg">
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt
        quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque quas!</p>
    <p-checkbox name="flower" [style]="{'margin-top': '2em'}" value="{{title}}" label="Order Now" [(ngModel)]="selectedFlowers"></p-checkbox>
</p-card>

flower-card.component.scss code:

.ui-card-title{
    background-color: lightpink !important;
}

.ui-chkbox-label{
    vertical-align:bottom;
}

Pass data between child component to parent component in Angular

In our previous application when user click on the checkboxes in each card checkbox value is bonded to the array using ngModel. So selected flower names will be stored in an array and you can directly print the value by iterating the array.

Now our checkbox is in child component and our printing logic is in parent component. So, you need to find a way to print the value when and checkbox checked event called in the child component.

To have child to parent communication we can capture the change in data due to any event within the child component. This event is then propagated to the parent component using the @Output decorator and Eventemitter.

First, I will define the object as below.

@Output() selectedFlower = new EventEmitter<string>();

To use @Output and EventEmitter you must import EventEmitter and Output component from @angular/core library.

In flower-card.component.html file I have added the check box change event. So, I can capture that event and emit it value to parent component as below. Since I used PrimeNG checkbox you must use (onChange) event.

<p-card header="{{title}}"  [style]="{width: '360px','float':'left','margin-bottom': '2em','margin-right': '2em','background-color':'light-pink'}" styleClass="p-card-shadow">
    <img alt="Card" width="100" height="100" src="assets/stock/{{title}}.jpg">
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt
        quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque quas!</p>
    <p-checkbox name="flower" [style]="{'margin-top': '2em'}" value="{{title}}" label="Order Now" (onChange)="selectCheckBox($event,title)" [(ngModel)]="selectedFlowers"></p-checkbox>
</p-card>

Then I am going to define the selectCheckBox event in flower-card.component.ts to emit the selected value.

selectCheckBox(value,name){
    this.selectedFlower.emit(name);
  }

import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import {CardModule} from 'primeng/card';
import {CheckboxModule} from 'primeng/checkbox';

@Component({
  selector: 'flower-card', // selector 
  templateUrl: './flower-card.component.html',
  styleUrls: ['./flower-card.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class FlowerCardComponent implements OnInit {

  constructor() { }

  @Input() title:string =''
  selectedFlowers: string[] = [];
  @Output() selectedFlower = new EventEmitter<string>();

  
  ngOnInit() {
  
  }

  selectCheckBox(value,name){
    this.selectedFlower.emit(name);
  }
}

Now we have implemented the code in child component. Let’s see how you can capture this emitted value and print it in a parent component.

For that you must bind the emitted event in a child component selected tag as below.

<flower-card [title]="flower.name" (selectedFlower)="printOrder($event)"></flower-card>

Here the bind event name should be same as the @Output name we defined in child component.
So now whenever user click on child component checkboxes that event is emitted to the parent component and printOrder() method defined in parent component will be executed. $event argument will contain the emitted value.

Inside the printOrder() method I have implemented the logic to check values is exist, if exist value will be remove from the array and if not value will be added to the array.

printOrder(flowerName){
    if(this.selectedFlowers.indexOf(flowerName)<0){
      this.selectedFlowers.push(flowerName)
    }
    else{
      let index = this.selectedFlowers.indexOf(flowerName);
      this.selectedFlowers.splice(index,1);
    }
}

Parent component code:

<h1>Welcome to my flower store</h1>

    <div *ngFor="let flower of myFlowerList;trackBy:trackFlowers">
      <flower-card [title]="flower.name" (selectedFlower)="printOrder($event)"></flower-card>
       </div>

    <div>Your have selected: {{searchText}}</div>

    <ul>
        <li *ngFor="let selectedFlower of selectedFlowers">
             {{selectedFlower}}

        </li>
    </ul>


import { Component, EventEmitter, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { flower } from '../../domain/flower';
import { DataService } from 'src/app/services/data.service';

@Component({
  selector: 'app-landing',
  templateUrl: './landing.component.html',
  styleUrls: ['./landing.component.scss'],
 
})
export class LandingComponent implements OnInit {

  myFlowerList:flower[]=[];
  selectedFlowers: string[] =[];

  checkedList:string[]=[];
  searchText:string='';
  constructor(private dataService:DataService) { }

  ngOnInit() {
    this.mySellingFlowers();
    this.dataService.getSearchText().subscribe(response => {
      this.printSearchtext(response);
    })
  }

  printSearchtext(searchText){
    this.searchText = searchText;
  }

  printOrder(flowerName){
    if(this.selectedFlowers.indexOf(flowerName)<0){
      this.selectedFlowers.push(flowerName)
    }
    else{
      let index = this.selectedFlowers.indexOf(flowerName);
      this.selectedFlowers.splice(index,1);
    }

  }

  mySellingFlowers(){
    let rose = new flower();
    rose.name = "Rose";
    rose.price = 100;
    rose.availableQuantity = 1000;
    rose.isChecked = false;
    this. myFlowerList.push(rose);

    let lily = new flower();
    lily.name = "Lilly";
    lily.price = 80;
    lily.availableQuantity = 2000;
    lily.isChecked = false;
    this. myFlowerList.push(lily);

    let tulip = new flower();
    tulip.name = "Tulip";
    tulip.price = 100;
    tulip.availableQuantity = 2300;
    lily.isChecked = false;

    this. myFlowerList.push(tulip);

    let carnation = new flower();
    carnation.name = "Carnation";
    carnation.price = 50;
    carnation.availableQuantity = 1500;
    lily.isChecked = false;

    this. myFlowerList.push(carnation);

    let gerbera = new flower();
    gerbera.name = "Gerbera";
    gerbera.price = 50;
    gerbera.availableQuantity = 1500;
    lily.isChecked = false;

    this. myFlowerList.push(gerbera);

    let orchid = new flower();
    orchid.name = "Orchid";
    orchid.price = 50;
    orchid.availableQuantity = 1500;
    lily.isChecked = false;

    this. myFlowerList.push(orchid);

  }

  trackFlowers(index,flower){
    return flower?flower.name:undefined
  }
}

Now see the magic in your browser by clicking on the checkboxes.

click on checkboxes to select items in store


You can find complete source code related to the angular implementation we have done so far here in GitHub.