Data Flow and Communication Patterns
1. Unidirectional Data Flow (Flux, Redux Architecture)
| Concept | Pattern | Description | Benefit |
|---|---|---|---|
| Flux Architecture | Action → Dispatcher → Store → View |
One-way data flow where actions trigger state changes, views subscribe to updates | Predictable state changes, easier debugging, time-travel capability |
| Actions | { type, payload } |
Plain objects describing what happened, dispatched to modify state | Serializable events, action log for debugging, middleware intercept |
| Reducers | (state, action) => newState |
Pure functions calculating next state based on current state and action | Testable, predictable, composable state transitions |
| Store | createStore(reducer) |
Single source of truth holding entire application state tree | Centralized state, subscription mechanism, middleware pipeline |
| Middleware | store => next => action |
Pipeline for side effects, logging, async actions (thunk, saga) | Separates business logic from UI, reusable effects |
Example: Flux Pattern with Redux
// Action Types
const INCREMENT = 'counter/increment';
const FETCH_USER = 'user/fetch';
// Action Creators
const increment = () => ({ type: INCREMENT });
const fetchUserAsync = (id) => async (dispatch) => {
dispatch({ type: FETCH_USER + '/pending' });
try {
const user = await api.getUser(id);
dispatch({ type: FETCH_USER + '/fulfilled', payload: user });
} catch (error) {
dispatch({ type: FETCH_USER + '/rejected', payload: error });
}
};
// Reducer (Pure Function)
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case INCREMENT:
return { ...state, value: state.value + 1 };
default:
return state;
}
}
// Store with Middleware
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(counterReducer, applyMiddleware(thunk));
// Unidirectional Flow
// View dispatches action
store.dispatch(increment());
// Reducer produces new state
// Store notifies subscribers
// View re-renders with new state
Flux Principles
- Single Direction: Data flows only one way (Action → Store → View)
- Predictability: Same actions + same state = same result
- Centralized State: One store, one source of truth
- Immutability: State never mutated directly, always replaced
2. Event-Driven Architecture (Pub/Sub, EventEmitter)
| Concept | Pattern | Description | Use Case |
|---|---|---|---|
| Event Emitter | EventEmitter |
Publish-subscribe pattern for decoupled component communication | Cross-component events, micro-frontend communication |
| Custom Events | new CustomEvent() |
Browser native event system for inter-component messaging | Web components, widget communication, analytics tracking |
| Event Bus | eventBus.emit/on |
Global message bus for application-wide events | Global notifications, cross-feature communication |
| RxJS Observables | Subject, BehaviorSubject |
Reactive streams for event handling with operators (map, filter, debounce) | Complex event flows, async operations, real-time updates |
| Message Queue | postMessage() |
Cross-origin communication between windows, iframes, workers | Micro-frontends, iframe isolation, web workers |
Event Emitter Pattern
// Simple Event Emitter
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
// Return unsubscribe function
return () => this.off(event, listener);
}
off(event, listener) {
if (!this.events[event]) return;
this.events[event] = this.events[event]
.filter(l => l !== listener);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(listener =>
listener(data)
);
}
once(event, listener) {
const onceWrapper = (data) => {
listener(data);
this.off(event, onceWrapper);
};
this.on(event, onceWrapper);
}
}
// Usage
const bus = new EventEmitter();
bus.on('user:login', (user) => {
console.log('User logged in:', user);
});
bus.emit('user:login', { id: 1, name: 'John' });
RxJS Observable Pattern
import { Subject, BehaviorSubject } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
// Subject - hot observable
const userEvents$ = new Subject();
userEvents$.subscribe(event => {
console.log('User event:', event);
});
userEvents$.next({ type: 'click', target: 'button' });
// BehaviorSubject - stores current value
const currentUser$ = new BehaviorSubject(null);
currentUser$.subscribe(user => {
console.log('Current user:', user);
});
currentUser$.next({ id: 1, name: 'John' });
// Search input with debounce
const searchInput$ = new Subject();
searchInput$
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(query => {
console.log('Search:', query);
// Trigger API call
});
// Emit search queries
searchInput$.next('react');
searchInput$.next('redux'); // Debounced
Example: Custom Events for Web Components
// Dispatch custom event
class UserCard extends HTMLElement {
connectedCallback() {
this.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('user-selected', {
detail: { userId: this.dataset.userId },
bubbles: true,
composed: true // Cross shadow DOM boundaries
}));
});
}
}
// Listen for custom event
document.addEventListener('user-selected', (event) => {
console.log('User selected:', event.detail.userId);
// Load user details, navigate, etc.
});
// React integration
function App() {
useEffect(() => {
const handler = (e) => console.log(e.detail);
document.addEventListener('user-selected', handler);
return () => document.removeEventListener('user-selected', handler);
}, []);
}
Caution: Event-driven patterns can lead to hidden dependencies
and difficult debugging. Use typed events, clear naming conventions, and consider Redux DevTools for observable
tracking.
3. Real-time Communication (WebSocket, Socket.io)
| Technology | API | Description | Use Case |
|---|---|---|---|
| WebSocket API | new WebSocket(url) |
Native browser API for full-duplex bidirectional communication over TCP | Real-time apps, low latency, persistent connection |
| Socket.IO | io() |
Library with automatic reconnection, fallbacks, rooms, namespaces | Chat apps, live collaboration, multiplayer games |
| Heartbeat/Ping-Pong | socket.ping() |
Keep-alive mechanism to detect disconnections early | Connection health monitoring, automatic reconnection |
| Rooms/Channels | socket.join(room) |
Broadcast messages to specific groups of connected clients | Chat rooms, game lobbies, team workspaces |
| Binary Support | ArrayBuffer, Blob |
Send binary data efficiently (images, files, audio) | File transfers, video streaming, game state sync |
Example: WebSocket and Socket.IO Implementation
// Native WebSocket
const ws = new WebSocket('wss://example.com/socket');
ws.onopen = () => {
console.log('Connected');
ws.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Message:', data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Disconnected');
// Implement reconnection logic
setTimeout(() => reconnect(), 1000);
};
// Socket.IO Client
import { io } from 'socket.io-client';
const socket = io('https://example.com', {
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
transports: ['websocket', 'polling'] // Fallback to polling
});
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('message', (data) => {
console.log('Received:', data);
});
socket.emit('chat-message', { room: 'general', text: 'Hello!' });
// Join room
socket.emit('join-room', 'room-123');
// React Hook for Socket.IO
function useSocket(url) {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const newSocket = io(url);
newSocket.on('connect', () => setIsConnected(true));
newSocket.on('disconnect', () => setIsConnected(false));
setSocket(newSocket);
return () => newSocket.close();
}, [url]);
return { socket, isConnected };
}
WebSocket vs Socket.IO
| Feature | WebSocket | Socket.IO |
|---|---|---|
| Protocol | Native WS protocol | Custom protocol |
| Reconnection | Manual implementation | Automatic |
| Fallback | None | Long polling |
| Rooms | Manual | Built-in |
| Bundle Size | 0KB (native) | ~50KB |
Best Practices
- Use
wss://(secure WebSocket) in production - Implement exponential backoff for reconnection
- Validate and sanitize all incoming messages
- Set connection timeout and heartbeat interval
- Use binary format (MessagePack) for efficiency
- Handle offline state gracefully with queue
- Monitor connection status in UI
- Close connections properly on unmount
4. Server-Sent Events (SSE, Event Stream)
| Concept | API | Description | Advantage |
|---|---|---|---|
| Server-Sent Events | new EventSource(url) |
One-way server-to-client push over HTTP, automatic reconnection | Simpler than WebSocket for one-way updates, HTTP/2 compatible |
| Text-Based Protocol | data: message\n\n |
UTF-8 text format with event types, IDs, retry configuration | Human-readable, easy debugging, works through proxies |
| Auto Reconnection | retry: 3000 |
Browser automatically reconnects with last event ID | Resilient connections without custom retry logic |
| Event Types | event: custom-event |
Named events for different message types from single endpoint | Type-safe event handling, multiple event listeners |
| HTTP Streaming | Transfer-Encoding: chunked |
Keeps HTTP connection open, sends chunks incrementally | Works with existing HTTP infrastructure, CDN support |
Example: Server-Sent Events Implementation
// Client: EventSource API
const eventSource = new EventSource('/api/events');
// Default message event
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Message:', data);
};
// Custom event types
eventSource.addEventListener('user-update', (event) => {
const user = JSON.parse(event.data);
console.log('User updated:', user);
});
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showToast(notification.message);
});
// Connection lifecycle
eventSource.onopen = () => {
console.log('SSE connection opened');
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};
// Close connection
eventSource.close();
// Server: Node.js/Express
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send initial message
res.write('data: {"message": "Connected"}\n\n');
// Send periodic updates
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 1000);
// Custom event with ID
res.write('event: user-update\n');
res.write('id: 123\n');
res.write('data: {"name": "John", "status": "online"}\n\n');
// Set retry timeout (milliseconds)
res.write('retry: 10000\n\n');
// Cleanup on client disconnect
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
When to use SSE: Perfect for one-way server push scenarios.
Simpler than WebSocket, works through firewalls, automatic reconnection, and HTTP/2 multiplexing support.
5. GraphQL Subscriptions (Real-time Updates, Apollo)
| Concept | Implementation | Description | Use Case |
|---|---|---|---|
| GraphQL Subscriptions | subscription { ... } |
Real-time GraphQL queries that push updates when data changes | Live data feeds, collaborative editing, chat applications |
| Transport Layer | WebSocket, SSE |
Subscriptions use WebSocket or SSE for bidirectional communication | Flexible transport based on requirements |
| Apollo Client | useSubscription() |
React hook for GraphQL subscriptions with automatic reconnection | Declarative real-time data in React components |
| Pub/Sub Pattern | pubsub.publish() |
Server-side publish-subscribe for triggering subscription updates | Multi-client updates, event-driven architecture |
| Filtering | withFilter() |
Server-side filtering to send updates only to relevant subscribers | Optimize bandwidth, user-specific updates |
Example: GraphQL Subscription Implementation
// Schema Definition
type Subscription {
messageAdded(roomId: ID!): Message
userStatusChanged(userId: ID!): UserStatus
notificationReceived: Notification
}
type Message {
id: ID!
text: String!
user: User!
timestamp: String!
}
// Server: GraphQL Resolver
import { PubSub, withFilter } from 'graphql-subscriptions';
const pubsub = new PubSub();
const MESSAGE_ADDED = 'MESSAGE_ADDED';
const resolvers = {
Mutation: {
addMessage: async (_, { roomId, text }, { user }) => {
const message = {
id: generateId(),
text,
user,
roomId,
timestamp: new Date().toISOString()
};
await saveMessage(message);
// Publish to subscribers
pubsub.publish(MESSAGE_ADDED, { messageAdded: message });
return message;
}
},
Subscription: {
messageAdded: {
// Filter: only send to users in the same room
subscribe: withFilter(
() => pubsub.asyncIterator([MESSAGE_ADDED]),
(payload, variables) => {
return payload.messageAdded.roomId === variables.roomId;
}
)
}
}
};
// Client: Apollo Client Setup
import { split, HttpLink, ApolloClient, InMemoryCache } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
const wsLink = new GraphQLWsLink(
createClient({ url: 'ws://localhost:4000/graphql' })
);
// Split based on operation type
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
});
Example: React Component with Subscription
import { useSubscription, gql, useMutation } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
text
user { name }
timestamp
}
}
`;
const ADD_MESSAGE = gql`
mutation AddMessage($roomId: ID!, $text: String!) {
addMessage(roomId: $roomId, text: $text) {
id
text
}
}
`;
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
// Subscription
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION, {
variables: { roomId },
onData: ({ data }) => {
setMessages(prev => [...prev, data.data.messageAdded]);
}
});
// Mutation
const [addMessage] = useMutation(ADD_MESSAGE);
const handleSend = (text) => {
addMessage({ variables: { roomId, text } });
};
return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.user.name}: {msg.text}</div>
))}
<input onSubmit={(e) => handleSend(e.target.value)} />
</div>
);
}
GraphQL Subscription Benefits
- Type Safety: Strongly typed real-time data with schema validation
- Selective Fields: Request only needed fields, reduce bandwidth
- Unified API: Queries, mutations, and subscriptions in one endpoint
- Client Control: Client decides what to subscribe to and when
6. Props Drilling Solutions (Context, Prop Composition)
| Pattern | Solution | Description | When to Use |
|---|---|---|---|
| Prop Drilling Problem | Parent → Child → GrandChild |
Passing props through intermediate components that don't need them | Recognize as code smell when props pass through 3+ levels |
| Component Composition | children, render props |
Pass components as children instead of data, invert dependencies | First solution to try, maintains component isolation |
| Context API | createContext + Provider |
Share data across component tree without explicit props | Theme, auth, locale - infrequently changing data |
| State Management | Redux, Zustand, Jotai |
External state store accessible from any component | Complex state logic, frequent updates, shared state |
| Custom Hooks | useCustomHook() |
Encapsulate data fetching and state logic in reusable hooks | Share logic without sharing state, API abstractions |
Problem: Prop Drilling
// Bad: Props drilled through components
function App() {
const [user, setUser] = useState(null);
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
// Dashboard doesn't use user
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
// Sidebar doesn't use user
return <UserProfile user={user} />;
}
function UserProfile({ user }) {
// Finally uses user
return <div>{user.name}</div>;
}
Solution 1: Composition
// Good: Composition pattern
function App() {
const [user, setUser] = useState(null);
return (
<Dashboard>
<Sidebar>
<UserProfile user={user} />
</Sidebar>
</Dashboard>
);
}
function Dashboard({ children }) {
return <div className="dashboard">{children}</div>;
}
function Sidebar({ children }) {
return <aside>{children}</aside>;
}
function UserProfile({ user }) {
return <div>{user.name}</div>;
}
Example: Context API Solution
// Solution 2: Context API
import { createContext, useContext } from 'react';
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={user}>
<Dashboard />
</UserContext.Provider>
);
}
function Dashboard() {
// No user prop needed
return <Sidebar />;
}
function Sidebar() {
// No user prop needed
return <UserProfile />;
}
function UserProfile() {
const user = useContext(UserContext);
return <div>{user.name}</div>;
}
// Solution 3: Custom Hook
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
function UserProfile() {
const user = useUser(); // Clean API
return <div>{user.name}</div>;
}
Performance Note: Context API causes all consumers to re-render when value changes. For
high-frequency updates, use state management libraries with selector functions
(Zustand, Jotai) or split contexts by update frequency.