I am creating dynamic pages using Nuxt. In the pages folder I have one file _url.vue. It contains the following code:
<template lang="pug">
div
component(
v-for="component in components"
:key="`${component.type}-${component.id}`"
:is="`the-${component.type}`"
)
</template>
<script>
// import vuex
export default {
computed: {
...mapGetters('app', {
components: 'getComponents'
})
}
}
</script>
setComponents happens at the middleware level:
export default async function ({ store }) {
await store.dispatch('app/setPage')
}
In the first milliseconds of page load, the content "jumps" as the components are rendered on the fly. How can this situation be corrected?
I'd first try to import the components manually, to see where this all comes from: the components taking some time to get injected or the layout being displayed, just to be sure.
Then, I had a discussion about it here, you may give it a look: Vue: wait to render until all components are mounted
There are several ways of handling this kind of micro-jumping explained there. You can choose your own solution. Also depends if you're using your app as universal or SPA only.
Looks like require is a way to go but some alternative are also available.
Related
A common use case I run across is having a button in a layout toggle something in the children. For example, you could have a notifications button in a persisted layout that opens a side pane in the main app.
Ideally you would be able to pass a isNotificationsPaneOpen state down to the children and have them render the pane. However,the Next 13 beta docs say this isn't possible:
Passing data between a parent layout and its children is not possible. However, you can fetch the same data in a route more than once, and React will automatically dedupe the requests without affecting performance.
Persisting this toggle state in the backend seems like a major overkill and feels unnatural. The backend shouldn't have to know about the frontend opening or closing a notifications panel.
It seems to me that this is a common enough use case that people will run into this issue often.
How should one generally think about this?
Concrete example:
// in layout.tsx
export default function NavbarLayout({
children,
}: {
children: React.ReactNode,
}) {
const [isNotificationsPaneOpen, setIsNotificationsPaneOpen]= useState(false);
return (
<div>
<nav>
<button onClick={()=>setIsNotificationsPanelOpen((prevValue)=>!prevValue)}>Notifications</button>
</nav>
{/* How do I pass isNotificationsPaneOpen to children? */}
{children}
</div>
);
}
So that in the page:
// in page.tsx
interface PageProps {
isNotificationsPaneOpen: boolean;
}
function Page({isNotificationsPaneOpen}:PageProps):JSX.Element {
// render something conditionally with isNotificationsPaneOpen
...
}
Current solutions
Include the pane to toggle in the layout (best candidate IMO)
If the component that depends on the layout's state remains the same across pages (like the notifications panel example), it makes sense to include it here. But other examples - like toggling light/dark mode in the layout - render different things on each page so this is not a general solution.
External storage like localstorage to pass data
More of a hack but you could have the layout send data to an external data source (ideally within the browser) and then query that data source from the main app to determine if the panel should be open or not.
I think your question is the result of a misconception of what a Layout is for in Next.js version 13, in the app directory:
A layout is UI that is shared between multiple pages. On navigation, layouts preserve state, remain interactive, and do not re-render. Layouts can also be nested.
It wraps a route segment, like "/about", which means surround the page.js, loading.js, and all the components that make "/about".
The root Layout, located in /app/layout.js, wraps all the others, so there you would put anything that's common for all paths. Each individual path can have its own Layout, that contains shared specific elements for this path.
So, if your component is not common to all routes, add it in the Layout of the specific segment where it's needed, like in /app/blog/layout.js, or in /app/blog/page.js if you wanna be even more specific.
A Layout should not be considered as a global state provider. For this need consider using already known technics, for example a context:
// app/theme-provider.js
'use client';
import { createContext } from 'react';
const ThemeContext = createContext();
export default function ThemeProvider({ children }) {
return (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
);
}
// app/layout.js
import ThemeProvider from './theme-provider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
I have a nav bar that loads user data, all of this happens after a user successfully logs into the application. The problem is, localStorage must be setting slightly after I load the nav bar. If I wrap it in a setTimeout() everything works but I would rather my variables be reactive in nature since they can change based on user activity.
Toolbar.vue
<template>
<!--begin::Toolbar wrapper-->
<div class="d-flex align-items-stretch flex-shrink-0">
<h2>check for value</h2>
<div v-if="activeAccountId">{{activeAccountId}}</div>
</div>
<!--end::Toolbar wrapper-->
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "topbar",
data() {
let activeAccountId = ref(JSON.parse(localStorage.getItem('activeAccountId') || '{}')).value;
return {
activeAccountId
}
}
});
</script>
I've tried using watchers, and using setup() verses data(), but nothing seems to work properly. As I mentioned, setTimeout() does work but I'd rather avoid manually triggering a timeout and let vue handle things how it wants to.
Here's a simple example, I can't setup a dummy code side since it won't have the localStorage item set.
For some additional context, after the user is logged in, I am hitting the API with an async() to get the account information and storing the account data in localStorage. I'm guessing at the same time the router is trying to load the navbar area which is why the localStorage items aren't available when the component mounts.
I don't know the vue3 words to use, but ideally I would want some type of async/await call to localStorage because the ref() doesns't seem to be working how I thought it would. It's as if the ref() doesn't see localStorage get updated.
localStorage being synchronous is the main issue.
use mounted lifecycle hook. and initialize user information there
Vue calls the mounted() hook when the component is added to the DOM. You can try putting your initial code in the mounted method and also try to change your code like this
export default defineComponent({
name: "topbar",
data() {
return {
activeAccountId:""
}
},
mounted(){
this.activeAccountId = JSON.parse(localStorage.getItem('activeAccountId')|| '{}');
}
});
Here is my base App.vue that holds the router-view:
<template>
<div>
<Navbar/>
<div class="container-fluid">
<router-view></router-view>
</div>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue';
export default {
name: 'App',
components: {
Navbar
}
}
</script>
In the Navbar.vue, I have a method:
methods: {
getLoggedStatus(){
console.log('asdasd')
}
}
Lastly, I have a Login.vue that is loaded there on the router-view. I wanna acess the getLoggedStatus() from the Navbar.vue within Login.vue. How can I achieve this?
I tried putting a ref to the Navbar tag in App.vue:
<Navbar ref="navvy"/>
And calling in on the Login.vue with:
this.$refs.navvy.getLoggedStatus()
But it doesn't work.
A ref only works on children. You're rendering <Navbar> within app, so you cannot call that ref from login. You can only access this.$refs.navvy from App.vue.
There are several solutions for your problem.
Emit an event from Login to App, so App calls the method from a ref.
You can set a listener in the router-view, as:
<router-view #loggedStatus="callLoggedStatus" />
In your login, when you would want to call the navbar getLoggedStatus, you would instead emit that event:
this.$emit('loggedStatus')
And then in App.vue, you would defined a callLoggedStatus methods that call the ref:
callLoggedStatus() {
this.$refs.navvy.getLoggedStatus();
}
Given that you add the ref to the <Navbar> component in the APP template.
This solution is arguably the most similar to your proposed code, but I think it is a mess and you should avoid it, since you can end up listening to a lot of different events in your App.vue.
Use Vuex
I don't exactly know what getLoggedStatus does, but if you want to change how your navbar behaves when the user is logged in, you should probably setup a vuex store, so you register there wether the user is logged or not. Then in your navbar component you render things conditionally depending upon the user is logged or not.
#Lana's answer follows this idea, and is probably the closest to the official way to thins in Vue.
Use an event emitter
If you want to directly communicate between components that are not in the same family, I think an event emitter is a reasonable choice. You could setup an application wide event emitter after creating the app:
const app = new Vue({...});
window.emitter = new Vue();
(in the example we use a new Vue as event emitter. There is also the 'events' module which allow to use a EventEmitter)
And then any component can use it to send messages, so Login could do:
window.emitter.$emit('user-logged', myCustomPayload);
And Navbar on the other hand could do:
window.emitter.$on('user-logged', function() {
this.getLoggedStatus();
})
This last option is not well considered in the Vue community -Vuex is preferred- but for small applications I think it is the simplest.
The dirty hack
You can always export your component to window. In the Navbar created hook you could do:
created() {
window.Navbar = this;
}
And then in Login.vue you could, at any time:
window.Navbar.getLoggedStatus()
And it will work. However, this is surely an anti pattern and can cause a lot of harm to your project maintainability if you start doing this with several components.
It looks like getLoggedStatus returns some global state of the app. Use Vuex for managing these global variables.
Create a store like this:
// Make sure to call Vue.use(Vuex) first if using a module system
const store = new Vuex.Store({
state: {
loggedStatus: 0
},
getters: {
getLoggedStatus: state => {
// to compute derived state based on store state
return state.loggedStatus
}
}
})
Use store.state.loggedStatus in any Vue-component to access the global state or use getters like store.getters.getLoggedStatus if you need to compute derived state based on store state.
I'm in the process of learning VUE JS. I have a very basic SPA that routes between various pages.
I have a number of THREE JS demos that I've built in my spare time and I've noticed that if I jump between pages on them eventually they grind to a halt from a build up of memory. I want to avoid pasting their scripts in here as they're massive and I don't think the problem lies within them.
I think the problem the lies somewhere in between how I'm instantiating them and my lack of knowledge in VUE JS is what's causing me trouble with this problem.
Here is an example of one of the views I'm routing to in VUE JS:
<template>
<div class="particles">
<main-menu></main-menu>
<div id="demo"></div>
</div>
</template>
<script>
import mainMenu from 'root/views/menu.vue';
export default {
components: { mainMenu },
mounted() {
var Particles = require('root/webgl/particles.js');
var demo = new Particles();
demo.run();
}
}
</script>
The original demos were built using traditional JavaScript (it's a ES5/6 class) and I was hoping that I could just plug them into my VUE SPA. Within each demo I am doing things like:
this.vsParticles = document.getElementById('vs-particles').textContent;
to load the shaders and to attach my THREE demo to a DOM element.
The issue I'm having is that somewhere something isn't being deleted. Within my demos i'm not creating anything new in the DOM and they run fine in non-SPA demos, but once I place them into a SPA app and jump between pages they build up.
I'm under the impression that when you change routes everything should get wiped. So all of the elements within my template tags and any objects I create within mounted(). But this doesn't seem to be the case and I'm just wondering is there something extra I need to include in VUE to completely clean everything up inbetween pages?
As Mathieu D. mentionned, you should move your require outside of the method.
Also, you may need to clear the WebGL context on the destroyed () Vue Component's hook.
I am not sure if it is the right way to do, but here is how I dispose it in my program :
this.renderer.forceContextLoss()
this.renderer.context = null
this.renderer.domElement = null
this.renderer = null
Could you try to import particles.js out of mounted method ?
For example, under your import of mainMenu ?
The import would be done one for all of your instanciation of Particles.
This will give you this code :
import mainMenu from 'root/views/menu.vue';
import Particles from 'root/webgl/particles.js';
export default {
components: { mainMenu },
mounted() {
var demo = new Particles();
demo.run();
}
}
You could also read reactivity in depth in the documentation. It will help you understand how VueJS store and access data. I don't know if in your component code you store some data of your ThreeJS examples, but if so, it could consume some memory. In that case, use destroyed hook to clean your data.
My case is that I have a static component on the desktop and it must become carousel on mobile.
The component is rendered server side because of seo and I use is="my-component" to trigger vue on it. Typically when I duplicate the markup and check in created() the breakpoint, I can trigger some carousel constructor. However, if a breakpoint is set to desktop, vue will still rerender component which is redundant.
I know that one case may not be that effective, but I have a lot of performance and parsing problems because of vue in my previous project, so I need to keep performance in mind from the beginning.
Is it possible to somehow prevent rendering on beforeCreate() hook, but still be able to use it in some conditional?
As I have read your comment, and you would like to use something else that is not v-if, I can think only in two ways of doing it.
1) If you are using vue-router you can run make use of Lazy Loading Routes which is basically a function that can return an import('component') (which is a promise).
MobileCarousel.ts
import { isMobile } from '#/utils/mediaQuery';
const MobileCarousel = (): Promise<Vue> | void => {
if (!isMobile()) {
return;
}
return import('#/components/MobileCarousel/MobileCarousel.vue');
};
export default MobileCarousel;
Routes.ts
import MobileCarousel from '#/components/MobileCarousel/MobileCarousel.ts';
...
{
path: 'route-that-has-a-mobile-only-carousel',
name: 'mobile-only-carousel',
component: MobileCarousel,
},
enter code here
My only concern with this approach is related to the server-side rendering. As I never have played with server side-rendering with Vue I cannot assure you that it will work as you expect, you can give a try. Hope it helps you.
2) Apart from using Lazy Loading Routes, I believe that a Vue component with a render function that returns only if it is mobile also can be useful for your case.