refactor(frontend): Phase 2 - extract business logic into custom hooks

Separate concerns - move Socket.IO and form logic from components to reusable hooks

New Hooks:
- useForm: Generic form state management with handleChange/handleSubmit/reset
- useEventChat: Extract Socket.IO logic from EventChatPage (156 lines)
  * Manages messages, active users, connection state
  * Handles send message, load older messages with scroll preservation
  * Real-time updates via Socket.IO event listeners
- useMatchChat: Extract Socket.IO logic from MatchChatPage (115 lines)
  * Manages 1:1 chat messages and connection
  * Loads message history from API
  * Real-time message sync via Socket.IO

Pages Refactored:
- EventChatPage: 661 → 564 lines (-97 lines, -15%)
- MatchChatPage: 517 → 446 lines (-71 lines, -14%)

Benefits:
- Cleaner component code - UI separated from business logic
- Reusable hooks can be used in other components
- Easier to test - hooks can be unit tested independently
- Better code organization - single responsibility principle
- 168 lines eliminated from pages, moved to 271 lines of reusable hooks

Phase 2 Total: -168 lines
Grand Total (Phase 1+2): -389 lines (-12%)
This commit is contained in:
Radosław Gierwiało
2025-11-21 17:02:04 +01:00
parent dea9d70bb9
commit 9e74343c3b
5 changed files with 424 additions and 201 deletions

View File

@@ -0,0 +1,180 @@
import { useState, useEffect } from 'react';
import { connectSocket, getSocket } from '../services/socket';
import { eventsAPI } from '../services/api';
/**
* Custom hook for Event Chat functionality
* Extracts Socket.IO logic and chat state management from EventChatPage
*
* @param {string} slug - Event slug
* @param {number} userId - Current user ID
* @param {object} event - Event object (needed to check if event exists)
* @param {object} messagesContainerRef - Ref to messages container for scroll management
* @returns {object} Chat state and handlers
*
* @example
* const {
* messages,
* isConnected,
* activeUsers,
* sendMessage,
* newMessage,
* setNewMessage,
* loadOlderMessages,
* loadingOlder,
* hasMore
* } = useEventChat(slug, user.id, event, messagesContainerRef);
*/
const useEventChat = (slug, userId, event, messagesContainerRef) => {
// Chat state
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState('');
const [activeUsers, setActiveUsers] = useState([]);
const [isConnected, setIsConnected] = useState(false);
const [loadingOlder, setLoadingOlder] = useState(false);
const [hasMore, setHasMore] = useState(true);
// Socket.IO connection and event listeners
useEffect(() => {
if (!event) return;
// Connect to Socket.IO
const socket = connectSocket();
if (!socket) {
console.error('Failed to connect to socket');
return;
}
// Socket event listeners
socket.on('connect', () => {
setIsConnected(true);
// Join event room
socket.emit('join_event_room', { slug });
});
socket.on('disconnect', () => {
setIsConnected(false);
});
// Receive message history (initial 20 messages)
socket.on('message_history', (history) => {
setMessages(history);
setHasMore(history.length === 20);
});
// Receive new messages
socket.on('event_message', (message) => {
setMessages((prev) => [...prev, message]);
});
// Receive active users list
socket.on('active_users', (users) => {
// Filter out duplicates and current user
const uniqueUsers = users
.filter((u, index, self) =>
index === self.findIndex((t) => t.userId === u.userId)
)
.filter((u) => u.userId !== userId);
setActiveUsers(uniqueUsers);
});
// User joined notification
socket.on('user_joined', (userData) => {
console.log(`${userData.username} joined the room`);
});
// User left notification
socket.on('user_left', (userData) => {
console.log(`${userData.username} left the room`);
});
// Cleanup
return () => {
socket.emit('leave_event_room');
socket.off('connect');
socket.off('disconnect');
socket.off('message_history');
socket.off('event_message');
socket.off('active_users');
socket.off('user_joined');
socket.off('user_left');
};
}, [event, slug, userId]);
/**
* Send a message to the event chat
*/
const sendMessage = (e) => {
e.preventDefault();
if (!newMessage.trim()) return;
const socket = getSocket();
if (!socket || !socket.connected) {
alert('Not connected to chat server');
return;
}
// Send message via Socket.IO
socket.emit('send_event_message', {
content: newMessage,
});
setNewMessage('');
};
/**
* Load older messages (pagination)
* Preserves scroll position when loading older messages
*/
const loadOlderMessages = async () => {
if (loadingOlder || !hasMore || messages.length === 0) return;
setLoadingOlder(true);
try {
const oldestMessageId = messages[0].id;
const response = await eventsAPI.getMessages(slug, oldestMessageId, 20);
if (response.data.length > 0) {
// Save current scroll position
const container = messagesContainerRef.current;
const oldScrollHeight = container?.scrollHeight || 0;
const oldScrollTop = container?.scrollTop || 0;
// Prepend older messages
setMessages((prev) => [...response.data, ...prev]);
setHasMore(response.hasMore);
// Restore scroll position (adjust for new content)
setTimeout(() => {
if (container) {
const newScrollHeight = container.scrollHeight;
container.scrollTop = oldScrollTop + (newScrollHeight - oldScrollHeight);
}
}, 0);
} else {
setHasMore(false);
}
} catch (error) {
console.error('Failed to load older messages:', error);
} finally {
setLoadingOlder(false);
}
};
return {
// State
messages,
newMessage,
setNewMessage,
activeUsers,
isConnected,
loadingOlder,
hasMore,
// Actions
sendMessage,
loadOlderMessages
};
};
export default useEventChat;