Let's create a React File Manager Chapter XII: Progress Bars, Skeltons And Overlays
Tags: react, typescript, mongez, javascript
Published: 2022-09-14 15:51:42

---
title: Let's create a React File Manager Chapter XII: Progress Bars, Skeltons And Overlays
published: true
description: 
series: File Manager React Mongez
tags: react, typescript, mongez, javascript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4fmnreiqss4xt7e9wuz1.png
# Use a ratio of 100:42 for best results.
---
We can now navigate between directories, but we don't have any feedback when we are loading the files, so let's add a loading progress bar.

## Updating Root Path

We forgot to update file manager root path when we load file manager, so let's do it now.

```diff
// FileManager.tsx

  // load the given directory path
  const load = useCallback(
    (path: string, isRoot = false) => {
      setIsLoading(true);

+      if (isRoot) {
+        fileManager.setRootPath(path);
+      }

      fileManager.load(path).then(node => {
        setCurrentDirectoryNode(node);

        setIsLoading(false);
        if (isRoot) {
          setRootDirectoryNode(node);
        }
      });
    },
    [fileManager],
  );
```

## Removing Modal

So modal is good to display file manager in isolated layout, but what if we need to make it take the entire page, thus we need to remove the modal, and we can make another wrapper component for modal.

```tsx
// FileManager.tsx
import { Grid } from "@mantine/core";
import BaseFileManager from "app/file-manager/utils/FileManager";
import { useCallback, useEffect, useRef, useState } from "react";
import Content from "../../Content";
import FileManagerContext from "../../contexts/FileManagerContext";
import { Node } from "../../types/FileManager.types";
import { BodyWrapper } from "./FileManager.styles";
import { FileManagerProps } from "./FileManager.types";
import LoadingProgressBar from "./LoadingProgressBar";
import Sidebar from "./Sidebar";
import Toolbar from "./Toolbar";

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

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

  // load the given directory path
  const load = useCallback(
    (path: string, isRoot = false) => {
      setIsLoading(true);

      if (isRoot) {
        fileManager.setRootPath(path);
      }

      fileManager.load(path).then(node => {
        setCurrentDirectoryNode(node);

        setIsLoading(false);
        if (isRoot) {
          setRootDirectoryNode(node);
        }
      });
    },
    [fileManager],
  );

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

    load(rootPath, true);
  }, [rootPath, fileManager, load]);

  return (
    <FileManagerContext.Provider value={fileManager}>
      <LoadingProgressBar />
      <Toolbar />
      <BodyWrapper>
        <Grid>
          <Grid.Col span={3}>
            <Sidebar rootDirectory={rootDirectoryNode} />
          </Grid.Col>
          <Grid.Col span={9}>
            <Content />
          </Grid.Col>
        </Grid>
      </BodyWrapper>
    </FileManagerContext.Provider>
  );
}

FileManager.defaultProps = {
  rootPath: "/",
};
```

Don't forget to update the props types as well.

```tsx
// FileManager.types.ts
import { Node } from "../../types/FileManager.types";

export type FileManagerProps = {
  /**
   * Root path to open in the file manager
   *
   * @default "/"
   */
  rootPath?: string;
  /**
   * Callback for when a file/directory is selected
   */
  onSelect?: (node: Node) => void;
  /**
   * Callback for when a file/directory is double clicked
   */
  onDoubleClick?: (node: Node) => void;
  /**
   * Callback for when a file/directory is right clicked
   */
  onRightClick?: (node: Node) => void;
  /**
   * Callback for when a file/directory is copied
   */
  onCopy?: (node: Node) => void;
  /**
   * Callback for when a file/directory is cut
   */
  onCut?: (node: Node) => void;
  /**
   * Callback for when a file/directory is pasted
   * The old node will contain the old path and the new node will contain the new path
   */
  onPaste?: (node: Node, oldNode: Node) => void;
  /**
   * Callback for when a file/directory is deleted
   */
  onDelete?: (node: Node) => void;
  /**
   * Callback for when a file/directory is renamed
   * The old node will contain the old path/name and the new node will contain the new path/name
   */
  onRename?: (node: Node, oldNode: Node) => void;
  /**
   * Callback for when a directory is created
   */
  onCreateDirectory?: (directory: Node) => void;
  /**
   * Callback for when file(s) is uploaded
   */
  onUpload?: (files: Node[]) => void;
  /**
   * Callback for when a file is downloaded
   */
  onDownload?: (node: Node) => void;
};
```

