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:
180
frontend/src/hooks/useEventChat.js
Normal file
180
frontend/src/hooks/useEventChat.js
Normal 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;
|
||||
Reference in New Issue
Block a user