26-React File Manager Chapter XXVI: The Node Watcher
Hasan Zohdy
Posted on September 20, 2022
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);
});
});
};
}
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;
}
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>
</>
);
}
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>
);
}
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>
);
}
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>
);
}
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.
Posted on September 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.