In our Vue/Vuetify 3 project we have generic table component which conditionally renders different components as table cells.
Unfortunately, tables are causing big memory leaks.
It is a big component, so I removed all the lines of code that I thougth are not important
<template>
<v-data-table-server :items="items">
<!-- Rows -->
<template #tbody="{ items: rows }">
<tbody v-if="!rows.length">
<tr
class="v-data-table-rows-loading">
<td :colspan="headers.length + showSelect">{{ $t(`common.${loading ? 'loading' : 'emptyList'}`) }}</td>
</tr>
</tbody>
<draggable
v-else
:list="rows"
draggable=".draggable-row"
ghost-class="ghost"
tag="tbody"
@end="onRowDrop" >
<tr v-for="item in rows"
:key="item[identifierColumn]"
:class="[draggableRows && !item.unremovable && 'draggable-row', item.color_raw]"
tabindex="0"
@click.exact.stop="!editingWidth.columnName && $emit('click:row', item)"
@click.ctrl.stop="!editingWidth.columnName && $emit('click:row', item, true)"
@keydown.enter="!editingWidth.columnName && $emit('click:row', item)">
<td v-for="(column, index) in headers"
:key="column.key + resetColumnWidthsKey"
style="height: 40px;"
:style="editingWidth.columnName === column.key ? `max-width: ${editingWidth.width}px; width: ${editingWidth.width}px;` : getTdStyle(column, index)"
:class="[item[column.key] && item[column.key].cell_color]"
:column="index">
<slot :name="`item.${column.key}`"
:item="item">
<div v-if="column.key === 'select'"
:style="{width: '28px'}">
<v-checkbox v-model="selectedItemsIds"
class="configurable-table-select"
:value="item[identifierColumn]"
hide-details
density="compact"
color="primary"
@click.stop
/>
</div>
<div v-else>
<DraggableIcon v-if="column.type === 'draggable'"
:disabled="item.unremovable" />
<component :is="getComponentName(column.type)"
v-else
:class="[item.is_read === 0 && 'font-weight-bold']"
:default-value="$t('common.noData')"
:row="item"
:column-name="column.key"
:disabled="item.unremovable"
:remote-dictionary="getRemoteDictionary(column.key)"
v-bind="{...column.props, ...$attrs, class: ''}"
:tab-index="index + 1"
:identifier-column="identifierColumn"
:format="column.format"
:patch-action="patchAction"
:action-prefix="actionPrefix"
:submodule-form-id="submoduleFormId"
:allow-overflow="allowOverflow"
:is-create-mode="isCreateMode"
:module-name="moduleName"
:submodule-name="submoduleName"
:can-editable="item.can_editable"
:parent-component-name="parentComponentName"
:dictionary="column.dictionary"
@click="$emit('click:cell-component', { row: item, column: column })"
@cellLoading="onCellLoading"
@editingWidth="editingWidth = $event" />
</div>
</slot>
</td>
</tr>
</draggable>
</template>
</v-data-table-server>
</template>
<script>
import { getColumnComponentName } from '@/services/table.service'
import { VueDraggableNext } from 'vue-draggable-next'
import { getDictionaries } from '@/services/dictionary.service.js'
export default {
name: 'ConfigurableTable',
components: {
draggable: VueDraggableNext
},
props: {
modelValue: {
type: Array,
default: () => []
},
columns: {
type: Array,
default: () => []
},
items: {
type: Array,
default: () => []
},
totalElements: {
type: Number,
default: 0
},
hideIdentifierColumn: {
type: Boolean
},
identifierColumn: {
type: String,
default: 'id'
},
loading: {
type: Boolean
},
remotePagination: {
type: Boolean
},
height: {
type: [String, Number],
default: '350px'
},
cursorPointer: {
type: [Boolean, Number],
default: false
},
draggableRows: {
type: Boolean
},
showSelect: {
type: Boolean
},
disablePagination: {
type: Boolean
},
moduleName: {
type: String,
required: false,
default: ''
},
submoduleName: {
type: String,
required: false,
default: ''
},
submoduleFormId: {
type: [String, Number],
default: null
},
relativeEntitySubmoduleId: {
type: [String, Number],
default: null
},
useSavedListSettings: {
type: Boolean,
default: false
},
isCreateMode: {
type: Boolean,
default: false
},
patchAction: {
type: Object,
default: () => ({})
},
parentComponentName: {
type: String,
default: null
},
customPageTransPrefix: {
type: String,
default: null
},
actionPrefix: {
type: String,
default: null
},
isPinDialog: {
type: Boolean,
default: false
},
footer: {
type: Object,
default: null
},
page: {
type: Number,
default: null
},
allColumns: {
type: Array,
default: () => []
},
allowOverflow: {
type: Boolean,
default: false
},
relativeModuleName: {
type: String,
required: false,
default: ''
},
isFirstLoad: {
type: Boolean,
default: false
},
tabName: {
type: String,
default: ''
},
listColumnSizes: {
type: Array,
default: null
},
submoduleId: {
type: Number,
default: null
},
staticListSettings: {
type: Boolean,
default: false
},
resetColumnWidths: {
type: Boolean,
default: false
},
draggableDisabled: {
type: Boolean,
default: false
},
level: {
type: Number,
default: null
},
customBody: {
type: Boolean,
default: false
}
},
data() {
return {
orderedColumns: [],
backendPerPage: 10,
sorting: {
column: null,
isDesc: false
},
remoteDictionaries: [],
cellLoading: false,
curCol: undefined,
pageX: undefined,
curColWidth: undefined,
curIndex: undefined,
curMinWidth: undefined,
columnsWidths: [],
listSettings: {
per_page: 50,
page: 1,
column: undefined,
direction: undefined
},
scrollingLeft: false,
scrollingRight: false,
resetColumnWidthsKey: 0,
editingWidth: {},
}
},
computed: {
headers() {
// For draggable columns draggable: col.draggable === undefined ? true : col.draggable, return [
...this.orderedColumns.map((col) => ({
...col,
draggable: false,
value: col.key,
sortable: this.isCreateMode ? false : col.sortable
}))
]
},
selectedItemsIds: {
get() {
return this.modelValue
},
set(items) {
this.$emit('update:modelValue', items)
}
}
},
methods: {
onRowDrop({ oldIndex, newIndex }) {
if (this.draggableDisabled || this.loading || this.cellLoading) return
this.$emit('changePosition', {
from: this.items[oldIndex],
to: this.items[newIndex],
oldIndex,
newIndex
})
},
getComponentName(columnType) {
return getColumnComponentName(columnType, this.parentComponentName)
},
getRemoteDictionary(columnKey) {
return this.remoteDictionaries[columnKey.split('.')[0]]
},
onCellLoading(loading) {
this.cellLoading = loading
},
}
}
</script>
Method ‘getComponentName’ takes name from js file where we have declared a bunch of components
export const getColumnComponentName = (columnType) => {
switch (columnType) {
case 'string':
return 'DefaultTableCell'
case 'checkbox':
return 'BooleanIndicator'
case 'editable':
return 'EditableCell'
case 'relationship_object':
return 'ListCell'
case 'hyperlink':
return 'HyperlinkCell'
case 'download':
return 'DownloadCell'
case 'initialsRelationshipObject':
return 'InitialsRelationshipCell'
case 'label':
return 'LabelCell'
case 'rangeInput':
return 'RangeInputCell'
case 'avatar':
return 'AvatarCell'
case 'copyInput':
return 'CopyInputCell'
case 'editableLabel':
return 'EditableLabelCell'
case 'actionCheckbox':
return 'ActionCheckboxCell'
case 'colorPicker':
return 'ColorPickerCell'
case 'attachment':
return 'AttachmentCell'
case 'attachments':
return 'AttachmentsCell'
case 'modalInput':
return 'ModalInputCell'
case 'tagsRelationshipObject':
return 'TagsRelationshipCell'
case 'editableString':
return 'EditableStringCell'
case 'editableDatePicker':
return 'EditableDatePickerCell'
case 'editableDateTimePicker':
return 'EditableDateTimePickerCell'
case 'editableTimeInput':
return 'EditableTimeInputCell'
case 'editableSelect':
return 'EditableSelectCell'
case 'editableRangeInput':
return 'EditableRangeInputCell'
case 'editableModal':
return 'EditableModalCell'
default:
return 'DefaultTableCell'
}
}
and these are registered globally in main.js file
import { createApp } from 'vue'
import App from './App.vue'
import { defineAsyncComponent } from 'vue'
const app = createApp(App)
app.component('EditableCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableCell.vue')));
app.component('DefaultTableCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/DefaultTableCell.vue')));
app.component('BooleanIndicator', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/BooleanIndicatorCell.vue')));
app.component('ListCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/ListCell.vue')));
app.component('DraggableIcon', defineAsyncComponent(() => import('@/components/DraggableIcon.vue')));
app.component('HyperlinkCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/HyperlinkCell.vue')));
app.component('DownloadCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/DownloadCell.vue')));
app.component('InitialsRelationshipCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/InitialsRelationshipCell.vue')));
app.component('LabelCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/LabelCell.vue')));
app.component('RangeInputCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/RangeInputCell.vue')));
app.component('AvatarCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/AvatarCell.vue')));
app.component('CopyInputCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/CopyInputCell.vue')));
app.component('EditableLabelCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableLabelCell.vue')));
app.component('ActionCheckboxCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/ActionCheckboxCell.vue')));
app.component('ColorPickerCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/ColorPickerCell.vue')));
app.component('AttachmentCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/AttachmentCell.vue')));
app.component('AttachmentsCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/AttachmentsCell.vue')));
app.component('ModalInputCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/ModalInputCell.vue')));
app.component('TagsRelationshipCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/TagsRelationshipCell.vue')));
app.component('EditableStringCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableStringCell.vue')));
app.component('EditableDatePickerCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableDatePickerCell.vue')));
app.component('EditableDateTimePickerCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableDateTimePickerCell.vue')));
app.component('EditableTimeInputCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableTimeInputCell.vue')));
app.component('EditableSelectCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableSelectCell.vue')));
app.component('EditableRangeInputCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableRangeInputCell.vue')));
app.component('EditableModalCell', defineAsyncComponent(() => import('@/components/ModulesComponents/TableComponents/EditableModalCell.vue')));
app.mount('#app')
I’m suspecting that’s because of usage of dynamic ‘component’ element, because I replaced it with dummy ‘div’ and memory leaks disappeared.
We don’t want to stop using dynamic cell components, because it will delete a lot of our functionalities. Any idea how to get rid of memory leaks and maintain functionality? Is it even possible? Maybe we should somehow compromise functionality and memory management? Maybe You see other cause of memory leaks?
I don’t know if it will be helpful, but we used to have the same problem when using Vue/Vuetify 2 and after migration to 3 it still haunts us.
Patryk Marchut is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.