DEV Community

Cover image for Let's create a React File Manager Chapter VI: Fetching Directories And Files
Hasan Zohdy
Hasan Zohdy

Posted on

Let's create a React File Manager Chapter VI: Fetching Directories And Files

Now let's start working with some logic, let's first define our workflow.

Workflow

Once the file manager is opened, we'll load the root path then list its directories on the sidebar, and its content (Files And Directories) on the right side.

So we need the following states:

  • Loading State to determine if the file manager is being loading directories.
  • currentDirectoryNode: State that contains the current loaded directory node.

That's it for now, let's see it in action.

// FileManager.tsx

export default function FileManager({
  open,
  onClose,
  rootPath,
}: FileManagerProps) {
  const [isLoading, setIsLoading] = useState(true);
  const [currentDirectoryNode, setCurrentDirectoryNode] = useState<Node>();

  return (
    <>
      <Modal size="xl" opened={open} onClose={onClose}>
        <Toolbar />
        <BodyWrapper>
          <Grid>
            <Grid.Col span={3}>
              <Sidebar />
            </Grid.Col>
            <Grid.Col span={9}>
              <Content />
            </Grid.Col>
          </Grid>
        </BodyWrapper>
      </Modal>
    </>
  );
}

FileManager.defaultProps = {
  rootPath: "/",
};
Enter fullscreen mode Exit fullscreen mode

I added rootPath in the default props object, and created two states as mentioned earlier.

Now let's make a new instance of the file manager and store it in a ref

import { Grid, Modal } from "@mantine/core";
import BaseFileManager from "app/file-manager/utils/FileManager";
import { useRef, useState } from "react";
import Content from "./Content";
import { BodyWrapper } from "./FileManager.styles";
import { FileManagerProps } from "./FileManager.types";
import Sidebar from "./Sidebar";
import Toolbar from "./Toolbar";
import { Node } from "../../types/FileManager.types";

export default function FileManager({
  open,
  onClose,
  rootPath,
}: FileManagerProps) {
  const [isLoading, setIsLoading] = useState(true);
  const [currentDirectoryNode, setCurrentDirectoryNode] = useState<Node>();

  const fileManagerRef = useRef(new BaseFileManager());

  return (
    <>
      <Modal size="xl" opened={open} onClose={onClose}>
        <Toolbar />
        <BodyWrapper>
          <Grid>
            <Grid.Col span={3}>
              <Sidebar />
            </Grid.Col>
            <Grid.Col span={9}>
              <Content />
            </Grid.Col>
          </Grid>
        </BodyWrapper>
      </Modal>
    </>
  );
}

FileManager.defaultProps = {
  rootPath: "/",
};
Enter fullscreen mode Exit fullscreen mode

We renamed it to BaseFileManager so we don't get confused between it and the component itself.

We actually may enhance the ref by directly destructing the current key

- const fileManagerRef = useRef(new BaseFileManager());
+ const { current: fileManager } = useRef(new BaseFileManager());
Enter fullscreen mode Exit fullscreen mode

Next let's create the useEffect hook that will load the root path.

import { Node } from "../../types/FileManager.types";

export default function FileManager({
  open,
  onClose,
  rootPath,
}: FileManagerProps) {
  const [isLoading, setIsLoading] = useState(true);
  const [currentDirectoryNode, setCurrentDirectoryNode] = useState<Node>();

  const { current: fileManager } = useRef(new BaseFileManager());

  // load root directory
  useEffect(() => {
    if (!rootPath) return;

    setIsLoading(true);

    fileManager.load(rootPath).then(() => {
      setIsLoading(false);
      setCurrentDirectoryPath(rootPath);
    });
  }, [rootPath, fileManager]);

  return (
    <>
      <Modal size="xl" opened={open} onClose={onClose}>
        <Toolbar />
        <BodyWrapper>
          <Grid>
            <Grid.Col span={3}>
              <Sidebar />
            </Grid.Col>
            <Grid.Col span={9}>
              <Content />
            </Grid.Col>
          </Grid>
        </BodyWrapper>
      </Modal>
    </>
  );
}

FileManager.defaultProps = {
  rootPath: "/",
};
Enter fullscreen mode Exit fullscreen mode

Now we added the rootPath to the dependency array, so the useEffect will be called once the rootPath is changed, also we called load as it will load the given path as current directory.

Before we jump to the load method, let's add another condition that if the file manager is not open then ignore the load.

  // load root directory
  useEffect(() => {
    if (!rootPath || !open) return;

    setIsLoading(true);

    fileManager.load(rootPath).then(directoryNode => {
      setIsLoading(false);
      currentDirectoryNode(directoryNode);
    });
  }, [rootPath, fileManager, open]);
