Issues with category synchronization between the client and API in Vue.js

I’m working on a Vue.js app that syncs categories with a server via an API. I’m having issues with category display after setting parent and child categories.

Situations encountered:

  • No category assignments: Works fine on both client and API.
  • Parent category assigned: Correct nesting on the client, API data accurate.
  • Parent + child categories assigned: Displays correctly on both client and API.
  • After “Cancel Child”: Before reload, categories are correct, but after reload, they desync, and the structure differs from the API.

The fetchCategories function retrieves categories from the server, but syncing fails after updating the structure.

How can I ensure proper synchronization between the client and API to prevent discrepancies in the category hierarchy?

async fetchCategories() {
  try {
    const response = await fetch(`${this.apiUrl}/categories`);
    this.logResponse(response, 'fetchCategories');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    this.categories = await response.json();
    this.checkCyclicDependencies();  // Check for cyclic dependencies after fetching
    console.log("Fetched categories:", this.categories);
  } catch (error) {
    console.error("Error fetching categories:", error);
    this.showMessage(`Failed to load categories: ${error.message}`, 'error');
  }
}
// Method for checking cyclic dependencies
checkCyclicDependencies() {
  const visited = new Set();
  const recStack = new Set();

  const hasCycle = (id) => {
    if (!visited.has(id)) {
      visited.add(id);
      recStack.add(id);

      const category = this.categories.find(cat => cat.ID === id);
      if (category) {
        if (recStack.has(category.ParentID)) {
          return { hasCycle: true, cycleStart: category.ParentID };
        }

        if (category.ParentID !== null) {
          const result = hasCycle(category.ParentID);
          if (result.hasCycle) {
            return result;
          }
        }
      }
    }
    recStack.delete(id);
    return { hasCycle: false };
  };

  for (const cat of this.categories) {
    const result = hasCycle(cat.ID);
    if (result.hasCycle) {
      const cyclePath = this.getCyclePath(result.cycleStart);
      this.showMessage(`Cyclic dependency detected: ${cyclePath.join(" -> ")}`, 'warning');
      this.breakCycle(result.cycleStart);
      break;
    }
  }
}

// Method to get the cycle path for displaying cyclic dependency
getCyclePath(startId) {
  const path = [];
  let currentId = startId;
  do {
    path.push(this.categories.find(cat => cat.ID === currentId).Name);
    currentId = this.categories.find(cat => cat.ID === currentId).ParentID;
  } while (currentId !== startId);
  path.push(path[0]);  // Return to the start of the cycle
  return path;
}

// Method to break the cycle in case of cyclic dependency
breakCycle(startId) {
  let currentId = startId;
  let nextId;
  do {
    nextId = this.categories.find(cat => cat.ID === currentId).ParentID;
    this.categories.find(cat => cat.ID === currentId).ParentID = null;  // Break the cycle by removing the parent
    currentId = nextId;
  } while (currentId !== startId);
  this.showMessage("Cyclic dependency automatically broken", 'info');
}

async toggleParentCategory(cat) {
    const confirmToggle = confirm("Вы уверены, что хотите отменить родительскую категорию?");
    if (confirmToggle) {
        try {
            const updatedCategory = {
                ...cat,
                ParentID: null
            };

            const response = await fetch(`${this.apiUrl}/categories/${cat.ID}`, {
                method: "PUT",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(updatedCategory),
            });
            this.logResponse(response, 'toggleParentCategory');

            if (!response.ok) {
                throw new Error('Не удалось отменить родительскую категорию');
            }

            // Обновляем локальный массив категорий
            const index = this.categories.findIndex(c => c.ID === cat.ID);
            if (index !== -1) {
                this.categories[index] = {...this.categories[index], ParentID: null};
            }

            // Обновляем визуальное представление
            this.$forceUpdate();

            this.showMessage('Статус родительской категории успешно изменен', 'success');
        } catch (error) {
            console.error("Ошибка при отмене родительской категории:", error);
            this.showMessage(`Ошибка при отмене родительской категории: ${error.message}`, 'error');

            // Откатываем изменения в случае ошибки
            await this.fetchCategories();
        }
    }
}

async toggleParentCategory(cat) {
  const confirmToggle = confirm("Are you sure you want to remove the parent category?");
  if (confirmToggle) {
    try {
      const updatedCategory = {
        ...cat,
        ParentID: null
      };

      const response = await fetch(`${this.apiUrl}/categories/${cat.ID}`, {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(updatedCategory),
      });
      this.logResponse(response, 'toggleParentCategory');

      if (!response.ok) {
        throw new Error('Failed to remove parent category');
      }

      // Update the local categories array
      const index = this.categories.findIndex(c => c.ID === cat.ID);
      if (index !== -1) {
        this.categories[index] = { ...this.categories[index], ParentID: null };
      }

      // Force update the UI
      this.$forceUpdate();

      this.showMessage('Parent category successfully removed', 'success');
    } catch (error) {
      console.error("Error removing parent category:", error);
      this.showMessage(`Error removing parent category: ${error.message}`, 'error');

      // Rollback changes in case of error
      await this.fetchCategories();
    }
  }
}