How to remove a tab-panel with keep-alive.



  • I have q-tabs with ‘close’ buttons on them. I’ve made q-tab-panels ‘keep-alive’, so if the tab is still available but not active and user returns to the previously active tab, it will show the same state as before. But even when the tab is closed, the panel remains in memory. What’s the best way to destroy it when user closes the tab?

    Note: Tabs are managed by a store module, and each tab-panel contains a component from a large selection of possible components. So removing a tab involves removing an element from an array in the store state. It’s at that point that I need to do whatever it takes to destroy the panel component.



  • well, let’s see some code. I assume you are using something like this:

      <q-tab-panels keep-alive :value="value" @input="inputChanged" class="fit" :swipeable="$q.platform.has.touch">
        <q-tab-panel v-for="tab in tabsList" :name="tab.id" :key="tab.id" class="fit q-pa-none">
          <component-1 :tab="tab" v-if="tab.kind==='app-tree'" :key="tab.id"></component-1>
          <component-2 :tab="tab" v-if="tab.kind==='app-dashboard'" :key="tab.id"></component-2>
          ...
          <component-99 :tab="tab" v-if="tab.kind==='something-else'" :key="tab.id"></component-99>
    
    

    In above configuration you have:

    • dynamic tabs based on tabsList computed value
    • tab close button (implemented separately - basically just removing tab from tabsList)
    • tab visual state preserved via keep-alive
    • tab app state preserved by vuex (implemented in each tab component)
    • each tab is its own component
    • each visual tab is properly “destroyed” by vue because of dynamic “v-if” and “:key”

    You can optimize this config and make it even more dynamic by using vue “is” special attribute:

    https://vuejs.org/v2/api/#is

    BUT I would suggest to firstly make it working and then deal with “is” specifics.



  • @qyloxe Don’t have the ‘v-if’ or ‘:key’ bits. Can you explain ‘v-if="tab.kind===’ ?

    Here’s my complete code…

    <template>
      <div class="bg-white">
        <q-tabs
          dense
          no-caps
          inline-label
          align="left"
          :breakpoint="0"
          active-color="blue-grey-1"
          active-bg-color="active"
          indicator-color="red-10"
          switch-indicator
          class="tab-strip-style"
          v-model="selectedTab"
          @drop="onDrop($event)"
          @dragover="dragOver($event, null)"
          @dragleave="dragLeave($event)"
        >
          <q-tab v-for="tab in tabs" :key="tab.name"
            :name="tab.name"
            :id="tab.name"
            :label="tab.name"
            class="tab-style"
            :draggable="draggable"
            @dragstart="startDrag($event, tab)"
            @dragover="dragOver($event, tab)"
            @dragleave="dragLeave($event, tab)"
            @drop="onDrop($event, tab)"
            @click="onClick(tab.template)"
            >
            <q-btn dense flat icon="fas fa-times" size="xs"
    
              class="q-pr-none q-ml-sm q-mb-sm"
              @click.stop="closeTab(tab.name)"
            />
            <q-menu
              touch-position
              context-menu
            >
              <q-list dense style="min-width: 100px">
                <q-item v-if="pane === 0"
                  clickable v-close-popup
                  :disable="tabs.length < 2"
                  @click="moveTab(tab.name, 'v')">
                  <q-item-section>Split right</q-item-section
                >
                </q-item>
                <q-item v-if="pane === 0"
                  clickable v-close-popup
                  :disable="tabs.length < 2"
                  @click="moveTab(tab.name, 'h')"
                >
                  <q-item-section>Split down</q-item-section>
                </q-item>
                <q-item v-if="pane !== 0"
                  clickable v-close-popup
                  @click="moveTab(tab.name, 'home')"
                >
                  <q-item-section>Un-split</q-item-section>
                </q-item>
                <q-separator />
                <q-item clickable v-close-popup
                  @click="closeTab(tab.name)"
                >
                  <q-item-section>Close tab</q-item-section>
                </q-item>
              </q-list>
            </q-menu>
          </q-tab>
        </q-tabs>
    
        <q-tab-panels class="overflow-hidden"
          keep-alive
          v-model="selectedTab"
        >
          <!-- overflow hidden to hide unneeded scrollbars        -->
          <q-tab-panel class="q-pa-none overflow-hidden" id="tab-panel"
            v-for="tab in tabs" :key="tab.name"
            :name="tab.name"
            :style="panelHeightStyle"
          >
              <component
                :is="tab.template"
                :componentProps="tab.props"
                :splitterRatio="splitterRatio"
                :height="panelHeight"
              >
              </component>
          </q-tab-panel>
        </q-tab-panels>
      </div>
    </template>
    


  • @CWoodman said in How to remove a tab-panel with keep-alive.:

    question: “when the tab is closed, the panel remains in memory” - which memory: javascript object is not garbage collected, vue virtual dom node is still accessible or browser’s DOM node is still alive? How do you know that it really remains - is it visible, you have something on console, you can see it in the object inspector?

    @qyloxe Don’t have the ‘v-if’ or ‘:key’ bits. Can you explain ‘v-if="tab.kind===’ ?

    oh, from Vue point of view it is essentially the same as your <component :is="..." solution. It allows Vue to dynamically “destroy” virtual node. Your code has a good concept.

    Here is what I would check:

    1

    >      <q-tab v-for="tab in tabs" :key="tab.name"
    

    Is tab.name REALLY unique? Check this rigorously. And then check again.

    2

    >       <q-tab v-for="tab in tabs" :key="tab.name"
    >         ...
    >         :id="tab.name"
    

    Probably you do not need :id attribute. It could mess with vdom-dom interaction. Remove this. If you do need to access this component, please use Vues “ref” attribute:
    https://vuejs.org/v2/api/#ref

    3

    >         <q-btn dense flat icon="fas fa-times" size="xs"
    >           ...
    >           @click.stop="closeTab(tab.name)"
    

    You are removing tab from vuex tabs array in the context of the event which is fired from the DOM node of that specific tab. There are possible async-sync quirks in such situations. Make sure, that Vue vdom is updated AFTER the change to the state in Vuex. If this would be the case then $nextTick could help:
    https://vuejs.org/v2/api/#vm-nextTick

    4

    >       <q-tab-panel class="q-pa-none overflow-hidden" id="tab-panel"
    >         v-for="tab in tabs" :key="tab.name"
    >         :name="tab.name"
    

    error: Using constant id attribute (tab-panel) when the node is in v-for is an error. Id attribute should be unique in the whole document:
    https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id

    This is the first thing I would correct, there’s possibility that it will fix your “memory error”.

    Check again if tab.name is unique, this time if it is still unique DURING your Vue problematic life cycle events (beforeCreate/beforeDestroy).

    5

    >           <component
    >             :is="tab.template"
    >             :componentProps="tab.props"
    

    This is tricky. Two possible problems:

    • firstly if your dynamic component selected by tab.template has some “static” data, ie objects shared between all component instances, there is possibility for Vue to keep all of them in memory.
    • secondly - depending on your tab.props implementation, async-sync sequence when the tab is closed/destroyed, it is possible, that this code could make a javascript circular reference and prevent garbage collector from removing those objects. It is implementation specific, could be quite easily checked in object inspector.


  • @qyloxe

    a javascript circular reference … could be quite easily checked in object inspector.

    How is circular reference checked easily in the inspector?



  • @qyloxe Thanks for your input. Note the ‘id=’ and ‘overflow-hidden’ stuff is left over from attempts to get rid of unwanted scrollbars when the tabpanel is moved into a splitter pane. I can just delete those now. (Trying a different approach, but that’s another story!)

    Tabs are created by a store module that ensures names are unique.

    Not sure how to use nextTick in this case…
    The tab manager component just calls a store action to delete a tab:

    methods: {
        ...mapActions('workTabs', ['addTab', 'removeTab', 'selectTab', 'setDragTab', 'rearrangeTab', 'addSplit', 'moveTabHome']),
        ...mapActions('main', ['showHelp']),
    
        closeTab (name) {
          this.removeTab(name)
        },
    

    And the new setup is rendered when the store state changes…

    computed: {
      tabs: function () {
          const allTabs = this.tabList
          const myTabs = allTabs.filter(item => item.pane === this.pane)
          return myTabs
        },
    

    Like this…

    <q-tab v-for="tab in tabs" :key="tab.name"
            :name="tab.name"
    

    Where should I put the ‘nextTick’?



  • @dobbel said in How to remove a tab-panel with keep-alive.:

    @qyloxe

    a javascript circular reference … could be quite easily checked in object inspector.

    How is circular reference checked easily in the inspector?

    like this for example:

    https://developers.google.com/web/tools/chrome-devtools/memory-problems/heap-snapshots



  • @CWoodman said in How to remove a tab-panel with keep-alive.:

    Where should I put the ‘nextTick’?

    $nextTick should be called in the context of the event - such as:

         closeTab (name) {
          this.$nextTick(function () {
            // DOM is now updated
            // `this` is bound to the current instance
                 this.removeTab(name)
                 })
         },
    

    If this removeTab is using vuex action then it should be ok, because vuex actions are asynchronous - BUT - devil is always in the implementations, and we need to test, test and test.

    There is a potential another problem:

       tabs: function () {
           const allTabs = this.tabList
           const myTabs = allTabs.filter(item => item.pane === this.pane)
           return myTabs
         },
    

    myTabs has object references to vuex objects. Vuex objects could be modified only via commits and yet, if you change directly something in nested tab object properties (for example in tab.props) then it would break Vuex and possibly lead to circular reference. Make sure that you are testing your app in dev mode, as in this mode, vuex shows some additional errors in such situations. More here:
    https://vuex.vuejs.org/guide/strict.html

    Anyway, I have a similar IDE style app with dynamic tabs, and I’m also putting all those data in Vuex. Yet, I’m too afraid 🙂 to use those objects directly, just to avoid such hard situations as yours and I’m using those data only via this:
    https://vuex-orm.org/
    I have defined tabs model, the state changes are very clear, I can use nested subobjects safely, everything works OK, the computed properties are in most situations unnecessary, I use my app state as local DB with queries, updates, and inserts. I do recommend to use vuex-orm.



  • @qyloxe Thanks for the nextTick instruction. But the panel content still stays in memory when I close the tab.

    Re: the ‘other problem’…
    I thought that using const allTabs = this.tabList would safely protect the store state tablist from unwanted modification. Is that not the case? (Still learning javascript 🙂

    And I’ll check out vuex-orm.

    I really appreciate your help.



  • Can a parent component destroy one of its children? Or tell the child to commit suicide?



  • @CWoodman said in How to remove a tab-panel with keep-alive.:

    But the panel content still stays in memory when I close the tab.

    🙂 still don’t know what that means in your situation and how do you measure/observe that…

    I thought that using const allTabs = this.tabList would safely protect the store state tablist from unwanted modification. Is that not the case? (Still learning javascript 🙂

    Nope. This tutorial looks ok in our case:
    https://javascript.info/object-copy

    That reminds me, that if you want to check if this is really a problem with vuex nested references, then there is a simple testing method. Instead of this:

       return myTabs
    

    use for testing this:

       return JSON.parse(JSON.stringify(myTabs))
    

    This is the most safe method for deep cloning objects. If your code will start working after that, then it means you are messing with vuex state. If your code would still have “memory” problems, then the problem is somewhere else.



  • @CWoodman said in How to remove a tab-panel with keep-alive.:

    Can a parent component destroy one of its children? Or tell the child to commit suicide?

    Well, there is this instance method:

    https://vuejs.org/v2/api/#vm-destroy

    BUT if you want to use that (at the app level), it just means, that you are doing something wrong. The visual components should be managed by Vue, the virtual dom mechanics and at the lower level, by javascript rules for object references (that’s why I ask and ask about what you exactly mean by “memory problem”). You shouldn’t manually “destroy” components, you should just declare/inform/tell that you are no longer use them or need them. That should do.

    Bear in mind, that memory management in browsers is indeterministict, so even if you perceive memory loss, it could be garbage collected much, much later and everything is just ok. The problem is with memory deltas, ie when same objects are continuously taking heap memory without releasing it. It is observable in devtools as I said earlier.

    From what I saw in this thread, the question is still what is that “memory loss” exactly?



  • @qyloxe Thanks, I had forgotten to remember about object references.

    Here are a couple of screen shots that make me think stuff is being left in memory unnecessarily…

    First shot shows two tabs open, ‘Introduction’ and ‘Recipe: untitled’.Inactive Tab.png

    If I now close the ‘Introduction’ tab, I still see it in the Vue devtools…
    Tab removed.png

    Note, if I move one of two tabs to a split pane, and then close it, I remove the split and the closed tab no longer appears in the Vue component list.

    Here’s a view with two tabs in separate split panes…
    Split tabs.png

    And here the ‘StockList’ tab has been closed, automatically unsplitting the window…
    Split removed.png

    Seems to me, when I just close a tab in the same pane, it should get removed from the Vue component list. No?



  • @CWoodman ach you’re saying about vue dev panel! That’s different. I’m not sure how that vue dev panel works exactly - for example it allows to change the history so maybe it just mess somehow with the app?

    My observations:

    1. I checked with my app, where I have also dynamic tabs: in my app, the tabs are properly removed from vue dev tools. All is ok.

    2. Interesting, that it marks those tabs as inactive grey nodes. Don’t know what that means. Please look up in vue dev tool docs. Why they’re “inactive”?

    3. What is exactly your method of removing tab object from tabs list in vuex? Can you show those specific functions in vuex actions and setters?

    4. I had in the past some quirks with keep-alive mode in q-tabs, there were some github discussions even 🙂 Maybe, this is a long shot, but keep-alive could be counteintuitive.

    5. Please override all vue lifecycle events for your tab component and just console.log in all of them to see, if this tab is REALLY destroyed/created and what is the real sequence of those events in dev mode and in production mode.



  • @qyloxe I tried watching the memory used in devtools while repetitively opening and closing a tab with a fairly big set of data, and the memory seemed pretty constant - went up when the tab was added, and went down shortly after closing it. So maybe I’m worried about nothing - just got concerned by watching the Vue devtools component list with its ‘inactive’ items.

    My Vuex code for removing a tab is pretty big, because I have to manage up to 2 possible splitter panes as well. Plus a tab can be dragged from one splitter pane to another. I’ll share it if you really want to see it.



  • @CWoodman said in How to remove a tab-panel with keep-alive.:

    @qyloxe I tried watching the memory used in devtools while repetitively opening and closing a tab with a fairly big set of data, and the memory seemed pretty constant - went up when the tab was added, and went down shortly after closing it.

    That’s what I thought when I asked about what measures of memory you are taking.

    The chrome dev tools are only reliable measure.

    Vue dev tools in terms of memory are not reliable, because its observations could intefere with observed objects.

    I’ll share it if you really want to see it.

    Not necessarily, no. The important part is only one line of code in vuex setter, where you remove item from list. You can debug it yourself, and check what is happening to this particular removed object. This is more advanced stuff yet very, very rewarding.

    So, at the end of the day, I would leave your question as resolved at this stage, and in the future, in quality assurance moment, I would put more stress tests on tab behaviours. In the meantime I would just go to build the app and consider this ride an excellent teaching moment 🙂



  • I agree. Thanks again for your help!


Log in to reply