Now let's clean our home page and just render the file manager.

```tsx
// HomePage.tsx
import Helmet from "@mongez/react-helmet";
import FileManager from "app/file-manager/components/FileManager";

export default function HomePage() {
  return (
    <>
      <Helmet title="home" appendAppName={false} />
      <FileManager />
    </>
  );
}
```

## Adding Progress Bar Component

As we mentioned before, we're going to use events as it's super powerful, so we can use it to listen when file manager is loading then we show the progress bar, once loading is done we hide it.

Create `components/LoadingProgressBar.tsx` file and add the following code:

```tsx
// LoadingProgressBar.tsx
import useFileManager from "../../hooks/useFileManager";

export default function LoadingProgressBar() {
  const fileManager = useFileManager();
  return <div>LoadingProgressBar</div>;
}
```

Nothing fancy here, we just need to get the file manager instance to listen to its events.

Now let's import it in our File Manager component and add it to the body.

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

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

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

  // load the given directory path
  const load = useCallback(
    (path: string, isRoot = false) => {
      setIsLoading(true);

      if (isRoot) {
        fileManager.setRootPath(path);
      }

      fileManager.load(path).then(node => {
        setCurrentDirectoryNode(node);

        setIsLoading(false);
        if (isRoot) {
          setRootDirectoryNode(node);
        }
      });
    },
    [fileManager],
  );

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

    load(rootPath, true);
  }, [rootPath, fileManager, open, load]);

  return (
    <FileManagerContext.Provider value={fileManager}>
      <Modal size="xl" opened={open} onClose={onClose}>
        <LoadingProgressBar />
        <Toolbar />
        <BodyWrapper>
          <Grid>
            <Grid.Col span={3}>
              <Sidebar rootDirectory={rootDirectoryNode} />
            </Grid.Col>
            <Grid.Col span={9}>
              <Content />
            </Grid.Col>
          </Grid>
        </BodyWrapper>
      </Modal>
    </FileManagerContext.Provider>
  );
}

FileManager.defaultProps = {
  rootPath: "/",
};
```

> Sometimes i paste the entire component code, and others i don't so you can see the changes, but you can always check the full code in the github repo.

Now let's use [Mantine Progress Bar](https://mantine.dev/core/progress/) and try it

```tsx
// LoadingProgressBar.tsx
import { Progress } from "@mantine/core";
import useFileManager from "../../hooks/useFileManager";

export default function LoadingProgressBar() {
  const fileManager = useFileManager();
  return <Progress size="lg" value={50} striped animate />;
}
```

It should look like

![Progress Bar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/seu2va23bmhopez144pc.png)

Now let's add the logic to show/hide the progress bar.

```tsx
// LoadingProgressBar.tsx
import { Progress } from "@mantine/core";
import { useEffect, useState } from "react";
import useFileManager from "../../hooks/useFileManager";

