diff --git a/apps/client/src/hooks/useAppFlow.ts b/apps/client/src/hooks/useAppFlow.ts index bb6bafc..b855763 100644 --- a/apps/client/src/hooks/useAppFlow.ts +++ b/apps/client/src/hooks/useAppFlow.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useReducer, useRef, useState } from "react"; import { socketManager } from "@client/network/SocketManager"; import { appConsts, config } from "@repo/shared"; import type { appTypes, roomTypes } from "@repo/shared"; @@ -12,81 +12,112 @@ requestJoin: (payload: roomTypes.JoinRoomPayload) => void; }; +type JoinState = { + isJoining: boolean; + joinErrorMessage: string | null; +}; + +type JoinAction = + | { type: "start" } + | { type: "complete"; errorMessage: string | null }; + +const initialJoinState: JoinState = { + isJoining: false, + joinErrorMessage: null, +}; + +const joinReducer = (state: JoinState, action: JoinAction): JoinState => { + if (action.type === "start") { + return { + isJoining: true, + joinErrorMessage: null, + }; + } + + if (action.type === "complete") { + return { + isJoining: false, + joinErrorMessage: action.errorMessage, + }; + } + + return state; +}; + export const useAppFlow = (): AppFlowState => { const [scenePhase, setScenePhase] = useState(appConsts.ScenePhase.TITLE); const [room, setRoom] = useState(null); const [myId, setMyId] = useState(null); - const [joinErrorMessage, setJoinErrorMessage] = useState(null); - const [isJoining, setIsJoining] = useState(false); + const [joinState, dispatchJoin] = useReducer(joinReducer, initialJoinState); const joinTimeoutRef = useRef | null>(null); const joinRejectedHandlerRef = useRef<((payload: roomTypes.JoinRoomRejectedPayload) => void) | null>(null); - const clearJoinRejectedHandler = () => { + const clearJoinRejectedHandler = useCallback(() => { if (!joinRejectedHandlerRef.current) { return; } socketManager.title.offJoinRejected(joinRejectedHandlerRef.current); joinRejectedHandlerRef.current = null; - }; + }, []); - const clearJoinTimeout = () => { + const clearJoinTimeout = useCallback(() => { if (!joinTimeoutRef.current) { return; } clearTimeout(joinTimeoutRef.current); joinTimeoutRef.current = null; - }; + }, []); - const requestJoin = (payload: roomTypes.JoinRoomPayload) => { - if (isJoining) { + const completeJoinRequest = useCallback((errorMessage: string | null = null) => { + clearJoinTimeout(); + clearJoinRejectedHandler(); + dispatchJoin({ type: "complete", errorMessage }); + }, [clearJoinRejectedHandler, clearJoinTimeout]); + + const getJoinRejectedMessage = useCallback((payload: roomTypes.JoinRoomRejectedPayload): string | null => { + if (payload.reason === "full") { + return `ルーム ${payload.roomId} は満員です`; + } + + if (payload.reason === "duplicate") { + return `ルーム ${payload.roomId} への参加要求が重複しました`; + } + + return null; + }, []); + + const requestJoin = useCallback((payload: roomTypes.JoinRoomPayload) => { + if (joinState.isJoining) { return; } - clearJoinTimeout(); - clearJoinRejectedHandler(); - setJoinErrorMessage(null); - setIsJoining(true); + completeJoinRequest(); + dispatchJoin({ type: "start" }); const handleJoinRejected = (payload: roomTypes.JoinRoomRejectedPayload) => { - clearJoinTimeout(); - setIsJoining(false); - joinRejectedHandlerRef.current = null; - - if (payload.reason === "full") { - setJoinErrorMessage(`ルーム ${payload.roomId} は満員です`); - return; - } - - if (payload.reason === "duplicate") { - setJoinErrorMessage(`ルーム ${payload.roomId} への参加要求が重複しました`); - } + const joinRejectedMessage = getJoinRejectedMessage(payload); + completeJoinRequest(joinRejectedMessage); }; joinRejectedHandlerRef.current = handleJoinRejected; socketManager.title.onceJoinRejected(handleJoinRejected); joinTimeoutRef.current = setTimeout(() => { - clearJoinRejectedHandler(); - setIsJoining(false); - setJoinErrorMessage("参加要求がタイムアウトしました,もう一度お試しください"); - joinTimeoutRef.current = null; + completeJoinRequest("参加要求がタイムアウトしました,もう一度お試しください"); }, config.GAME_CONFIG.JOIN_REQUEST_TIMEOUT_MS); socketManager.title.joinRoom(payload); - }; + }, [completeJoinRequest, getJoinRejectedMessage, joinState.isJoining]); useEffect(() => { const handleConnect = (id: string) => { setMyId(id); }; const handleRoomUpdate = (updatedRoom: roomTypes.Room) => { - clearJoinTimeout(); - clearJoinRejectedHandler(); + completeJoinRequest(); setRoom(updatedRoom); - setIsJoining(false); - setJoinErrorMessage(null); setScenePhase(appConsts.ScenePhase.LOBBY); }; const handleGameStart = () => { @@ -98,12 +129,18 @@ socketManager.game.onceGameStart(handleGameStart); return () => { - clearJoinTimeout(); - clearJoinRejectedHandler(); + completeJoinRequest(); socketManager.common.offConnect(handleConnect); socketManager.lobby.offRoomUpdate(handleRoomUpdate); }; - }, []); + }, [completeJoinRequest]); - return { scenePhase, room, myId, joinErrorMessage, isJoining, requestJoin }; + return { + scenePhase, + room, + myId, + joinErrorMessage: joinState.joinErrorMessage, + isJoining: joinState.isJoining, + requestJoin, + }; };