Reputation: 1
I am developing a real-time code editor and I am facing circular dependency between two contexts FileContext
and SocketContext
as FileContext
using socket client methods to synchronize and handle the real time events like createfile
and etc. while SocketContext
is using some methods of fileContext
to synchronize files when a user enters in a room.
How can I resolve this circular dependencies?
The issue I am facing is when my App.jsx component renders at first in SocketContext
it is using methods from FileContext
and vice versa. Since in App.jsx SocketProvider
is before the FileSystemProvider
it is giving error like methods from FileContext
is not defined in SocketContext
.
FileContext.jsx
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState
} from "react";
import { useSocket } from "./SocketContext";
import { SocketEvent } from "../types/socket";
const FileSystemContext = createContext();
export const FileSystemProvider = ({ children }) => {
const [files, setFiles] = useState([{
id: Date.now.toString(),
name: "index.cpp",
content: "#include <iostream>\nint main() { std::cout << \"Hello, World!\"; return 0; }"
}]);
const [openFiles, setOpenFiles] = useState([{
id: Date.now.toString(),
name: "index.cpp",
content: "#include <iostream>\nint main() { std::cout << \"Hello, World!\"; return 0; }"
}]);
const [activeFile, setActiveFile] = useState({
id: Date.now.toString(),
name: "index.cpp",
content: "#include <iostream>\nint main() { std::cout << \"Hello, World!\"; return 0; }"
});
const { socket } = useSocket();
const handleFileCreated = (newFile) => {
setFiles((prevFiles) => [...prevFiles, newFile]);
setOpenFiles((prevOpenFiles) => [...prevOpenFiles, newFile]);
setActiveFile(newFile); // Immediately set the new file as active
};
// Create new file locally & emit to socket
const createFile = (fileName) => {
if (!fileName.trim()) return;
const newFile = {
id: Date.now().toString(),
name: fileName,
content: "to start coding",
};
setFiles((prevFiles) => [...prevFiles, newFile]);
setOpenFiles((prevOpenFiles) => [...prevOpenFiles, newFile]);
setActiveFile(newFile); // Immediately set the new file as active
if (socket) {
socket.emit(SocketEvent.FILE_CREATED, newFile);
}
};
// Update file content locally
const updateFileContent = useCallback(
(fileId, newContent) => {
setFiles((prevFiles) =>
prevFiles.map((file) =>
file.id === fileId ? { ...file, content: newContent } : file
)
);
setOpenFiles((prevOpenFiles) =>
prevOpenFiles.map((file) =>
file.id === fileId ? { ...file, content: newContent } : file
)
);
// Only update active file content if it's the same file
setActiveFile((prevActive) =>
prevActive?.id === fileId ? { ...prevActive, content: newContent } : prevActive
);
},
[]
);
// Handle file update from socket (Fix applied)
const handleFileUpdate = useCallback(
(data) => {
const { fileId, newContent } = data;
// Update the content for all users, but don't change their active file
updateFileContent(fileId, newContent);
// Only update activeFile **if it's the same file currently active**
setActiveFile((prevActive) =>
prevActive?.id === fileId ? { ...prevActive, content: newContent } : prevActive
);
},
[updateFileContent]
);
// Listen for file update events from socket
const renameFile = useCallback(
(fileId, newName, sendToSocket = true) => {
// Update files array
console.log(newName)
setFiles((prevFiles) =>
prevFiles.map((file) =>
file.id === fileId ? { ...file, name: newName } : file
)
);
// Update open files
setOpenFiles((prevOpenFiles) =>
prevOpenFiles.map((file) =>
file.id === fileId ? { ...file, name: newName } : file
)
);
// Update active file if it's the renamed file
setActiveFile((prevActive) =>
prevActive?.id === fileId ? { ...prevActive, name: newName } : prevActive
);
if (!sendToSocket) return true;
socket.emit(SocketEvent.FILE_RENAMED, {
fileId,
newName,
});
return true;
},
[socket]
);
const deleteFile = useCallback(
(fileId, sendToSocket = true) => {
// Remove the file from files array
setFiles(prevFiles =>
prevFiles.filter(file => file.id !== fileId)
);
// Remove the file from openFiles
if (openFiles.some(file => file.id === fileId)) {
setOpenFiles(prevOpenFiles =>
prevOpenFiles.filter(file => file.id !== fileId)
);
}
// Set the active file to first file from open files if it's the file being deleted
if (activeFile?.id === fileId) {
setActiveFile(openFiles[0]);
}
if (!sendToSocket) return;
socket.emit(SocketEvent.FILE_DELETED, { fileId });
},
[activeFile?.id, openFiles, socket]
);
const handleFileRenamed = useCallback(
(data) => {
const { fileId, newName } = data;
renameFile(fileId, newName, false);
},
[renameFile]
);
const handleFileDeleted = useCallback(
(data) => {
const { fileId } = data;
deleteFile(fileId, false);
},
[deleteFile]
);
useEffect(() => {
if (!socket) return;
socket.on(SocketEvent.FILE_CREATED,handleFileCreated);
socket.on(SocketEvent.FILE_UPDATED, handleFileUpdate);
socket.on(SocketEvent.FILE_RENAMED,handleFileRenamed);
socket.on(SocketEvent.FILE_DELETED,handleFileDeleted);
return () => {
socket.off(SocketEvent.FILE_CREATED , handleFileCreated);
socket.off(SocketEvent.FILE_UPDATED, handleFileUpdate);
socket.off(SocketEvent.FILE_RENAMED , handleFileRenamed);
socket.off(SocketEvent.FILE_DELETED , handleFileDeleted);
};
}, [socket, handleFileUpdate]);
return (
<FileSystemContext.Provider
value={{
files,
createFile,
openFiles,
activeFile,
setActiveFile,
setOpenFiles,
setFiles,
updateFileContent,
renameFile,
deleteFile,
}}
>
{children}
</FileSystemContext.Provider>
);
};
export const useFileSystem = () => useContext(FileSystemContext);
SocketContext.jsx
import {
SocketEvent,
SocketContext as SocketContextType
} from "../types/socket";
import { createRemoteUser , createUser , USER_STATUS } from "../types/user";
import {
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
} from "react"
import { toast } from "react-hot-toast"
import { io } from "socket.io-client"
import { useAppContext } from "./AppContext"
import { useFileSystem } from "./FileContext";
const SocketContext = createContext(SocketContextType)
export const useSocket = () => {
const context = useContext(SocketContext)
if (!context) {
throw new console.error(
"useSocketContext must be used within a Socketprovider"
);
}
return context;
}
const BACKEND_URL = "http://localhost:5000/";
const SocketProvider = ({children}) =>{
const {
users,
setUsers,
setStatus,
setCurrentUser,
// drawingData,
// setDrawingData,
} = useAppContext()
const {
files,
setFiles,
activeFile,
setActiveFile,
openFiles,
setOpenFiles
} = useFileSystem();
const socket = useMemo(() =>
io(BACKEND_URL, { reconnectionAttempts: 1 }),
[]
)
const handleError = useCallback((err) => {
console.log("socket error", err)
setStatus(USER_STATUS.CONNECTION_FAILED)
toast.dismiss()
toast.error("Failed to connect to the server")
}, [setStatus])
const handleUsernameExist = useCallback(() => {
toast.dismiss()
setStatus(USER_STATUS.INITIAL)
toast.error(
"The username you chose already exists in the room. Please choose a different username.",
)
}, [setStatus])
const handleJoiningAccept = useCallback(({ user, users }) => {
setCurrentUser(user)
setUsers(users)
// toast.dismiss()
setStatus(USER_STATUS.JOINED)
setFiles(files);
setOpenFiles(openFiles);
setActiveFile(activeFile);
// if (users.length > 1) {
// toast.loading("Syncing data, please wait...")
// }
}, [setCurrentUser, setStatus, setUsers])
const handleUserLeft = useCallback(({ user }) => {
toast.success(`${user.username} left the room`)
setUsers(users.filter((u) => u.username !== user.username))
}, [setUsers, users])
const handleUserJoined = useCallback(({ user, users }) => {
setUsers(users);
toast.success(`${user.username} joined the room`)
}, [setUsers])
useEffect(() => {
socket.on(SocketEvent.CONNECTTION_ERROR, handleError)
socket.on(SocketEvent.CONNECTTION_FAILED, handleError)
socket.on(SocketEvent.USERNAME_EXISTS, handleUsernameExist)
socket.on(SocketEvent.JOIN_ACCEPTED, handleJoiningAccept)
socket.on(SocketEvent.USER_DISCONNECTED, handleUserLeft)
socket.on(SocketEvent.USER_JOINED, handleUserJoined)
// socket.on(SocketEvent.REQUEST_DRAWING, handleRequestDrawing)
// socket.on(SocketEvent.SYNC_DRAWING, handleDrawingSync)
return () => {
socket.off(SocketEvent.CONNECTTION_ERROR)
socket.off(SocketEvent.CONNECTTION_FAILED)
socket.off(SocketEvent.USERNAME_EXISTS)
socket.off(SocketEvent.JOIN_ACCEPTED)
socket.off(SocketEvent.USER_JOINED)
socket.off(SocketEvent.USER_DISCONNECTED)
// socket.off(SocketEvent.REQUEST_DRAWING)
// socket.off(SocketEvent.SYNC_DRAWING)
}
}, [
// handleDrawingSync,
handleError,
handleJoiningAccept,
// handleRequestDrawing,
handleUserLeft,
handleUsernameExist,
handleUserJoined,
setUsers,
socket,
])
return (
<SocketContext.Provider value={{ socket}}>
{children}
</SocketContext.Provider>
)
}
export { SocketProvider }
export default SocketContext
App.jsx
import Home from "./components/pages/Home";
import EditorPage from "./components/pages/EditorPage";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import Toast from "./components/toast/Toast";
import { AppProvider } from "./context/AppContext";
import { SocketProvider } from "./context/SocketContext";
import { ViewProvider } from "./context/ViewContext";
import { ChatContextProvider } from "./context/ChatContext";
import { FileSystemProvider } from "./context/FileContext";
import { ExecuteCodeContextProvider } from "./context/ExecuteCodeContext";
function App() {
return (
<Router>
<AppProvider>
<SocketProvider> {/* Initialize socket first */}
<FileSystemProvider> {/* Now safe to use */}
<ViewProvider>
<ExecuteCodeContextProvider>
<ChatContextProvider>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/editor/:roomId" element={<EditorPage />} />
</Routes>
</ChatContextProvider>
</ExecuteCodeContextProvider>
</ViewProvider>
</FileSystemProvider>
</SocketProvider>
</AppProvider>
<Toast />
</Router>
);
}
export default App;
I have tried to resolve this issue by using AI but it is still not getting the correct solution.
Upvotes: -1
Views: 19