export default function LoadingProgressBar() {
  const fileManager = useFileManager();
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    // let's create an interval that will update progress every 300ms
    let interval: ReturnType<typeof setInterval>;

    // we'll listen for loading state
    const loadingEvent = fileManager.on("loading", () => {
      setProgress(5);

      interval = setInterval(() => {
        // we'll increase it by 10% every 100ms
        // if it's more than 100% we'll set it to 100%
        setProgress(progress => {
          if (progress >= 100) {
            clearInterval(interval);
            return 100;
          }

          return progress + 2;
        });
      }, 100);
    });

    // now let's listen when the loading is finished
    const loadEvent = fileManager.on("load", () => {
      // clear the interval
      setProgress(100);

      setTimeout(() => {
        clearInterval(interval);

        // set progress to 0
        setProgress(0);
      }, 300);
    });

    // unsubscribe events on unmount or when use effect dependencies change
    return () => {
      loadingEvent.unsubscribe();
      loadEvent.unsubscribe();
    };
  }, [fileManager]);

  if (progress === 0) return null;

  return <Progress size="lg" value={progress} striped animate />;
}
```

The code looks a bit complicated, but it's not that hard, we just create an interval that will increase the progress by 10% every 100ms, and we'll listen to `loading` and `load` events to start and stop the interval.

And when the effect is unmounted or dependencies change we'll unsubscribe the events.

Now to test it, we'll fake the loading by adding a `setTimeout` in our `list` function.

```tsx
// 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 => {
      setTimeout(() => {
        resolve({
          data: {
            node: fetchNode(directoryPath),
          },
        });
      }, 3000);
    });
  }
}
```

Now the progress bar should look like

![Progress Bar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lnv2bu11tcs56gblxiyv.png)

The sidebar is hidden, let's add [Skelton](https://mantine.dev/core/skeleton/) to it.

First we declare a loading state, then we'll listen for loading events.

```tsx
// Sidebar.tsx
import { Card, Skeleton } from "@mantine/core";
import { IconFolder, IconHome2 } from "@tabler/icons";
import { useEffect, useMemo, useState } from "react";
import useFileManager from "../../../hooks/useFileManager";
import { Node } from "../../../types/FileManager.types";
import SidebarNode from "./SidebarNode";

export type SidebarProps = {
  rootDirectory?: Node;
};

export default function Sidebar({ rootDirectory }: SidebarProps) {
  const rootChildren = useMemo(() => {
    return rootDirectory?.children?.filter(child => child.isDirectory);
  }, [rootDirectory]);

  const fileManager = useFileManager();

  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const loadingEvent = fileManager.on("loading", () => setIsLoading(true));

    const loadEvent = fileManager.on("load", () => setIsLoading(false));

    return () => {
      loadingEvent.unsubscribe();
      loadEvent.unsubscribe();
    };
  }, [fileManager]);

  if (isLoading) {
    return (
      <Card shadow={"sm"}>
        <Skeleton height={8} mt={6} radius="xl" />
        <Skeleton height={12} mt={6} width="80%" radius="sm" />
        <Skeleton height={8} mt={6} width="60%" radius="xl" />
        <Skeleton height={8} mt={6} radius="xl" />
        <Skeleton height={12} mt={6} width="80%" radius="sm" />
        <Skeleton height={8} mt={6} width="60%" radius="xl" />
        <Skeleton height={8} mt={6} radius="xl" />
        <Skeleton height={12} mt={6} width="80%" radius="sm" />
        <Skeleton height={8} mt={6} width="60%" radius="xl" />
        <Skeleton height={8} mt={6} radius="xl" />
        <Skeleton height={12} mt={6} width="80%" radius="sm" />
        <Skeleton height={8} mt={6} width="60%" radius="xl" />
      </Card>
    );
  }

  if (!rootDirectory) return null;

  return (
    <>
      <Card shadow="sm">
        <SidebarNode
          node={rootDirectory}
          navProps={{
            p: 0,
          }}
          icon={<IconHome2 size={16} color="#78a136" />}
        />
        {rootChildren?.map(child => (
          <SidebarNode
            navProps={{
              p: 0,
              pl: 10,
            }}
            key={child.path}
            icon={<IconFolder size={16} fill="#31caf9" />}
            node={child}
          />
        ))}
      </Card>
    </>
  );
}
```

Pretty neat, right?

Now let's jump to the content part.

## Content Loading State

We'll add a loading state to the content part, and we'll show a [Overlay](https://mantine.dev/core/loading-overlay/) when the content is loading.

As previous in sidebar, we'll add loading state and listen for loading events, we can just **copy/paste** the code xD.

```tsx
// Content.tsx
import { Card } from "@mantine/core";
import { useEffect, useState } from "react";
import useFileManager from "../hooks/useFileManager";

