Let's create a React File Manager Chapter XI: Navigating between directories

hassanzohdy

Hasan Zohdy

Posted on September 14, 2022

Let's create a React File Manager Chapter XI: Navigating between directories

So we can see proper results, we need to make a file to fetch data from, as pseudo data, we will use data.json file, it will contain an array of directories and files, each directory will have an array of files and directories, and each file will have a name and a type, we will use this data to create our file manager.

Create utils/data.json file and insert inside it the following code:

{
  "path": "/",
  "name": "/",
  "size": 70533,
  "isDirectory": true,
  "children": [
    {
      "path": "/Library",
      "name": "Library",
      "size": 68201,
      "isDirectory": true,
      "children": [
        {
          "path": "/var/log/rhodium_lodge.lwp",
          "name": "var",
          "size": 87444,
          "isDirectory": false
        }
      ]
    },
    {
      "path": "/media/intelligent.jnlp",
      "name": "media",
      "size": 1236,
      "isDirectory": false
    },
    {
      "path": "/var/log/fire_furthermore_purity.mp4v",
      "name": "var",
      "size": 95179,
      "isDirectory": false
    },
    {
      "path": "/local/log",
      "name": "local",
      "size": 9739,
      "isDirectory": true,
      "children": [
        {
          "path": "/opt",
          "name": "opt",
          "size": 68469,
          "isDirectory": true,
          "children": [
            {
              "path": "/usr/libdata/southwest.vtf",
              "name": "usr",
              "size": 75879,
              "isDirectory": false
            }
          ]
        }
      ]
    },
    {
      "path": "/etc/mail/bossy_fess_compress.mvb",
      "name": "etc",
      "size": 47549,
      "isDirectory": false
    },
    {
      "path": "/usr/bin/arkansas_suv.mus",
      "name": "usr",
      "size": 98329,
      "isDirectory": false
    },
    {
      "path": "/home/concept_alliance_account.avi",
      "name": "home",
      "size": 37505,
      "isDirectory": false
    },
    {
      "path": "/private/tmp",
      "name": "private",
      "size": 96386,
      "isDirectory": true,
      "children": [
        {
          "path": "/opt/bin/program_rupee_views.ifb",
          "name": "opt",
          "size": 36417,
          "isDirectory": false
        }
      ]
    },
    {
      "path": "/home/qui_account_joule.srt",
      "name": "home",
      "size": 59250,
      "isDirectory": false
    },
    {
      "path": "/tmp",
      "name": "tmp",
      "size": 79276,
      "isDirectory": true,
      "children": [
        {
          "path": "/opt/share/east_beauty.sgl",
          "name": "opt",
          "size": 70401,
          "isDirectory": false
        }
      ]
    },
    {
      "path": "/home",
      "name": "home",
      "size": 77920,
      "isDirectory": true,
      "children": [
        {
          "path": "/private/var",
          "name": "private",
          "size": 50803,
          "isDirectory": true,
          "children": [
            {
              "path": "/var/mail",
              "name": "var",
              "size": 51658,
              "isDirectory": true,
              "children": [
                {
                  "path": "/Users",
                  "name": "Users",
                  "size": 66068,
                  "isDirectory": true,
                  "children": [
                    {
                      "path": "/media/bespoke_connecticut.wmd",
                      "name": "media",
                      "size": 92243,
                      "isDirectory": false
                    },
                    {
                      "path": "/usr/include",
                      "name": "usr",
                      "size": 59751,
                      "isDirectory": true,
                      "children": [
                        {
                          "path": "/usr/local/bin",
                          "name": "usr",
                          "size": 86273,
                          "isDirectory": true,
                          "children": [
                            {
                              "path": "/home/account.sc",
                              "name": "home",
                              "size": 28456,
                              "isDirectory": false
                            }
                          ]
                        },
                        {
                          "path": "/lost+found",
                          "name": "lost+found",
                          "size": 95753,
                          "isDirectory": true,
                          "children": [
                            {
                              "path": "/net/invoice_connecting_kuwait.ssf",
                              "name": "net",
                              "size": 48547,
                              "isDirectory": false
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ]
            },
            {
              "path": "/usr/bin/sdd_shoes_lead.ddf",
              "name": "usr",
              "size": 86112,
              "isDirectory": false
            }
          ]
        },
        {
          "path": "/net/steel_paradigms_generate.geo",
          "name": "net",
          "size": 5962,
          "isDirectory": false
        }
      ]
    },
    {
      "path": "/src",
      "name": "src",
      "size": 41634,
      "isDirectory": true,
      "children": [
        {
          "path": "/usr/libdata",
          "name": "usr",
          "size": 39378,
          "isDirectory": true,
          "children": [
            {
              "path": "/usr/lib",
              "name": "usr",
              "size": 20505,
              "isDirectory": true,
              "children": [
                {
                  "path": "/root/aggregate.igm",
                  "name": "root",
                  "size": 65337,
                  "isDirectory": false
                },
                {
                  "path": "/home/user/fussy_integrate.m21",
                  "name": "home",
                  "size": 69135,
                  "isDirectory": false
                }
              ]
            },
            {
              "path": "/var/mail",
              "name": "var",
              "size": 15543,
              "isDirectory": true,
              "children": [
                {
                  "path": "/etc/ppp/response_savings_southeast.xaml",
                  "name": "etc",
                  "size": 267,
                  "isDirectory": false
                }
              ]
            }
          ]
        },
        {
          "path": "/sbin",
          "name": "sbin",
          "size": 88790,
          "isDirectory": true,
          "children": [
            {
              "path": "/etc/periodic",
              "name": "etc",
              "size": 23663,
              "isDirectory": true,
              "children": [
                {
                  "path": "/media",
                  "name": "media",
                  "size": 81614,
                  "isDirectory": true,
                  "children": [
                    {
                      "path": "/Network/field.xer",
                      "name": "Network",
                      "size": 44406,
                      "isDirectory": false
                    },
                    {
                      "path": "/home/user/dir",
                      "name": "home",
                      "size": 89296,
                      "isDirectory": true,
                      "children": [
                        {
                          "path": "/selinux/orchestrator.xsm",
                          "name": "selinux",
                          "size": 84414,
                          "isDirectory": false
                        },
                        {
                          "path": "/media",
                          "name": "media",
                          "size": 68960,
                          "isDirectory": true,
                          "children": [
                            {
                              "path": "/usr/monstrous_moratorium_response.ttl",
                              "name": "usr",
                              "size": 57457,
                              "isDirectory": false
                            },
                            {
                              "path": "/usr/obj/movies.exr",
                              "name": "usr",
                              "size": 52956,
                              "isDirectory": false
                            }
                          ]
                        }
                      ]
                    }
                  ]
                },
                {
                  "path": "/opt/include",
                  "name": "opt",
                  "size": 24011,
                  "isDirectory": true,
                  "children": [
                    {
                      "path": "/usr/libdata",
                      "name": "usr",
                      "size": 57283,
                      "isDirectory": true,
                      "children": [
                        {
                          "path": "/var/log/sievert_west_guilder.cdkey",
                          "name": "var",
                          "size": 36818,
                          "isDirectory": false
                        },
                        {
                          "path": "/usr/ports",
                          "name": "usr",
                          "size": 62616,
                          "isDirectory": true,
                          "children": [
                            {
                              "path": "/Applications/kyat_saturate_home.grv",
                              "name": "Applications",
                              "size": 21664,
                              "isDirectory": false
                            },
                            {
                              "path": "/root",
                              "name": "root",
                              "size": 10739,
                              "isDirectory": true,
                              "children": [
                                {
                                  "path": "/private/var",
                                  "name": "private",
                                  "size": 90775,
                                  "isDirectory": true,
                                  "children": [
                                    {
                                      "path": "/Library",
                                      "name": "Library",
                                      "size": 85881,
                                      "isDirectory": true,
                                      "children": [
                                        {
                                          "path": "/mnt/cobalt_bicycle_earum.wsdl",
                                          "name": "mnt",
                                          "size": 55769,
                                          "isDirectory": false
                                        },
                                        {
                                          "path": "/etc",
                                          "name": "etc",
                                          "size": 8215,
                                          "isDirectory": true,
                                          "children": [
                                            {
                                              "path": "/usr/obj/virginia_manager.ait",
                                              "name": "usr",
                                              "size": 3988,
                                              "isDirectory": false
                                            }
                                          ]
                                        }
                                      ]
                                    }
                                  ]
                                },
                                {
                                  "path": "/lost+found/deposit_auxiliary.evy",
                                  "name": "lost+found",
                                  "size": 14874,
                                  "isDirectory": false
                                }
                              ]
                            }
                          ]
                        }
                      ]
                    },
                    {
                      "path": "/net/exploit_wooden_lead.rif",
                      "name": "net",
                      "size": 64496,
                      "isDirectory": false
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "path": "/usr/libdata",
      "name": "usr",
      "size": 76429,
      "isDirectory": true,
      "children": [
        {
          "path": "/etc/mail/riverside_associate.vst",
          "name": "etc",
          "size": 74947,
          "isDirectory": false
        }
      ]
    },
    {
      "path": "/tmp/uphold_input.ma",
      "name": "tmp",
      "size": 5102,
      "isDirectory": false
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now let's import it in our service and remove the old generators.

// file-manager-service.ts
- import { newDirectoryNode } from "../utils/data";
+ import rootNode from "./../utils/data.json";


  /**
   * {@inheritDoc}
   */
  public list(directoryPath: string): Promise<any> {
    return new Promise(resolve => {
      resolve({
        data: {
-          node: newDirectoryNode(),
+          node: rootNode,
        },
      });
    });
  }
Enter fullscreen mode Exit fullscreen mode

Now we can have stable nodes, but we need to make it more real, let's create a function that fetch the proper directory based on the given directory path.

Create fetchDirectory function in utils/helpers.ts file.

// helpers.ts
import { Node } from "../types/FileManager.types";
import rootNode from "./data.json";

export default function fetchNode(
  directoryPath: string,
  node?: Node,
): Node | undefined {
  if (!node) {
    node = rootNode;
  }

  if (node.path === directoryPath) {
    return node;
  } else if (node.isDirectory && node.children) {
    for (const child of node.children) {
      const foundNode = fetchNode(directoryPath, child);

      if (foundNode) {
        return foundNode;
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A simple recursion function here to check if the given node is the one we are looking for, if not, we will check the children of the node and so on.

Let's go back to our service again and update its code.

// file-manager-service.ts
import FileManagerServiceInterface from "../types/FileManagerServiceInterface";
import fetchNode from "../utils/helpers";

export class FileManagerService implements FileManagerServiceInterface {
  /**
   * {@inheritDoc}
   */
  public list(directoryPath: string): Promise<any> {
    return new Promise(resolve => {
      resolve({
        data: {
          node: fetchNode(directoryPath),
        },
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Before we move on

Again, as i mentioned in previous article, we're developing and scaling up our application as it will be more complicated over the next articles, but we're walking step by step so you know where we are now and where we're going.

Sidebar navigation

Now let's add the ability to navigate between directories, we will do that by adding a click handler to the sidebar node, when the user clicks on a node, we will set the current directory node to the clicked node.

// SidebarNode.tsx

  return (
    <>
      <NavLink
        {...navProps}
        active={isActiveNode}
+        onClick={() => fileManager.load(node.path)}
        label={
          <>
            <IconWrapper>{icon}</IconWrapper>
            <span>{node.name}</span>
          </>
        }
      />
    </>
  );
Enter fullscreen mode Exit fullscreen mode

Now we need to display a loader to tell that the file manager is loading a content, here we are going to introduce our secret weapon: Events

File Manager Events

So we need to know from the file manager when it is loading a directory, so we can display a loader, we can do that by adding an event to the file manager, so we can listen to it from the sidebar node.

// FileManager.types.tsx

/**
 * File manager events
 */
export type FileManagerEvents = "loading" | "load" | "directoryChange";
Enter fullscreen mode Exit fullscreen mode

loading event will be triggered before loading directory.

load event will be triggered after loading directory.

directoryChange event will be triggered after changing current directory.

Now let's add an event listener in our file manager.

// FileManager.tsx
import events, { EventSubscription } from "@mongez/events";

...
  /**
   * Add event listener to the given event
   */
  public on(event: FileManagerEvents, callback: any): EventSubscription {
    return events.subscribe(`file-manger.${event}`, callback);
  }
Enter fullscreen mode Exit fullscreen mode

We're going to use Mongez Events to manage events.

Now let's add trigger method to trigger the event.

// FileManager.tsx
...
  /**
   * Trigger the given event
   */
  public trigger(event: FileManagerEvents, ...args: any[]): void {
    events.trigger(`file-manger.${event}`, ...args);
  }
Enter fullscreen mode Exit fullscreen mode

Now let's add trigger the events from load method.

// FileManager.tsx
...
  /**
   * Load the given path
   */
  public load(path: string): Promise<Node> {
    // trigger loading event
    this.trigger("loading");

    return new Promise((resolve, reject) => {
      fileManagerService
        .list(path)
        .then(response => {
          // trigger load event as the directory has been loaded successfully.
          this.trigger("load", response.data.node);
          this.currentDirectoryPath = path;
          this.currentDirectoryNode = response.data.node;
          // if the current directory is not as the same loaded directory path,
          // then we'll trigger directory changed event.
          if (response.data.node.path !== this.currentDirectoryPath) {
            this.trigger("directoryChange", this.currentDirectoryNode);
          }

          resolve(this.currentDirectoryNode as Node);
        })
        .catch(reject);
    });
  }
Enter fullscreen mode Exit fullscreen mode

loading event will be triggered before loading directory, and once the response is sent successfully to us, we'll trigger load event, and if the current directory is not as the same loaded directory path, then we'll trigger directoryChange event.

Listening to current directory change

Now let's add an event listener to current directory change in sidebar node, if the current directory is changed then update the active state.

// SidebarNode.tsx
...
  useEffect(() => {
    fileManager.on("directoryChange", (newCurrentDirectory: Node) => {
      if (newCurrentDirectory.path !== node.path && isActiveNode) {
        setIsActiveNode(false);
      } else if (newCurrentDirectory.path === node.path && !isActiveNode) {
        setIsActiveNode(true);
      }
    });
  }, [fileManager, isActiveNode, node.path]);
Enter fullscreen mode Exit fullscreen mode

So we have two cases here, first one is current node is active but new current directory node's path is not the same as the node path, then we'll set it to be false.

Second case the current node is not active but new current directory node's path is the same as the node path, then we'll set it to be true.

Now we'll remove the old useEffect that was listening for fileManager.currentDirectoryNode to not make any conflicts.

So the final sidebar node should look like

// SidebarNode.tsx
import { NavLink, NavLinkProps } from "@mantine/core";
import { IconFolder } from "@tabler/icons";
import { useEffect, useState } from "react";
import useFileManager from "../../../hooks/useFileManager";
import { Node } from "../../../types/FileManager.types";
import { IconWrapper } from "./SidebarNode.styles";

export type SidebarNodeProps = {
  node: Node;
  icon?: React.ReactNode;
  navProps?: Partial<NavLinkProps>;
};

export default function SidebarNode({
  icon,
  node,
  navProps = {},
}: SidebarNodeProps) {
  const fileManager = useFileManager();

  const [isActiveNode, setIsActiveNode] = useState(
    node === fileManager.currentDirectoryNode,
  );

  useEffect(() => {
    const event = fileManager.on(
      "directoryChange",
      (newCurrentDirectory: Node) => {
        if (newCurrentDirectory.path !== node.path && isActiveNode) {
          setIsActiveNode(false);
        } else if (newCurrentDirectory.path === node.path && !isActiveNode) {
          setIsActiveNode(true);
        }
      },
    );
  }, [fileManager, isActiveNode, node]);

  return (
    <>
      <NavLink
        {...navProps}
        active={isActiveNode}
        onClick={() => fileManager.load(node.path)}
        label={
          <>
            <IconWrapper>{icon}</IconWrapper>
            <span>{node.name}</span>
          </>
        }
      />
    </>
  );
}

SidebarNode.defaultProps = {
  icon: <IconFolder fill="#31caf9" />,
};
Enter fullscreen mode Exit fullscreen mode

We need to unsubscribe from the event listener when the component is unmounted or the effect dependencies are updated.

  useEffect(() => {
    const event = fileManager.on(
      "directoryChange",
      (newCurrentDirectory: Node) => {
        if (newCurrentDirectory.path !== node.path && isActiveNode) {
          setIsActiveNode(false);
        } else if (newCurrentDirectory.path === node.path && !isActiveNode) {
          setIsActiveNode(true);
        }
      },
    );

+    return () => event.unsubscribe();
  }, [fileManager, isActiveNode, node]);
Enter fullscreen mode Exit fullscreen mode

Now we can successfully navigate between sidebar links.

File Manager

File Manager

In our next chapter, we will make progress bar to indicate loading progress.

The Power Of Events

If you noticed, we added an event to each sidebar node, but the catch here only the affected sidebar nodes will be re-rendered, so if we have 1000 nodes, and we click on one of them, only the affected node will be re-rendered, and the rest will be untouched.

Article Repository

You can see chapter files in Github Repository

Don't forget the main branch has the latest updated code.

Tell me where you are now

If you're following up with me this series, tell me where are you now and what you're struggling with, i'll try to help you as much as i can.

Salam.

💖 💪 🙅 🚩
hassanzohdy
Hasan Zohdy

Posted on September 14, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related