Sumit Gohil
Sumit Gohil

Reputation: 1

How to resolve circular dependency between two contexts in ReactJS?

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

Answers (0)

Related Questions