export default function Content() {
  const fileManager = useFileManager();

  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const loadingEvent = fileManager.on("loading", () => setIsLoading(true));

    const loadEvent = fileManager.on("load", () => setIsLoading(false));

    return () => {
      loadingEvent.unsubscribe();
      loadEvent.unsubscribe();
    };
  }, [fileManager]);

  return (
    <>
      <Card shadow="sm">
        <div>Content</div>
      </Card>
    </>
  );
}
```

If we notice, there is a pattern here, we're looking for a loading state, and we're listening for loading events, so we can create a custom hook to handle this.

Let's create a `hooks/useLoading` hook.

```tsx
// hooks/useLoading.ts
import { useEffect, useState } from "react";
import useFileManager from "./useFileManager";

export default function useLoading(): boolean {
  const fileManager = useFileManager();

  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const loadingEvent = fileManager.on("loading", () => setIsLoading(true));

    const loadEvent = fileManager.on("load", () => setIsLoading(false));

    return () => {
      loadingEvent.unsubscribe();
      loadEvent.unsubscribe();
    };
  }, [fileManager]);

  return isLoading;
}
```

Now let's use it in `Content.tsx` and `Sidebar.tsx`.

```tsx
// Sidebar.tsx
import { Card, Skeleton } from "@mantine/core";
import { IconFolder, IconHome2 } from "@tabler/icons";
import { useMemo } from "react";
import useLoading from "../../../hooks/useLoading";
import { Node } from "../../../types/FileManager.types";
import SidebarNode from "./SidebarNode";

export type SidebarProps = {
  rootDirectory?: Node;
};

