Skip to main content

How to properly organize mobx data structure in react app with complex hierarchy

I'm writing an app with react framework, mobx state manager and using typescript. The application includes a left menu and a workspace in the form of tabs. As in the picture below. Visual representation of the application Each tab contains its own content. As in any browser.Tabs can be created by clicking on the menu button, as well as by clicking on the button from the content of the tab itself. For example, we open the first tab. Its content is a table that contains information about some list of elements. We can edit each element by clicking on the "Edit" button, this should open a new tab. Table with editable data Thus we get a hierarchical structure. Also, the same tabs should manage one global storage. What MobX data structure would you suggest to use for such an application?

At the moment, I came up with the following data structure Based on the MVVM pattern. There is a global tab manager which has the following methods: add tab, remove tab and set current open tab.

class TabManager {
    currentTab: BaseTabViewModel | null = null
    allTabs: BaseTabViewModel[] = []

    constructor() {
        makeAutoObservable(this, {}, { autoBind: true })
    }

    addTab<T extends BaseTabViewModel>(Tab: T) {
        this.allTabs.push(Tab)
        this.setCurrentTab(Tab)
    }

    deleteTab<T extends BaseTabViewModel>(Tab: T) {
        this.allTabs.splice(this.allTabs.indexOf(Tab), 1)
    }

    setCurrentTab<T extends BaseTabViewModel>(Tab: T) {
        this.currentTab = Tab
    }
}

export default TabManager

Each tab is inherited from the base tab and is rendered by the view model.The base model looks like this.

class BaseTabViewModel {
    id: string | null = null
    title = ''

    constructor(id: string, title: string) {
        this.id = id
        this.title = title
    }
}

export default BaseTabViewModel

The tab itself might look like this::

class SomeTabViewModel extends BaseTabViewModel {
    
    constructor(
        id: string,
        title: string,
        private readonly _someStore: SomeStore,
        private readonly _someService: typeof SomeService
    ) {
        super(id, title)
        this.getListItems()
    }

    get someStore() {
        return this._someStore
    }

    getListItems = () => {
        this._someService
            .getListItems()
            .then((someItemsResponse) => {
                runInAction(() => {
                this.someStore.setListItems(someItemsResponse)
                })
            })
            .catch()
    }

    addSomeItem = (data: NewSomeItemRequest) => {
        this._someService
            .addSomeItem(data)
            .then((someItemResponse) => {
                runInAction(() => {
                this.someStore.addSomeItem(someItemResponse)
                })
            })
            .catch()
    }

    //...other methods
}

export default SomeTabViewModel 

Services simply interact with the server through requests. The store simply stores business entities and operations on them. The base store from which the rest are inherited:

export default class BaseListStore<TListItem extends Identifiable> {
    protected items: Array<TListItem> = []

    constructor() {
        makeObservable<BaseListStore<TListItem>>(this, {
            list: computed,
            setItems: action,
            addItem: action,
            removeItem: action
        })
    }

    get list(): Array<TListItem> {
        return this.items
    }

    setItems(items: Array<TListItem>) {
        this.items = items
    }

    addItem(item: TListItem) {
        this.items.push(item)
    }

  removeItem(item: TListItem) {
    this.items.splice(this.items.indexOf(item), 1)
  }
}

export type BaseListStoreType = BaseListStore<Identifiable>

Tabs are rendered according to the following principle.

const MainTabs: FC = observer(() => {
    const { allTabs, currentTab, deleteTab } = useTabManager()

    return (
        <div className={styles.tabsContainer}>
            {allTabs.length !== 0 && (
                <Tabs>
                    <TabList>
                        {allTabs.map((tab) => (
                            <Tab key={tab.id}>
                                <div className={styles.title}>{tab.title}</div>
                                <div className={styles.close} onClick={() => deleteTab(tab)}>
                                    <FontAwesomeIcon icon={faTimes} />
                                </div>
                            </Tab>
                        ))}
                    </TabList>
                    {allTabs.map((tab) => (
                        <TabPanel key={tab.id}>
                            {tab instanceof SomeTabViewModel && <SomeTabScreen content={tab} />}
                            {tab instanceof SecondSomeTabViewModel && (
                                <SecondSomeTabScreen content={tab} />
                            )}
                        </TabPanel>
                    ))}
                </Tabs>
            )}
        </div>
    )
})

export default MainTabs

Complexity arises in those situations when we create a tab that depends on the previous one. Since the tabs are stored independently of each other and neither of them knows about the existence of the other, there is some difficulty. Let's imagine that we have a tab with a table and we clicked on the "Edit" button from the example above. How to transfer a specific item there? I didn’t come up with anything better than creating a view model that stores information about a specific entity. Example:

class RedactorTabViewModel extends BaseTabViewModel {

    constructor(
        id: string,
        title: string,
        private readonly _specificItem: Item,
        private readonly _notificationService: typeof SomeService
    ) {
        super(id, title)
    }

    get item() {
        return this._specificItem
    }

    getFile = () => {
        if (!this.item.fileId) return
        this._someService.getFile(this.item.fileId).then((data: File) => {
            runInAction(() => {
                this.item.setFile(data)
            })
        })
    }
}

export default RedactorTabViewModel

But this approach, in my opinion, is not the most correct. Because we are modifying an entity that is in a particular store regardless of that store. What storage structure can you suggest or my approach is optimal?

Via Active questions tagged javascript - Stack Overflow https://ift.tt/FTH1t2w

Comments

Popular posts from this blog

How to show number of registered users in Laravel based on usertype?

i'm trying to display data from the database in the admin dashboard i used this: <?php use Illuminate\Support\Facades\DB; $users = DB::table('users')->count(); echo $users; ?> and i have successfully get the correct data from the database but what if i want to display a specific data for example in this user table there is "usertype" that specify if the user is normal user or admin i want to user the same code above but to display a specific usertype i tried this: <?php use Illuminate\Support\Facades\DB; $users = DB::table('users')->count()->WHERE usertype =admin; echo $users; ?> but it didn't work, what am i doing wrong? source https://stackoverflow.com/questions/68199726/how-to-show-number-of-registered-users-in-laravel-based-on-usertype

Why is my reports service not connecting?

I am trying to pull some data from a Postgres database using Node.js and node-postures but I can't figure out why my service isn't connecting. my routes/index.js file: const express = require('express'); const router = express.Router(); const ordersCountController = require('../controllers/ordersCountController'); const ordersController = require('../controllers/ordersController'); const weeklyReportsController = require('../controllers/weeklyReportsController'); router.get('/orders_count', ordersCountController); router.get('/orders', ordersController); router.get('/weekly_reports', weeklyReportsController); module.exports = router; My controllers/weeklyReportsController.js file: const weeklyReportsService = require('../services/weeklyReportsService'); const weeklyReportsController = async (req, res) => { try { const data = await weeklyReportsService; res.json({data}) console...

How to split a rinex file if I need 24 hours data

Trying to divide rinex file using the command gfzrnx but getting this error. While doing that getting this error msg 'gfzrnx' is not recognized as an internal or external command Trying to split rinex file using the command gfzrnx. also install'gfzrnx'. my doubt is I need to run this program in 'gfzrnx' or in 'cmdprompt'. I am expecting a rinex file with 24 hrs or 1 day data.I Have 48 hrs data in RINEX format. Please help me to solve this issue. source https://stackoverflow.com/questions/75385367/how-to-split-a-rinex-file-if-i-need-24-hours-data