Enter fullscreen mode Exit fullscreen mode

Creating load method

The load method will load the given path and return a promise that will be resolved once the path is loaded, also it will return a Node with the loaded directory as well.

Also we need to define the root path in the file manager, so we'll create a setRootPath method.


// file-manager/utils/FileManager.ts
import { Node } from "../types/FileManager.types";

export default class FileManager {
  /**
   * Root path
   */
  protected rootPath = "/";

  /**
   * Current directory path
   */
  protected currentDirectoryPath = "/";

  /**
   * Current directory node
   */
  protected currentDirectoryNode?: Node;

  /**
   * Set root path
   */
  public setRootPath(rootPath: string): FileManager {
    this.rootPath = rootPath;
    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

Heading to FileManagerService let's create a new instance from it and export it so we can use it directly.

At the end of the file, we'll export the instance.

// file-manager-service.ts
const fileManagerService = new FileManagerService();

export default fileManagerService;
Enter fullscreen mode Exit fullscreen mode

Now let's define the load method in the FileManager

// FileManager.ts
import fileManagerService from "../services/file-manager-service";
import { Node } from "../types/FileManager.types";

export default class FileManager {
  /**
   * Root path
   */
  protected rootPath = "/";

  /**
   * Current directory path
   */
  protected currentDirectoryPath = "/";

  /**
   * Current directory node
   */
  protected currentDirectoryNode?: Node;

  /**
   * Set root path
   */
  public setRootPath(rootPath: string): FileManager {
    this.rootPath = rootPath;
    return this;
  }

  /**
   * Load the given path
   */
  public load(path: string): Promise<Node> {
    return new Promise((resolve, reject) => {
      fileManagerService
        .list(path)
        .then(response => {
          this.currentDirectoryPath = path;
          this.currentDirectoryNode = response.data.node;
          resolve(this.currentDirectoryNode as Node);
        })
        .catch(reject);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

We used the list method from the FileManagerService to get the directory node, then we stored it in the currentDirectoryNode and returned it.

But we need to make a small modification in the list method, we need to return a single node from the backend that contains all listed children inside it directly, so the backend handle it.

import FileManagerServiceInterface from "../types/FileManagerServiceInterface";
import { 
+    newNode, 
-    listNodes
     } from "../utils/data";

export class FileManagerService implements FileManagerServiceInterface {
  /**
   * {@inheritDoc}
   */
  public list(directoryPath: string): Promise<any> {
    return new Promise(resolve => {
      resolve({
        data: {
-          node: listNodes(),
+          node: newNode(),
        },
      });
    });
  }
}

const fileManagerService = new FileManagerService();

export default fileManagerService;
Enter fullscreen mode Exit fullscreen mode

Going back to the load method, we also defined the current directory node which will be resolved for the load promise.

Now let's head to our FileManager.tsx component and console.log currentDirectoryNode to see what will be returned

  ...
  const { current: fileManager } = useRef(new BaseFileManager());

  console.log(currentDirectoryNode);
Enter fullscreen mode Exit fullscreen mode

As it is being randomly generated let's make another generated node for directories to make sure we always have directory.

// utils/data.ts

export function newNode(): Node {
  const isDirectory = faker.datatype.boolean();
  const node: Node = {
    name: isDirectory ? faker.system.directoryPath() : faker.system.fileName(),
    path: faker.system.filePath(),
    size: faker.datatype.number({ min: 1, max: 100000 }),
    isDirectory,
  };

  if (node.isDirectory) {
    node.children = listNodes(1, 3);
  }

  return node;
}

export function newDirectoryNode() {
  const node = newNode();
  node.children = listNodes(faker.datatype.number({ min: 3, max: 4 }), 5);
  node.name = faker.system.directoryPath();
  node.isDirectory = true;
  return node;
}
Enter fullscreen mode Exit fullscreen mode

I also made small modification on listNodes to accept min and max values to make sure we always have a directory with at least three children.

Now let's update our service class to call the newDirectoryNode instead of newNode

- import { newNode } from "../utils/data";
+ import { newDirectoryNode } from "../utils/data";

    return new Promise(resolve => {
      resolve({
        data: {
-          node: newNode(),
+          node: newDirectoryNode(),
        },
      });
    });
Enter fullscreen mode Exit fullscreen mode

We'll pause here and continue in the next article, if you get confused read the final code from the repository below then read the article again.

Next article will be about listing directories in the sidebar.

Article Repository

You can see chapter files in Github Repository

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

Salam.

Top comments (0)