export default function Sidebar({ rootDirectory }: SidebarProps) {
  const rootChildren = useMemo(() => {
    return rootDirectory?.children?.filter(child => child.isDirectory);
  }, [rootDirectory]);

  const isLoading = useLoading();

  if (isLoading) {
    return (
      <Card shadow={"sm"}>
        <Skeleton height={8} mt={6} radius="xl" />
        <Skeleton height={12} mt={6} width="80%" radius="sm" />
        <Skeleton height={8} mt={6} width="60%" radius="xl" />
        <Skeleton height={8} mt={6} radius="xl" />
        <Skeleton height={12} mt={6} width="80%" radius="sm" />
        <Skeleton height={8} mt={6} width="60%" radius="xl" />
        <Skeleton height={8} mt={6} radius="xl" />
        <Skeleton height={12} mt={6} width="80%" radius="sm" />
        <Skeleton height={8} mt={6} width="60%" radius="xl" />
        <Skeleton height={8} mt={6} radius="xl" />
        <Skeleton height={12} mt={6} width="80%" radius="sm" />
        <Skeleton height={8} mt={6} width="60%" radius="xl" />
      </Card>
    );
  }

  if (!rootDirectory) return null;
...
```

Same in `Content.tsx`.

```tsx
// Content.tsx
import { Card } from "@mantine/core";
import useLoading from "../hooks/useLoading";

export default function Content() {
  const isLoading = useLoading();

  return (
    <>
      <Card shadow="sm">
        <div>Content</div>
      </Card>
    </>
  );
}
```

Now let's create our overlay, but first we need to make a wrapper for the content, so we can position the overlay.

create a `Content.styles.tsx` file and add the following code.

```tsx
// Content.styles.tsx
import styled from "@emotion/styled";

export const ContentWrapper = styled.div`
  label: ContentWrapper;
  position: relative;
`;
```

Now we import it

```tsx
// Content.tsx
import { Card } from "@mantine/core";
import useLoading from "../hooks/useLoading";
import { ContentWrapper } from "./Content.styles";

export default function Content() {
  const isLoading = useLoading();

  return (
    <>
      <Card shadow="sm">
        <ContentWrapper>Content</ContentWrapper>
      </Card>
    </>
  );
}
```

As content height is small, let's set a height, and add a `overflow: auto` to the content wrapper.

```tsx
// Content.styles.tsx
import styled from "@emotion/styled";

export const ContentWrapper = styled.div`
  label: ContentWrapper;
  position: relative;
  height: 300px;
  overflow: auto;
`;
```

Let's also create a `SidebarWrapper` and add a `overflow: auto` to it.

```tsx
// Sidebar.styles.tsx
import styled from "@emotion/styled";

export const SidebarWrapper = styled.div`
  label: SidebarWrapper;
  overflow: auto;
  height: 300px;
  position: relative;
`;
```

Let's inject it in both Cards, the one in the loading and the other for the content.

```tsx
// Sidebar.tsx
import { Card, Skeleton } from "@mantine/core";
import { IconFolder, IconHome2 } from "@tabler/icons";
import { useMemo } from "react";
import useLoading from "../../../hooks/useLoading";
import { Node } from "../../../types/FileManager.types";
import { SidebarWrapper } from "./Sidebar.styles";
import SidebarNode from "./SidebarNode";

export type SidebarProps = {
  rootDirectory?: Node;
};

export default function Sidebar({ rootDirectory }: SidebarProps) {
  const rootChildren = useMemo(() => {
    return rootDirectory?.children?.filter(child => child.isDirectory);
  }, [rootDirectory]);

  const isLoading = useLoading();

  if (isLoading) {
    return (
      <Card shadow={"sm"}>
        <SidebarWrapper>
          <Skeleton height={8} mt={6} radius="xl" />
          <Skeleton height={12} mt={6} width="80%" radius="sm" />
          <Skeleton height={8} mt={6} width="60%" radius="xl" />
          <Skeleton height={8} mt={6} radius="xl" />
          <Skeleton height={12} mt={6} width="80%" radius="sm" />
          <Skeleton height={8} mt={6} width="60%" radius="xl" />
          <Skeleton height={8} mt={6} radius="xl" />
          <Skeleton height={12} mt={6} width="80%" radius="sm" />
          <Skeleton height={8} mt={6} width="60%" radius="xl" />
          <Skeleton height={8} mt={6} radius="xl" />
          <Skeleton height={12} mt={6} width="80%" radius="sm" />
          <Skeleton height={8} mt={6} width="60%" radius="xl" />
        </SidebarWrapper>
      </Card>
    );
  }

  if (!rootDirectory) return null;

  return (
    <Card shadow="sm">
      <SidebarWrapper>
        <SidebarNode
          node={rootDirectory}
          navProps={{
            p: 0,
          }}
          icon={<IconHome2 size={16} color="#78a136" />}
        />
        {rootChildren?.map(child => (
          <SidebarNode
            navProps={{
              p: 0,
              pl: 10,
            }}
            key={child.path}
            icon={<IconFolder size={16} fill="#31caf9" />}
            node={child}
          />
        ))}
      </SidebarWrapper>
    </Card>
  );
}
```

Now it looks like this:

![File Manager](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b8ednhthtrmxuolntmiu.png)

Back to Content Component, let's add the overlay.

```tsx
// Content.tsx
import { Card, LoadingOverlay } from "@mantine/core";
import useLoading from "../hooks/useLoading";
import { ContentWrapper } from "./Content.styles";

export default function Content() {
  const isLoading = useLoading();

  return (
    <>
      <Card shadow="sm">
        <ContentWrapper>
          <LoadingOverlay visible={isLoading} overlayBlur={2} />
        </ContentWrapper>
      </Card>
    </>
  );
}
```

And our final look is:

![File Manager](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cn37eircpz1jv6do2sjp.png)

We're done with loaders.

And we're good now with our progress, next chapter we'll make a stop and clean up our code, also we'll reorganize our files and structure.

## Article Repository

You can see chapter files in [Github Repository](https://github.com/hassanzohdy/file-manager-react/tree/12-Progress-Bars-Skeltons-And-Overlays)

> 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.