Memory leak in Vue 3 with router and v-for - javascript

I have serious problems with memory leaks in a rather large Vue 3 application (TS, Vuex).
It has a hierarchical structure with a large number of sub-components which amplifies the problems.
I managed to reproduce the issue in a very simple way and search your advice and opinion on that.
Reproduction:
Create a fresh Vue 3 application with Vue CLI (4.5.15) with TypeScript & Babel (maybe not necessary, but similar to my application).
Replace the Home.vue with the following code:
<template>
<div>
<button #click="clearArray">clearArray</button>
<button #click="fillArray">fillArray</button>
<div v-for="i in testArray" :key="i">{{ i }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "LeakDetector",
beforeUnmount() {
this.clearArray();
},
setup() {
let testArray = ref<number[] | null>(null);
const clearArray = () => {
testArray.value = null;
};
const fillArray = () => {
testArray.value = Array.from({ length: 100000 }, (v, k) => k);
};
return { testArray, clearArray, fillArray };
},
});
</script>
I was thinking that Vue would take care of the onmounting/cleaning of regular HTML elements. But if I now
click the fillArray button
go to About page and back to Home page
the memory of those many div elememts cannot be freed by garbage collection. The call of clearArray in the beforeUnmount hook does not help.
But if I
click the fillArray button
click the button clearArray and then
change the route and back
the garbage collection works.
I checked in chrome dev tools (memory tab) and I can find those detached elements there in case the array wasn't cleared. Also in Chrome Task Manager memory piles up quickly if the clearArray button isn't clicked before route change.
So:
Am I doing / understanding / analysing something wrong?
Is it necessary to do some "cleaning" for v-for-tags? (I didn't find anything in the vue docs)
Is it a misbehavior / bug inside of vue?
UPDATE:
I tried a production build and the problem disappeared. That made me think of Vue.js devtools (6.0.0 beta 21) running in my browser. So I disabled that and it made the problem in the reproduction example disappear... I'll analyse that in the application and give a final update in case anybody else faces that issue. Thanks so far!
CONCLUTION:
I was on a wrong track there. The memory leak of the real application emerged from the following 2 things (and a bit of missing experience in interpreting the heaps):
Usage of const removeWatcher = this.$watch(...) which does not seem to remove the listeners when calling the removeWatcher() function. So replaced those with different functionality. I didn't debug that deeply. Might be another issue.
I forgot an .off() when using mitt for an EventBus.

Related

Call a function on a react child functional component from parent

I have a very large and complex React application. It is designed to behave like a desktop application. The interface is a document style interface with tabs, each tab can be one of many different type of editor component (there are currently 14 different editor screens). It is possible to have a very large number of tabs open at once (20-30 tabs). The application was originally written all with React class components, but with newer components (and where significant refactors have been required) I've moved to functional components using hooks. I prefer the concise syntax of functions and that seems to be the recommended direction to take in general, but I've encountered a pattern from the classes that I don't know how to replicate with functions.
Basically, each screen (tab) on the app is an editor of some sort (think Microsoft office, but where you can have a spreadsheet, text document, vector image, Visio diagram, etc all in tabs within the same application... Because each screen is so distinct they manage their own internal state. I don't think Redux or anything like that is a good solution here because the amount of individually owned bits of state are so complex. Each screen needs to be able to save it's current working document to the database, and typically provides a save option. Following standard object oriented design the 'save' function is implemented as a method on the top level component for each editor. However I need to perform a 'save-all' function where I iterate through all of the open tabs and call the save method (using a reference) on each of the tabs. Something like:
openTabs.forEach((tabRef) => tabRef.current.save());
So, If I make this a functional component then I have my save method as a function assigned to a constant inside the function:
const save = () => {...}
But how can I call that from a parent? I think the save for each component should live within that component, not at a higher level. Aside from the fact that would make it very difficult to find and maintain, it also would break my modular loading which only loads the component when needed as the save would have to be at a level above the code-splitting.
The only solution to this problem that I can think of is to have a save prop on the component and a useEffect() to call the save when that save prop is changed - then I'd just need to write a dummy value of anything to that save prop to trigger a save... This seems like a very counter-intuitive and overly complex way to do it.... Or do I simply continue to stick with classes for these components?
Thankyou,
Troy
But how can I call that from a parent? I think the save for each component should live within that component, not at a higher level.
You should ask yourself if the component should be smart vs dumb (https://www.digitalocean.com/community/tutorials/react-smart-dumb-components).
Consider the following:
const Page1 = ({ onSave }) => (...);
const Page2 = ({ onSave }) => (...);
const App = () => {
const handleSavePage1 = (...) => { ... };
const handleSavePage2 = (...) => { ... };
const handleSaveAll = (...) => {
handleSavePage1();
handleSavePage2();
};
return (
<Page1 onSave={handleSavePage1} />
<Page2 onSave={handleSavePage2} />
<Button onClick={handleSaveAll}>Save all</button>
);
};
You've then separated the layout from the functionality, and can compose the application as needed.
I don't think Redux or anything like that is a good solution here because the amount of individually owned bits of state are so complex.
I don't know if for some reason Redux is totally out of the picture or not, but I think it's one of the best options in a project like this.
Where you have a separated reducer for each module, managing the module's state, also each reducer having a "saveTabX" action, all of them available to be dispatched in the Root component.

Can I give arguments to a react-app? (dependency injection with React)

I would like to use a React-App, and give it some arguments (props), depending on where I embed it.
So one step back, we are using React actually as a library, and not the entire page is in React. We have a very functioning website, and some parts are now being build in react. Our motivation is: If you want the same component in another page, you can simply copy-paste 3 lines which include the css and js files, put a <div id="myReactAppRoot"></div>, and that's it. Very quick, very clean, and instantly functioning.
My question
When I have another "copy" of the React app, I would like to give it a different starting state.
One example use-case: I have two pages, in one I want the data to be grouped by X and in the other grouped by Y.
Another use-case, which I will have on my next project: disable editng, depending on the users permission level (the permission level is known by the main page).
How can I achive this?
My current solution
My current solution is simply having a utils.js in the React app, which I use to access the windows object and get out the pieces I want:
const utils = {
fun1: window.myProject.forReact,fun1,
groupElements: window.myProject.forReact.groupElements,
permissionLevel: window.myProject.forReact.permissionLevel
};
export default utils;
Then importing utils in other components and using the functions/reading the values. And of course making those objects available on the main page.
But really, this feels wrong.
My ideal solution would look a lot like the Vue.js way:
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
Because I can just copy paste this bit, and change the Hello Vue!, and all the sudden the starting state is different in my "second copy" of it. Or even more cleverly, wrap it in a function that gets some initialState and put that JavaScript part in a file which I reference.
I have considered
I have considered editing the compiled JavaScript code that is being referenced. So far I only saw that the actual hooking into the myReactAppRoot-element is being done in the main.chunck.js, and editing that file seems too much like a hack (feels more wrong than my current solution).
To expand on Mike's comment, let's first translate your Vue app into React:
//html
<div id="root"></div>
//JS
const App = ({message}) => <div>{message}</div>
const rootElement = document.getElementById("root");
ReactDOM.render(
<App message={"Hello, React!"}/>
rootElement
);
A function that mounts an instance of App on a DOM node with id domId would be something like
function mountAppWithMessageOnNode(message, domId){
const rootElement = document.getElementById(domId);
ReactDOM.render(
<App message={message}/>
rootElement
);
}

how to emulate messages/events with react useState and useContext?

I'm creating a react app with useState and useContext for state management. So far this worked like a charm, but now I've come across a feature that needs something like an event:
Let's say there is a ContentPage which renders a lot of content pieces. The user can scroll through this and read the content.
And there's also a BookmarkPage. Clicking on a bookmark opens the ContentPage and scrolls to the corresponding piece of content.
This scrolling to content is a one-time action. Ideally, I would like to have an event listener in my ContentPage that consumes ScrollTo(item) events. But react pretty much prevents all use of events. DOM events can't be caught in the virtual dom and it's not possible to create custom synthetic events.
Also, the command "open up content piece XYZ" can come from many parts in the component tree (the example doesn't completely fit what I'm trying to implement). An event that just bubbles up the tree wouldn't solve the problem.
So I guess the react way is to somehow represent this event with the app state?
I have a workaround solution but it's hacky and has a problem (which is why I'm posting this question):
export interface MessageQueue{
messages: number[],
push:(num: number)=>void,
pop:()=>number
}
const defaultMessageQueue{
messages:[],
push: (num:number) => {throw new Error("don't use default");},
pop: () => {throw new Error("don't use default");}
}
export const MessageQueueContext = React.createContext<MessageQueue>(defaultMessageQueue);
In the component I'm providing this with:
const [messages, setmessages] = useState<number[]>([]);
//...
<MessageQueueContext.Provider value={{
messages: messages,
push:(num:number)=>{
setmessages([...messages, num]);
},
pop:()=>{
if(messages.length==0)return;
const message = messages[-1];
setmessages([...messages.slice(0, -1)]);
return message;
}
}}>
Now any component that needs to send or receive messages can use the Context.
Pushing a message works as expected. The Context changes and all components that use it re-render.
But popping a message also changes the context and also causes a re-render. This second re-render is wasted since there is no reason to do it.
Is there a clean way to implement actions/messages/events in a codebase that does state management with useState and useContext?
Since you're using routing in Ionic's router (React-Router), and you navigate between two pages, you can use the URL to pass params to the page:
Define the route to have an optional path param. Something like content-page/:section?
In the ContentPage, get the param (section) using React Router's useParams. Create a useEffect with section as the only changing dependency only. On first render (or if section changes) the scroll code would be called.
const { section } = useParams();
useEffect(() => {
// the code to jump to the section
}, [section]);
I am not sure why can't you use document.dispatchEvent(new CustomEvent()) with an associated eventListener.
Also if it's a matter of scrolling you can scrollIntoView using refs

Injecting a node into an external Vue component

I am a junior developer who lacks experience, so I apologize if my question showcases signs of sheer ignorance. My title may not be expressive of the problem I face, so I shall do my best to be descriptive.
In my project, I am making use of a 3rd party component (a dropdown menu), which I would like to modify in my application. I don't want to fork and edit their code since I would like to pull the latest styling changes since my modification is only slight, being that I would like to add some text next to the dropdown icon.
Here is a (simplified) version of the code.
<template>
<overflow-menu
ref="overflow_menu"
>
<overflow-menu-item
v-for="item in overflowMenuItems"
:id="item.id"
:key="item.name"
>
{{ item.tabName }}
</overflow-menu-item>
</overflow-menu>
</template>
<script>
import { OverflowMenu, OverflowMenuItem } from '#some-library/vue'; //Don't have control of the implementation of these components.
export default {
name: 'CustomOverflowMenu',
components: {
OverflowMenu,
OverflowMenuItem,
},
props: {
overflowMenuItems: Array,
label: String,
},
mounted() {
this.injectOverflowMenuLabel();
},
methods: {
injectOverflowMenuLabel() {
const overflowMenuElement = this.$refs.overflow_menu.$el.firstChild;
const span = document.createElement("span");
const node = document.createTextNode(this.$props.label);
span.appendChild(node);
overflowMenuElement.insertBefore(
span,
overflowMenuElement.firstChild,
);
}
}
};
</script>
It functionally works ok, however, it doesn't seem a particularly elegant solution, and I feel as if I could be doing it in a more "Vuey" way. I also am greeted with a Vue warning of: Error in created hook: "TypeError: Cannot read property 'insertBefore' of undefined. This ultimately means I am not able to mount my component and unit test my custom overflow menu.
Is there a way to get this functionality, but in a more maintainable manner. I would either like to simplify the logic of the injectOverflowMenuLabel function, or perhaps there is a completely alternative approach that I haven't considered.
Would appreciate any help, you lovely people.
Thanks,
Will.

MobX observable boolean does not re-render component, but observable number does

I'm getting started with a new create-react-app application using TypeScript, hooks, and mobx-react-lite. Despite having used MobX extensively in a React Native app in the past, I've run into an issue that doesn't make any sense to me.
I have a store with two observables: one number and one boolean. There is an initialize() method that runs some library code, and in the success callback, it sets the number and the boolean to different values (see Line A and Line B below).
The issue: my component ONLY re-renders itself when Line A is present. In that case, after the initialization is complete, the 'ready' text appears, and the button appears. If I delete Line B, the 'ready' text still appears. But if I delete Line A (and keep Line B), the button never renders. I've checked things over a hundred times, everything is imported correctly, I have decorator support turned on. I can't imagine why observing a number can trigger a re-render but observing a boolean cannot. I'm afraid I'm missing something horribly obvious here. Any ideas?
The relevant, simplified code is as follows:
// store/app.store.ts
export class AppStore {
#observable ready = false
#observable x = 5
initialize() {
// Takes a callback
ThirdPartyService.init(() => {
this.ready = true
this.x = 10
})
}
}
// context/stores.ts
const appStore = new AppStore()
const storesContext = React.createContext({
appStore
})
export const useStores = () => React.useContext(storesContext)
// App.tsx
const App = observer(() => {
const { appStore } = useStores()
useEffect(() => {
appStore.initialize()
}, [appStore])
return (
<div>
{ appStore.x === 10 && 'ready' } // <-- Line A
{ appStore.ready && <button>Go</button> } // <-- Line B
</div>
)
}
EDIT: A bit more information. I've added some logging statements to just before the return statement for the App component. I also refactored the button conditional to a const. This may provide more insight:
const button = appStore.ready ? <button>Go</button> : null
console.log('render', appStore.ready)
console.log('button', button)
return (
<div className="App">
<header className="App-header">{button}</header>
</div>
)
When appStore.ready is updated, the component does re-render, but the DOM isn't updated. The console shows 'render' true and shows a representation of the button, as it should, but inspecting the document itself shows no button there. Somehow, though, changing the condition from appStore.ready to appStore.x === 10 does update the DOM.
Turns out I didn't quite give complete information in my question. While I was creating a minimal reproduction, I decided to try dropping the top-level <React.StrictMode> component from index.tsx. Suddenly, everything worked. As it happens, mobx-react-lite#1.5.2, the most up-to-date stable release at the time of my project's creation, does not play nice with Strict Mode. Until it's added to a stable release, the two options are:
Remove strict mode from the React component tree
Use mobx-react-lite#next

Categories