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";
function ChatMessages() {
const { messages } = useChatState();
const { state } = useChatState();
return (
<>
{messages.map((message) => (
{state.messages.map((message) => (
<div key={message.id}>
<div>
<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 { ChatContext } from "../contexts/ChatContext";
import { chatReducer } from "../reducers/chatReducers";
export function ChatProvider({ children }: { children: React.ReactNode }) {
const [messages, setMessages] = React.useState<ChatMessage[]>([]);
const [currentUser, setCurrentUser] = React.useState<User | null>(null);
const [state, dispatch] = useReducer(chatReducer, {
messages: [],
currentUser: null,
isConnected: false,
});
const addMessage = (message: ChatMessage) => {
setMessages((prev) => [...prev, message]);
const value = {
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 (
<ChatContext.Provider
value={{ messages, currentUser, addMessage, setCurrentUser }}
>
{children}
</ChatContext.Provider>
);
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
}

View file

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

View file

@ -1,17 +1,36 @@
import { createContext, useContext } from "react";
import { createContext, useContext, Dispatch } from "react";
import { ChatMessage, User } from "../../../shared";
interface ChatContextType {
export type ChatState = {
messages: ChatMessage[];
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;
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 useChatState = () => {
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;
};

View file

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

View file

@ -9,15 +9,15 @@ import { useChatState } from "../contexts/ChatContext";
function Room() {
const { roomId } = useParams();
const { isConnected, connect, sendMessage } = useWebSocket(roomId ?? "");
const { currentUser } = useChatState();
const { state } = useChatState();
return (
<Layout>
<section className="w-full">
<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>
{currentUser && (
<span className="font-medium">User: {currentUser?.name}</span>
{state.currentUser && (
<span className="font-medium">User: {state.currentUser?.name}</span>
)}
<span
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;
}
}