diff --git a/src/public/javascripts/services/context_menu.js b/src/public/javascripts/services/context_menu.js
index a0baa028e..895b968c4 100644
--- a/src/public/javascripts/services/context_menu.js
+++ b/src/public/javascripts/services/context_menu.js
@@ -4,10 +4,11 @@ const $contextMenuContainer = $("#context-menu-container");
 let dateContextMenuOpenedMs = 0;
 
 /**
+ * @param {NoteTreeWidget} treeWidget
  * @param event - originating click event (used to get coordinates to display menu at position)
  * @param {object} contextMenu - needs to have getContextMenuItems() and selectContextMenuItem(e, cmd)
  */
-async function initContextMenu(event, contextMenu) {
+async function initContextMenu(treeWidget, event, contextMenu) {
     event.stopPropagation();
 
     $contextMenuContainer.empty();
diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js
index 4c31c33ad..1a2fd5ddc 100644
--- a/src/public/javascripts/services/tree.js
+++ b/src/public/javascripts/services/tree.js
@@ -18,6 +18,10 @@ import keyboardActionService from "./keyboard_actions.js";
 
 let tree;
 
+function setTree(treeInstance) {
+    tree = treeInstance;
+}
+
 let setFrontendAsLoaded;
 const frontendLoaded = new Promise(resolve => { setFrontendAsLoaded = resolve; });
 
@@ -424,104 +428,6 @@ async function treeInitialized() {
     setFrontendAsLoaded();
 }
 
-async function initFancyTree($tree, treeData) {
-    utils.assertArguments(treeData);
-
-    $tree.fancytree({
-        autoScroll: true,
-        keyboard: false, // we takover keyboard handling in the hotkeys plugin
-        extensions: ["hotkeys", "dnd5", "clones"],
-        source: treeData,
-        scrollParent: $tree,
-        minExpandLevel: 2, // root can't be collapsed
-        click: (event, data) => {
-            const targetType = data.targetType;
-            const node = data.node;
-
-            if (targetType === 'title' || targetType === 'icon') {
-                if (event.shiftKey) {
-                    node.setSelected(!node.isSelected());
-                    node.setFocus(true);
-                }
-                else if (event.ctrlKey) {
-                    noteDetailService.loadNoteDetail(node.data.noteId, { newTab: true });
-                }
-                else {
-                    node.setActive();
-
-                    clearSelectedNodes();
-                }
-
-                return false;
-            }
-        },
-        activate: async (event, data) => {
-            // click event won't propagate so let's close context menu manually
-            contextMenuWidget.hideContextMenu();
-
-            const notePath = await treeUtils.getNotePath(data.node);
-
-            noteDetailService.switchToNote(notePath);
-        },
-        expand: (event, data) => setExpandedToServer(data.node.data.branchId, true),
-        collapse: (event, data) => setExpandedToServer(data.node.data.branchId, false),
-        init: (event, data) => treeInitialized(), // don't collapse to short form
-        hotkeys: {
-            keydown: await treeKeyBindingService.getKeyboardBindings()
-        },
-        dnd5: dragAndDropSetup,
-        lazyLoad: function(event, data) {
-            const noteId = data.node.data.noteId;
-
-            data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note));
-        },
-        clones: {
-            highlightActiveClones: true
-        },
-        enhanceTitle: async function (event, data) {
-            const node = data.node;
-            const $span = $(node.span);
-
-            if (node.data.noteId !== 'root'
-                && node.data.noteId === await hoistedNoteService.getHoistedNoteId()
-                && $span.find('.unhoist-button').length === 0) {
-
-                const unhoistButton = $('  (unhoist)');
-
-                $span.append(unhoistButton);
-            }
-
-            const note = await treeCache.getNote(node.data.noteId);
-
-            if (note.type === 'search' && $span.find('.refresh-search-button').length === 0) {
-                const refreshSearchButton = $('  ');
-
-                $span.append(refreshSearchButton);
-            }
-        },
-        // this is done to automatically lazy load all expanded search notes after tree load
-        loadChildren: (event, data) => {
-            data.node.visit((subNode) => {
-                // Load all lazy/unloaded child nodes
-                // (which will trigger `loadChildren` recursively)
-                if (subNode.isUndefined() && subNode.isExpanded()) {
-                    subNode.load();
-                }
-            });
-        }
-    });
-
-    $tree.on('contextmenu', '.fancytree-node', function(e) {
-        const node = $.ui.fancytree.getNode(e);
-
-        contextMenuWidget.initContextMenu(e, new TreeContextMenu(node));
-
-        return false; // blocks default browser right click menu
-    });
-
-    tree = $.ui.fancytree.getTree("#tree");
-}
-
 async function reload() {
     const notes = await loadTreeData();
 
@@ -745,12 +651,6 @@ async function sortAlphabetically(noteId) {
     await reload();
 }
 
-async function showTree($tree) {
-    const treeData = await loadTreeData();
-
-    await initFancyTree($tree, treeData);
-}
-
 ws.subscribeToMessages(message => {
    if (message.type === 'refresh-tree') {
        reload();
@@ -908,7 +808,6 @@ export default {
     getSelectedOrActiveNodes,
     clearSelectedNodes,
     sortAlphabetically,
-    showTree,
     loadTreeData,
     treeInitialized,
     setExpandedToServer,
@@ -923,5 +822,6 @@ export default {
     scrollToActiveNote,
     createNewTopLevelNote,
     duplicateNote,
-    getNodeByKey
+    getNodeByKey,
+    setTree
 };
\ No newline at end of file
diff --git a/src/public/javascripts/services/tree_context_menu.js b/src/public/javascripts/services/tree_context_menu.js
index dc9facebd..3739621dd 100644
--- a/src/public/javascripts/services/tree_context_menu.js
+++ b/src/public/javascripts/services/tree_context_menu.js
@@ -12,7 +12,12 @@ import protectedSessionHolder from "./protected_session_holder.js";
 import searchNotesService from "./search_notes.js";
 
 class TreeContextMenu {
-    constructor(node) {
+    /**
+     * @param {NoteTreeWidget} treeWidget
+     * @param {FancytreeNode} node
+     */
+    constructor(treeWidget, node) {
+        this.treeWidget = treeWidget;
         this.node = node;
     }
 
@@ -37,7 +42,7 @@ class TreeContextMenu {
         // some actions don't support multi-note so they are disabled when notes are selected
         // the only exception is when the only selected note is the one that was right-clicked, then
         // it's clear what the user meant to do.
-        const selNodes = treeService.getSelectedNodes();
+        const selNodes = this.treeWidget.getSelectedNodes();
         const noSelectedNotes = selNodes.length === 0
                 || (selNodes.length === 1 && selNodes[0] === this.node);
 
@@ -128,19 +133,19 @@ class TreeContextMenu {
             protectedSessionService.protectSubtree(this.node.data.noteId, false);
         }
         else if (cmd === "copy") {
-            clipboard.copy(treeService.getSelectedOrActiveNodes(this.node));
+            clipboard.copy(this.treeWidget.getSelectedOrActiveNodes(this.node));
         }
         else if (cmd === "cloneTo") {
-            const nodes = treeService.getSelectedOrActiveNodes(this.node);
+            const nodes = this.treeWidget.getSelectedOrActiveNodes(this.node);
             const noteIds = nodes.map(node => node.data.noteId);
 
             import("../dialogs/clone_to.js").then(d => d.showDialog(noteIds));
         }
         else if (cmd === "cut") {
-            clipboard.cut(treeService.getSelectedOrActiveNodes(this.node));
+            clipboard.cut(this.treeWidget.getSelectedOrActiveNodes(this.node));
         }
         else if (cmd === "moveTo") {
-            const nodes = treeService.getSelectedOrActiveNodes(this.node);
+            const nodes = this.treeWidget.getSelectedOrActiveNodes(this.node);
 
             import("../dialogs/move_to.js").then(d => d.showDialog(nodes));
         }
@@ -151,7 +156,7 @@ class TreeContextMenu {
             clipboard.pasteInto(this.node);
         }
         else if (cmd === "delete") {
-            treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(this.node));
+            treeChangesService.deleteNodes(this.treeWidget.getSelectedOrActiveNodes(this.node));
         }
         else if (cmd === "export") {
             const exportDialog = await import('../dialogs/export.js');
@@ -162,7 +167,7 @@ class TreeContextMenu {
             importDialog.showDialog(this.node);
         }
         else if (cmd === "collapseSubtree") {
-            treeService.collapseTree(this.node);
+            this.treeWidget.collapseTree(this.node);
         }
         else if (cmd === "forceNoteSync") {
             syncService.forceNoteSync(this.node.data.noteId);
diff --git a/src/public/javascripts/widgets/note_tree.js b/src/public/javascripts/widgets/note_tree.js
index c2f4da4e7..4e53e16cf 100644
--- a/src/public/javascripts/widgets/note_tree.js
+++ b/src/public/javascripts/widgets/note_tree.js
@@ -5,6 +5,13 @@ import keyboardActionService from "../services/keyboard_actions.js";
 import treeService from "../services/tree.js";
 import treeUtils from "../services/tree_utils.js";
 import noteDetailService from "../services/note_detail.js";
+import utils from "../services/utils.js";
+import contextMenuWidget from "../services/context_menu.js";
+import treeKeyBindingService from "../services/tree_keybindings.js";
+import dragAndDropSetup from "../services/drag_and_drop.js";
+import treeCache from "../services/tree_cache.js";
+import treeBuilder from "../services/tree_builder.js";
+import TreeContextMenu from "../services/tree_context_menu.js";
 
 const TPL = `