chore: migrate to reducer pattern instead of useState

This commit is contained in:
Alexander Daichendt 2025-02-12 11:33:48 +01:00
parent 18dd8b83f0
commit 7150ca87c3
7 changed files with 85 additions and 29 deletions

View file

@ -2,11 +2,11 @@ import React from "react";
import { useChatState } from "../contexts/ChatContext"; import { useChatState } from "../contexts/ChatContext";
function ChatMessages() { function ChatMessages() {
const { messages } = useChatState(); const { state } = useChatState();
return ( return (
<> <>
{messages.map((message) => ( {state.messages.map((message) => (
<div key={message.id}> <div key={message.id}>
<div> <div>
<span>{message.author.name}</span> <span>{message.author.name}</span>

View file

@ -1,20 +1,26 @@
import React from "react"; import React, { useReducer } from "react";
import { ChatMessage, User } from "../../../shared"; import { ChatMessage, User } from "../../../shared";
import { ChatContext } from "../contexts/ChatContext"; import { ChatContext } from "../contexts/ChatContext";
import { chatReducer } from "../reducers/chatReducers";
export function ChatProvider({ children }: { children: React.ReactNode }) { export function ChatProvider({ children }: { children: React.ReactNode }) {
const [messages, setMessages] = React.useState<ChatMessage[]>([]); const [state, dispatch] = useReducer(chatReducer, {
const [currentUser, setCurrentUser] = React.useState<User | null>(null); messages: [],
currentUser: null,
isConnected: false,
});
const addMessage = (message: ChatMessage) => { const value = {
setMessages((prev) => [...prev, message]); state,
dispatch,
actions: {
addMessage: (message: ChatMessage) =>
dispatch({ type: "ADD_MESSAGE", payload: message }),
setUser: (user: User) => dispatch({ type: "SET_USER", payload: user }),
setConnected: (isConnected: boolean) =>
dispatch({ type: "SET_CONNECTED", payload: isConnected }),
},
}; };
return ( return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
<ChatContext.Provider
value={{ messages, currentUser, addMessage, setCurrentUser }}
>
{children}
</ChatContext.Provider>
);
} }

View file

@ -6,7 +6,7 @@ interface ConnectProps {
} }
function Connect({ onConnect }: ConnectProps) { function Connect({ onConnect }: ConnectProps) {
const { setCurrentUser } = useChatState(); const { actions } = useChatState();
const [name, setName] = useState(""); const [name, setName] = useState("");
function handleConnect() { function handleConnect() {
@ -14,8 +14,11 @@ function Connect({ onConnect }: ConnectProps) {
return; return;
} }
// leave userId empty for now, it is server generated actions.setUser({
setCurrentUser({ name, userId: "" }); name: name,
userId: "", // leave userId empty for now, it is server generated
});
onConnect(name); onConnect(name);
} }

View file

@ -1,17 +1,36 @@
import { createContext, useContext } from "react"; import { createContext, useContext, Dispatch } from "react";
import { ChatMessage, User } from "../../../shared"; import { ChatMessage, User } from "../../../shared";
interface ChatContextType { export type ChatState = {
messages: ChatMessage[]; messages: ChatMessage[];
currentUser: User | null; currentUser: User | null;
isConnected: boolean;
};
export type ChatAction =
| { type: "ADD_MESSAGE"; payload: ChatMessage }
| { type: "SET_USER"; payload: User }
| { type: "SET_CONNECTED"; payload: boolean }
| { type: "CLEAR_MESSAGES" };
export type ChatActions = {
addMessage: (message: ChatMessage) => void; addMessage: (message: ChatMessage) => void;
setCurrentUser: (user: User) => void; setUser: (user: User) => void;
setConnected: (isConnected: boolean) => void;
};
interface ChatContextType {
state: ChatState;
dispatch: Dispatch<ChatAction>;
actions: ChatActions;
} }
export const ChatContext = createContext<ChatContextType | null>(null); export const ChatContext = createContext<ChatContextType | null>(null);
export const useChatState = () => { export const useChatState = () => {
const context = useContext(ChatContext); const context = useContext(ChatContext);
if (!context) throw new Error("useChat must be used within ChatProvider"); if (!context) {
throw new Error("useChatState must be used within ChatProvider");
}
return context; return context;
}; };

View file

@ -9,7 +9,7 @@ import { useChatState } from "../contexts/ChatContext";
export function useWebSocket(roomId: string) { export function useWebSocket(roomId: string) {
const [ws, setWs] = useState<WebSocket | null>(null); const [ws, setWs] = useState<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const { addMessage, currentUser, setCurrentUser } = useChatState(); const { state, actions } = useChatState();
// Handle incoming messages // Handle incoming messages
const handleMessage = useCallback( const handleMessage = useCallback(
@ -19,18 +19,18 @@ export function useWebSocket(roomId: string) {
switch (message.type) { switch (message.type) {
case "USER_JOINED": case "USER_JOINED":
console.log("User joined", message.payload); console.log("User joined", message.payload);
if (message.payload.user.name === currentUser?.name) { if (message.payload.user.name === state.currentUser?.name) {
setCurrentUser(message.payload.user); actions.setUser(message.payload.user);
} }
break; break;
case "CHAT_MESSAGE": case "CHAT_MESSAGE":
addMessage(message.payload); actions.addMessage(message.payload);
break; break;
default: default:
console.error("Unknown message type", message); console.error("Unknown message type", message);
} }
}, },
[currentUser, setCurrentUser, addMessage], [state.currentUser, actions],
); );
// Send a message to the server // Send a message to the server

View file

@ -9,15 +9,15 @@ import { useChatState } from "../contexts/ChatContext";
function Room() { function Room() {
const { roomId } = useParams(); const { roomId } = useParams();
const { isConnected, connect, sendMessage } = useWebSocket(roomId ?? ""); const { isConnected, connect, sendMessage } = useWebSocket(roomId ?? "");
const { currentUser } = useChatState(); const { state } = useChatState();
return ( return (
<Layout> <Layout>
<section className="w-full"> <section className="w-full">
<h2 className="text-xl bg-gray-100 p-4 flex justify-between items-center rounded-md shadow-sm"> <h2 className="text-xl bg-gray-100 p-4 flex justify-between items-center rounded-md shadow-sm">
<span className="font-medium">Room ID: {roomId}</span> <span className="font-medium">Room ID: {roomId}</span>
{currentUser && ( {state.currentUser && (
<span className="font-medium">User: {currentUser?.name}</span> <span className="font-medium">User: {state.currentUser?.name}</span>
)} )}
<span <span
className={`px-3 py-1 rounded-full text-sm font-medium ${ className={`px-3 py-1 rounded-full text-sm font-medium ${

View file

@ -0,0 +1,28 @@
import { ChatAction, ChatState } from "../contexts/ChatContext";
export function chatReducer(state: ChatState, action: ChatAction): ChatState {
switch (action.type) {
case "ADD_MESSAGE":
return {
...state,
messages: [...state.messages, action.payload],
};
case "SET_USER":
return {
...state,
currentUser: action.payload,
};
case "SET_CONNECTED":
return {
...state,
isConnected: action.payload,
};
case "CLEAR_MESSAGES":
return {
...state,
messages: [],
};
default:
return state;
}
}