Web Animations: Framer Motion and CSS for React Developers
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
Learn to implement real-time features with WebSockets and Server-Sent Events — chat apps, live notifications, collaborative editing, and handling connection state in React.
By codefunc
Real-time data makes apps feel alive — chat messages appear instantly, notifications pop up without refresh, and collaborative documents update as teammates type. WebSockets enable bidirectional communication between browser and server. Server-Sent Events (SSE) provide simpler one-way streaming.
Bottom line: Use WebSockets for bidirectional real-time communication (chat, multiplayer). Use SSE for server-to-client streaming (notifications, feeds). Handle connection lifecycle carefully — reconnect on disconnect, queue messages when offline. Libraries like Socket.IO simplify reconnection and fallbacks, but add overhead. Native WebSocket API works well for simple cases.
| Feature | WebSockets | SSE | Polling |
|---|---|---|---|
| Direction | Bidirectional | Server → Client | Client → Server |
| Connection | Persistent | Persistent | Repeated requests |
| Overhead | Low | Low | High |
| Browser support | All modern | All modern | All |
| Auto-reconnect | Manual | Built-in | N/A |
| Binary data | Yes | No (text only) | Yes |
| Best for | Chat, games | Notifications, feeds | Simple updates |
const socket = new WebSocket('wss://api.example.com/ws');
// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected');
socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
});
// Receive messages
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
});
// Handle errors
socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});
// Connection closed
socket.addEventListener('close', (event) => {
console.log('Disconnected:', event.code, event.reason);
});
// Send JSON
socket.send(JSON.stringify({
type: 'message',
content: 'Hello!',
timestamp: Date.now(),
}));
// Check ready state before sending
if (socket.readyState === WebSocket.OPEN) {
socket.send(data);
}
| State | Value | Meaning |
|---|---|---|
CONNECTING |
0 | Connection in progress |
OPEN |
1 | Connected and ready |
CLOSING |
2 | Closing handshake |
CLOSED |
3 | Connection closed |
import { useEffect, useRef, useState, useCallback } from 'react';
interface UseWebSocketOptions {
onMessage?: (data: any) => void;
onOpen?: () => void;
onClose?: () => void;
onError?: (error: Event) => void;
reconnect?: boolean;
reconnectInterval?: number;
}
function useWebSocket(url: string, options: UseWebSocketOptions = {}) {
const {
onMessage,
onOpen,
onClose,
onError,
reconnect = true,
reconnectInterval = 3000,
} = options;
const socketRef = useRef<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
const connect = useCallback(() => {
const socket = new WebSocket(url);
socketRef.current = socket;
socket.onopen = () => {
setIsConnected(true);
onOpen?.();
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
onMessage?.(data);
};
socket.onclose = () => {
setIsConnected(false);
onClose?.();
if (reconnect) {
reconnectTimeoutRef.current = setTimeout(connect, reconnectInterval);
}
};
socket.onerror = (error) => {
onError?.(error);
};
}, [url, onMessage, onOpen, onClose, onError, reconnect, reconnectInterval]);
useEffect(() => {
connect();
return () => {
clearTimeout(reconnectTimeoutRef.current);
socketRef.current?.close();
};
}, [connect]);
const send = useCallback((data: any) => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(JSON.stringify(data));
}
}, []);
return { isConnected, send };
}
function Chat() {
const [messages, setMessages] = useState<Message[]>([]);
const { isConnected, send } = useWebSocket('wss://api.example.com/chat', {
onMessage: (data) => {
if (data.type === 'message') {
setMessages((prev) => [...prev, data.message]);
}
},
onOpen: () => {
send({ type: 'join', room: 'general' });
},
});
const sendMessage = (content: string) => {
send({ type: 'message', content });
};
return (
<div>
<div className={isConnected ? 'connected' : 'disconnected'}>
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
</div>
<MessageList messages={messages} />
<MessageInput onSend={sendMessage} disabled={!isConnected} />
</div>
);
}
Socket.IO adds features like automatic reconnection, rooms, and fallbacks.
npm install socket.io-client
import { io, Socket } from 'socket.io-client';
const socket = io('https://api.example.com', {
autoConnect: false,
auth: {
token: getAuthToken(),
},
});
// Connect
socket.connect();
// Events
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});
socket.on('message', (data) => {
console.log('Message:', data);
});
// Emit events
socket.emit('join', { room: 'general' });
socket.emit('message', { content: 'Hello!' });
// With acknowledgment
socket.emit('message', { content: 'Hello!' }, (response) => {
console.log('Server acknowledged:', response);
});
import { createContext, useContext, useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
const SocketContext = createContext<Socket | null>(null);
export function SocketProvider({ children }: { children: React.ReactNode }) {
const [socket, setSocket] = useState<Socket | null>(null);
useEffect(() => {
const newSocket = io('https://api.example.com', {
auth: { token: getAuthToken() },
});
setSocket(newSocket);
return () => {
newSocket.close();
};
}, []);
return (
<SocketContext.Provider value={socket}>
{children}
</SocketContext.Provider>
);
}
export function useSocket() {
const socket = useContext(SocketContext);
if (!socket) {
throw new Error('useSocket must be used within SocketProvider');
}
return socket;
}
function Notifications() {
const socket = useSocket();
const [notifications, setNotifications] = useState<Notification[]>([]);
useEffect(() => {
socket.on('notification', (data) => {
setNotifications((prev) => [data, ...prev]);
});
return () => {
socket.off('notification');
};
}, [socket]);
return <NotificationList items={notifications} />;
}
SSE is simpler than WebSockets for one-way server-to-client streaming.
const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Event:', data);
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
};
// Named events
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
});
// Close connection
eventSource.close();
EventSource doesn't support custom headers. Use query params or cookies:
// Query param (less secure)
const eventSource = new EventSource(`/api/events?token=${token}`);
// Or use fetch with ReadableStream (more flexible)
async function streamEvents() {
const response = await fetch('/api/events', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
handleEvent(data);
}
}
}
}
function useSSE<T>(url: string, onMessage: (data: T) => void) {
useEffect(() => {
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
onMessage(data);
};
eventSource.onerror = () => {
// EventSource auto-reconnects
console.log('SSE error, reconnecting...');
};
return () => {
eventSource.close();
};
}, [url, onMessage]);
}
// Usage
function LiveFeed() {
const [items, setItems] = useState<FeedItem[]>([]);
useSSE<FeedItem>('/api/feed', (item) => {
setItems((prev) => [item, ...prev].slice(0, 50));
});
return <FeedList items={items} />;
}
class ReconnectingWebSocket {
private url: string;
private socket: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private baseDelay = 1000;
constructor(url: string) {
this.url = url;
this.connect();
}
private connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.reconnectAttempts = 0;
};
this.socket.onclose = () => {
this.scheduleReconnect();
};
}
private scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return;
}
const delay = Math.min(
this.baseDelay * Math.pow(2, this.reconnectAttempts),
30000 // Max 30 seconds
);
this.reconnectAttempts++;
setTimeout(() => this.connect(), delay);
}
}
Queue messages while disconnected:
class QueuedWebSocket {
private socket: WebSocket | null = null;
private messageQueue: string[] = [];
send(data: any) {
const message = JSON.stringify(data);
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(message);
} else {
this.messageQueue.push(message);
}
}
private onConnect() {
// Flush queue
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift()!;
this.socket?.send(message);
}
}
}
function usePresence(roomId: string) {
const [users, setUsers] = useState<User[]>([]);
const socket = useSocket();
useEffect(() => {
socket.emit('join', { roomId });
socket.on('presence', (data) => {
setUsers(data.users);
});
socket.on('user:join', (user) => {
setUsers((prev) => [...prev, user]);
});
socket.on('user:leave', (userId) => {
setUsers((prev) => prev.filter((u) => u.id !== userId));
});
return () => {
socket.emit('leave', { roomId });
socket.off('presence');
socket.off('user:join');
socket.off('user:leave');
};
}, [socket, roomId]);
return users;
}
function useTypingIndicator(roomId: string) {
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const socket = useSocket();
const typingTimeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
socket.on('typing:start', ({ userId }) => {
setTypingUsers((prev) =>
prev.includes(userId) ? prev : [...prev, userId]
);
});
socket.on('typing:stop', ({ userId }) => {
setTypingUsers((prev) => prev.filter((id) => id !== userId));
});
return () => {
socket.off('typing:start');
socket.off('typing:stop');
};
}, [socket]);
const startTyping = useCallback(() => {
socket.emit('typing:start', { roomId });
clearTimeout(typingTimeoutRef.current);
typingTimeoutRef.current = setTimeout(() => {
socket.emit('typing:stop', { roomId });
}, 3000);
}, [socket, roomId]);
return { typingUsers, startTyping };
}
function useOptimisticMessages() {
const [messages, setMessages] = useState<Message[]>([]);
const pendingRef = useRef<Map<string, Message>>(new Map());
const socket = useSocket();
const sendMessage = (content: string) => {
const tempId = crypto.randomUUID();
const optimistic: Message = {
id: tempId,
content,
status: 'pending',
createdAt: new Date().toISOString(),
};
// Add optimistically
setMessages((prev) => [...prev, optimistic]);
pendingRef.current.set(tempId, optimistic);
// Send to server
socket.emit('message', { tempId, content });
};
useEffect(() => {
// Server confirms message
socket.on('message:confirmed', ({ tempId, message }) => {
pendingRef.current.delete(tempId);
setMessages((prev) =>
prev.map((m) => (m.id === tempId ? message : m))
);
});
// Server rejects message
socket.on('message:failed', ({ tempId, error }) => {
pendingRef.current.delete(tempId);
setMessages((prev) =>
prev.map((m) =>
m.id === tempId ? { ...m, status: 'failed', error } : m
)
);
});
// New message from others
socket.on('message:new', (message) => {
setMessages((prev) => [...prev, message]);
});
}, [socket]);
return { messages, sendMessage };
}
Use the JSON Formatter to inspect WebSocket message payloads. Validate message schemas with JSON Validator. Generate unique message IDs with UUID Generator.
Network tab → WS filter shows WebSocket connections:
Start with native WebSocket API for simple use cases. Use Socket.IO when you need rooms, reconnection, and fallbacks out of the box. Use SSE for one-way server-to-client streaming. Handle disconnection gracefully — show connection state to users, queue messages, implement reconnection with backoff. Keep messages small and schema-consistent. Test with network throttling and disconnection scenarios.
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
A practical guide to authentication for frontend developers — understanding OAuth flows, JWT structure, session cookies, and implementing secure auth in React applications.
A practical guide to using GraphQL in frontend applications — writing queries and mutations, client-side caching with Apollo and urql, and patterns for React integration.
Find a developer tool