Reputation: 11
I am attempting to create a UI centered around Windows which are able to split vertically and horizontally. I attempted to implement this with a tree structure using recursion. The WindowManager is connected to a rootstore and receives commands from the Windows to perform operations. The WindowManager stores the rootnode and begins the recursion. There are two main components, SplitWindow and Window. SplitWindows contain an array which stores SplitWindows and Windows for a specified direction and renders the splitter component. A Window is the main UI Component which contains tabs and the functionality to split/delete which is fed to the WindowManager using the rootstore.
When I split a Window, a MobX observer causes React re-render components, however, this causes some Windows to lose their state. For example when I split Window 1 vertically, window 1 is not re rendered ,however , the Windows below it are re-rendered and lose their tabs. Before Window 1 Vertical Split After Window 1 Vertical Split What can I do to prevent re-rendering of these Windows?
This is the Code I have written:
WindowManager:
import { useRootStore } from "../RootStore"
import WindowNode from "./WindowNode";
import SplitWindowNode from "./SplitWindowNode";
import { makeObservable, observable, action } from "mobx"
export function useWindowStore() {
const { windowManager } = useRootStore()
return windowManager
}
export default class WindowManager {
constructor() {
this.layoutRoot = new SplitWindowNode(null, [new WindowNode()])
makeObservable(this, {
splitWindow: action,
deleteWindow: action,
})
}
splitWindow(dir, windowNode) {
const parent = this.getNode(windowNode.parentKey)
if (parent.direction === dir || parent.direction == null) {
parent.direction = dir
const insertIdx = parent.children.findIndex(c => c.key === windowNode.key) + 1;
parent.addChild(new WindowNode(), insertIdx)
} else {
const split = new SplitWindowNode(dir, [windowNode, new WindowNode()])
parent.swap(windowNode, split)
}
}
deleteWindow(windowNode) {
let parent = this.getNode(windowNode.parentKey)
const windowNodeIndex = parent.children.indexOf(windowNode)
parent.children.splice(windowNodeIndex, 1)
if (parent.children.length < 2) {
const parentOfParent = this.getNode(parent.parentKey)
if (parentOfParent) {
if (parent.children.length == 1) {
parentOfParent.swap(parent, parent.children[0])
parent = null
}
} else {
if (parent.children.length == 0) {
parent.addChild(new WindowNode(), null)
}
}
}
}
getNode(nodeKey) {
if (nodeKey) {
const queue = [this.layoutRoot]
while (queue.length > 0) {
const node = queue.shift()
if (!node) return null
if (node.key === nodeKey) return node
if (node instanceof SplitWindowNode) {
node.children.forEach(c => queue.push(c))
}
}
}
return null
}
}
WindowContainer:
import { useWindowStore } from "./WindowManager"
import { Container } from "./WindowStyles";
import SplitWindow from "./SplitWindow";
function WindowContainer() {
const windowManager = useWindowStore()
return (
<Container>
<SplitWindow
splitWindowNode={windowManager.layoutRoot}
/>
</Container >
)
}
export default WindowContainer
SplitWindowNode:
import getID from "../UniqueId"
import { makeObservable, observable, computed } from "mobx"
class SplitWindowNode {
key = getID()
children = observable.array([])
constructor(direction, nodes) {
this.direction = direction
for (const n of nodes) {
this.addChild(n)
}
makeObservable(this, {
children: observable.shallow,
})
}
set parentKey(key) {
this._parentKey = key;
}
get parentKey() {
return this._parentKey;
}
addChild(node, pos) {
node.parentKey = this.key;
if (pos) {
this.children.splice(pos, 0, node)
} else {
this.children.push(node);
}
}
swap(toSwap, newNode) {
const swapIdx = this.children.findIndex(c => c.key === toSwap.key);
if (swapIdx === -1) {
throw new Error(`Cannot swap, node '${toSwap.key}' wasn't found in children.`);
}
newNode.parentKey = this.key;
this.children[swapIdx] = newNode;
}
}
export default SplitWindowNode
SplitWindow:
import React from "react"
import { observer } from "mobx-react-lite"
import ReactSplit, { SplitDirection as ReactSplitDirection } from "@devbookhq/splitter"
import Window from "./Window"
import WindowNode from "./WindowNode"
import { SplitDirection } from "../SplitDirection"
const SplitWindow = observer(({ splitWindowNode }) => {
const direction =
splitWindowNode.direction === SplitDirection.Horizontal
? ReactSplitDirection.Horizontal
: ReactSplitDirection.Vertical
return (
<ReactSplit direction={direction} onDidResize={console.log}>
{splitWindowNode.children.map(c => (
<React.Fragment key={c.key}>
{c instanceof WindowNode ? (
<Window key={c.key} windowNode={c} />
) : (
<SplitWindow key={c.key} splitWindowNode={c} />
)}
</React.Fragment>
))}
</ReactSplit>
)
})
export default SplitWindow
WindowNode:
import getID from "../UniqueId"
import { makeAutoObservable } from "mobx"
class WindowNode {
key = getID()
constructor() {
// makeAutoObservable(this)
}
set parentKey(key) {
this._parentKey = key
}
get parentKey() {
return this._parentKey
}
}
export default WindowNode
Window:
import React, { useEffect, useState } from "react"
import { observer } from "mobx-react-lite"
import { useWindowStore } from "./WindowManager"
import { SplitDirection } from "../SplitDirection"
import {
WindowContainer,
WindowHeader,
WindowControls,
WindowBody,
TabContainer
} from "./WindowStyles"
import {
Grid,
IconButton,
Tab
} from '@mui/material';
import {
TabList,
TabContext, TabPanel
} from '@mui/lab';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import SwapVertIcon from '@mui/icons-material/SwapVert';
import CloseIcon from '@mui/icons-material/Close';
import AddIcon from '@mui/icons-material/Add';
import ApplicationButton from "../applications/ApplicationButton";
const Window = ({ windowNode }) => {
const windowManager = useWindowStore()
function splitHorizontally() {
windowManager.splitWindow(SplitDirection.Horizontal, windowNode)
}
function splitVertically() {
windowManager.splitWindow(SplitDirection.Vertical, windowNode)
}
function close() {
windowManager.deleteWindow(windowNode)
}
const [tabs, setTabs] = useState([{
index: "Tab 0",
icon: <AppsIcon />,
application: "application_selector"
}])
const [selectedTab, setSelectedTab] = useState(tabs[0].index);
const [tabIndex, setTabIndex] = useState(1)
const createTab = () => {
const index = `Tab ${tabIndex}`
const newTab = {
index: index,
icon: <AppsIcon />,
application: "application_selector"
}
setTabs([...tabs, newTab])
setSelectedTab(index)
setTabIndex(tabIndex + 1)
}
const handleChange = (event, newTab) => {
if (event.target.type === "button") {
setSelectedTab(newTab);
}
};
const handleTabClose = (event, tab) => {
if (tabs.length > 1) {
const index = tabs.indexOf(tab)
const tabArr = tabs.filter(x => x !== tab)
setTabs(tabArr)
if (tab.index == selectedTab) {
if (index > tabArr.length - 1) {
setSelectedTab(tabArr[index - 1].index)
} else {
setSelectedTab(tabArr[index].index)
}
}
} else {
close()
}
}
function setApp(icon, app) {
const newTabs = [...tabs];
const selectedTabIndex = getSelectedTabIndex()
newTabs[selectedTabIndex].icon = icon
newTabs[selectedTabIndex].application = app
setTabs(newTabs)
}
function getSelectedTabIndex() {
return tabs.findIndex(tab => tab.index == selectedTab)
}
return (
<WindowContainer>
<TabContext value={selectedTab}>
<WindowHeader>
<TabContainer>
<TabList onChange={handleChange}>
{tabs.map(tab => (
<Tab
icon={
<CloseIcon onClick={(e) => handleTabClose(e, tab)} />
} iconPosition='end'
key={tab.index}
label={tab.icon}
value={tab.index}
/>
))}
</TabList>
<IconButton onClick={createTab}>
<AddIcon />
</IconButton>
</TabContainer>
{windowNode.key}
<WindowControls>
<IconButton onClick={splitHorizontally}>
<SwapHorizIcon />
</IconButton>
<IconButton onClick={splitVertically}>
<SwapVertIcon />
</IconButton>
<IconButton onClick={close}>
<CloseIcon />
</IconButton>
</WindowControls>
</WindowHeader>
<WindowBody>
{tabs.map(tab => (
<TabPanel key={tab.index} value={tab.index}>
</TabPanel>
))}
</WindowBody>
</TabContext>
</WindowContainer>
)
}
export default Window
Upvotes: 1
Views: 78