DEV Community

Cover image for 26-React File Manager Chapter XXVI: The Node Watcher
Hasan Zohdy
Hasan Zohdy

Posted on

26-React File Manager Chapter XXVI: The Node Watcher

So we've made our kernel to contain a beautiful tree, now we need to make it watch for changes in the tree, so we can update the tree when a new node is added or removed.

Create Directory

Now let's return to our createDirectory action, we should update the kernel tree once the directory is created.

// createDirectory.ts
import { toastLoading } from "design-system/components/Toast";
import Kernel from "../Kernel";
import fileManagerService from "../services/file-manager-service";

export default function createDirectory(kernel: Kernel) {
  return function create(
    directoryName: string,
    directoryPath: string = kernel.currentDirectoryNode?.path as string,
  ) {
    return new Promise((resolve, reject) => {
      const loader = toastLoading(
        "Creating directory...",
        "We are creating your directory, please wait a moment.",
      );

      fileManagerService
        .createDirectory(directoryName, directoryPath)
        .then(response => {
          loader.success("Success!", "Your directory has been created.");

          👉🏻 kernel.tree.setNode(response.data.node);

          resolve(response.data.node);
        })
        .catch(error => {
          loader.error("Error", error.response.data.error);
          reject(error);
        });
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

Now we told the kernel tree to set this node where it belongs, so it will update the tree.

useNodeWatcher

Now let's create a hook that will watch for changes in the tree, and update the tree when a change is detected.

// hooks/useNodeWatcher.ts
import { Node } from "app/file-manager/Kernel";
import { useEffect, useState } from "react";
import useKernel from "./useKernel";

export default function useWatchNodeChange(node?: Node) {
  const kernel = useKernel();

  // Store the node internally in a state
  const [internalNode, setNode] = useState<Node | undefined>(node);

  useEffect(() => {
    // watch for node change
    const event = kernel.on("nodeChange", (newNode: Node) => {
      // if the updated node is the same as the one we are watching
      // then update the internal node
      if (newNode.path === internalNode?.path) {
        setNode({ ...newNode });
      }
    });

    return () => event.unsubscribe();
  }, [internalNode, kernel]);

  useEffect(() => {
    setNode(node);
  }, [node]);

  return internalNode;
}
Enter fullscreen mode Exit fullscreen mode

Nothing here to explain, we stored the node in a state, and we're watching for changes in the kernel, if the updated node is the same as the one we're watching, then we update the internal node.

Updating the Nodes List

As we're already listening for directory change event, we need also to watch for the node itself to update the nodes list.

// NodesList.tsx
import { Grid } from "@mantine/core";
import {
  useCurrentDirectoryNode,
  useNodeWatcher,
} from "app/file-manager/hooks";
import { DirectoryNode, FileNode } from "./ContentNode";

export default function NodesList() {
  const currentDirectoryNode = useCurrentDirectoryNode();

  const node = useNodeWatcher(currentDirectoryNode);

  return (
    <>
      <Grid>
        {node?.directories?.map(node => (
          <Grid.Col key={node.path} span={2}>
            <DirectoryNode node={node} />
          </Grid.Col>
        ))}
        {node?.files?.map(node => (
          <Grid.Col key={node.path} span={2}>
            <FileNode node={node} />
          </Grid.Col>
        ))}
      </Grid>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

We removed the memo that collects the files and directories from the node thanks to prepareNode method.

Then we added the useNodeWatcher hook to watch for changes in the node.

Updating Sidebar

As the root is also considered as a node, we need to update the sidebar to watch for changes in the root node.

// Sidebar.tsx
import {
  Card,
  ScrollArea,
  Skeleton,
  ThemeIcon,
  useMantineTheme,
} from "@mantine/core";
import { IconFolder, IconHome2 } from "@tabler/icons";
import { useKernel, useLoading } from "app/file-manager/hooks";
import useWatchNodeChange from "../../hooks/useNodeWatcher";
import { SidebarWrapper } from "./Sidebar.styles";
import SidebarNode from "./SidebarNode";

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

  const theme = useMantineTheme();

  // get the kernel
  const kernel = useKernel();

  // watch for the root node for change
  const rootNode = useWatchNodeChange(kernel.rootNode);

  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 no root node yet, return null
  if (!rootNode) return null;

  return (
    <Card shadow="sm">
      <SidebarWrapper>
        <ScrollArea type="auto" style={{ height: "300px" }}>
          <SidebarNode
            node={rootNode}
            icon={
              <ThemeIcon variant="light" color={theme.colors.lime[1]}>
                <IconHome2 size={16} color={theme.colors.lime[9]} />
              </ThemeIcon>
            }
          />
          {rootNode.directories?.map(child => (
            <SidebarNode
              navProps={{
                pl: 25,
              }}
              key={child.path}
              icon={
                <ThemeIcon variant="light" color={theme.colors.blue[1]}>
                  <IconFolder size={16} color={theme.colors.blue[5]} />
                </ThemeIcon>
              }
              node={child}
            />
          ))}
        </ScrollArea>
      </SidebarWrapper>
    </Card>
  );
}
Enter fullscreen mode Exit fullscreen mode

We added the useWatchNodeChange hook to watch for changes in the root node.

Let's move the sidebar skeleton to a separate component.

// SidebarSkeleton.tsx
import { Card, Skeleton } from "@mantine/core";

export default function SidebarSkeleton() {
  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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now our sidebar component looks like this.

// Sidebar.tsx
import { Card, ScrollArea, ThemeIcon, useMantineTheme } from "@mantine/core";
import { IconFolder, IconHome2 } from "@tabler/icons";
import { useKernel, useLoading } from "app/file-manager/hooks";
import useWatchNodeChange from "../../hooks/useNodeWatcher";
import { SidebarWrapper } from "./Sidebar.styles";
import SidebarNode from "./SidebarNode";
import SidebarSkeleton from "./SidebarSkeleton";

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

  const theme = useMantineTheme();

  // get the kernel
  const kernel = useKernel();

  // watch for the root node for change
  const rootNode = useWatchNodeChange(kernel.rootNode);

  if (isLoading) return <SidebarSkeleton />;

  // if no root node yet, return null
  if (!rootNode) return null;

  return (
    <Card shadow="sm">
      <SidebarWrapper>
        <ScrollArea type="auto" style={{ height: "300px" }}>
          <SidebarNode
            node={rootNode}
            icon={
              <ThemeIcon variant="light" color={theme.colors.lime[1]}>
                <IconHome2 size={16} color={theme.colors.lime[9]} />
              </ThemeIcon>
            }
          />
          {rootNode.directories?.map(child => (
            <SidebarNode
              navProps={{
                pl: 25,
              }}
              key={child.path}
              icon={
                <ThemeIcon variant="light" color={theme.colors.blue[1]}>
                  <IconFolder size={16} color={theme.colors.blue[5]} />
                </ThemeIcon>
              }
              node={child}
            />
          ))}
        </ScrollArea>
      </SidebarWrapper>
    </Card>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next Chapter

In the next chapter we'll start make the node selection algorithm.

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.

Top comments (0)