Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async Component - Resolve Component + Child Page Property #15035

Closed
tada5hi opened this issue Sep 27, 2022 · 11 comments
Closed

Async Component - Resolve Component + Child Page Property #15035

tada5hi opened this issue Sep 27, 2022 · 11 comments

Comments

@tada5hi
Copy link
Contributor

tada5hi commented Sep 27, 2022

Environment


  • Operating System: Windows_NT
  • Node Version: v16.17.0
  • Nuxt Version: 3.0.0-rc.11
  • Nitro Version: 0.5.4
  • Package Manager: npm@8.4.1
  • Builder: vite
  • User Config: css, alias, vite, runtimeConfig, modules
  • Runtime Modules: @nuxtjs/google-fonts@^3.0.0-0, @pinia/nuxt@^0.4.2
  • Build Modules: -

Reproduction

[id].ts

import { defineNuxtComponent, navigateTo, useRoute } from '#app';
import { resolveComponent } from '#imports';

export default defineNuxtComponent({
    async setup() {
        const entity = await /** some async api call **/
        const nuxtPage = resolveComponent('NuxtPage');

        return () => h('div', [
            h('div', [
                h(nuxtPage, {
                    entity,
                }),
            ]),
        ]);
    },
});
;

[id]/index.ts

import { defineNuxtComponent, navigateTo, useRoute } from '#app';
import { resolveComponent } from '#imports';

export default defineComponent({
    props: {
        entity: {
            type: Object,
            required: true,
        },
    },
    setup(props) {
        console.log(props.entity); 
        /** component is renedered two times. First time with null value and second time with the actual value **/
   })
});

Describe the bug

There are actually two problems.

  1. Resolve Component:
    The resolveComponent method is not working in an async setup function, but in a normal one it is. So it is not possible to resolve the NuxtPage Component.
    The error code in the console is the following one:
runtime-core.esm-bundler.js:38 [Vue warn]: resolveComponent can only be used in render() or setup().
  1. Child Page Property
    When i created an extra Component which is a wrapper for the NuxtPage component, which is just passing the $attr object to the NuxtPage component, i got around the first problem, because i am now able to import the wrapper component directly via an import statement.
    The child page component is rendered two times and the first time with null property values and the second time with the actual value.
    In the reproduction section it is for example the entitiy property.

Additional context

No response

Logs

No response

Copy link
Member

The issue here is that Vue (and Nuxt) need synchronous access to the context. If you have an async setup, then you need to ensure that any functions that need access to the context - such as resolveComponent - should be before your first await.

Normally, Vue (and Nuxt) perform some transforms to allow you to access the context after await. However, for a setup function, this only works within <script setup> blocks - see vue documentation.

However, there's good news in your case - resolveComponent can also be called within your render function.

Here's some code that hopefully explains the issue:

import { defineComponent, resolveComponent } from '#imports';

export default defineComponent({
  async setup() {
    await Promise.resolve();
    // this will fail (and print a warning) as there is no accessible context
    resolveComponent('NuxtPage');

    return () => {
      // this will succeed as render functions are synchronous
      // and it is specifically designed to be called in a render function
      const nuxtPage = resolveComponent('NuxtPage');

      return h('div', [h(nuxtPage)]);
    };
  },
});

@danielroe danielroe closed this as not planned Won't fix, can't repro, duplicate, stale Sep 27, 2022
@tada5hi
Copy link
Contributor Author

tada5hi commented Sep 27, 2022

You are the man @danielroe 👌 . This solved my first problem, but the second one is sadly still present.

@danielroe
Copy link
Member

I can't reproduce your second issue: https://stackblitz.com/edit/github-vga5kg-vv1onx.

I'll happily have a look if you can provide a reproduction.

@tada5hi
Copy link
Contributor Author

tada5hi commented Sep 27, 2022

Yeah, i'm also trying to reproduce the issue on stackblitz, but sadly it just occours on the frontend package of my monorepo.
I am working at the moment on a authentication & authorization application, which is split in different standalone packages. Recently i refactored the vue2 library to vue3 and i am now creating the frontend application.
If you do not mind installing the application, i can give you introductions on how to reproduce the issue in this context.

And thank you very much again for your efforts and your time ☺️ .

Copy link
Member

Ideally I would like a minimal reproduction, even if it's not a stackblitz.

@tada5hi
Copy link
Contributor Author

tada5hi commented Sep 27, 2022

Mhhhh, maybe you can see the problem from the screenshots and the
code of the parent and child component:

The funny thing is, the error only occurs when i route to the page. If i load it via direct url call or F5, the error don't occur.

[id].ts

import { PermissionID, User } from '@authelion/common';
import { useToast } from 'vue-toastification';
import { NuxtLink } from '#components';
import { defineNuxtComponent, navigateTo, useRoute } from '#app';
import {
    definePageMeta, resolveComponent, useAPI,
} from '#imports';
import { LayoutKey, LayoutNavigationID } from '~/config/layout';

export default defineNuxtComponent({
    async setup() {
        definePageMeta({
            [LayoutKey.NAVIGATION_ID]: LayoutNavigationID.ADMIN,
            [LayoutKey.REQUIRED_LOGGED_IN]: true,
            [LayoutKey.REQUIRED_PERMISSIONS]: [
                PermissionID.USER_EDIT,
                PermissionID.USER_ROLE_ADD,
                PermissionID.USER_ROLE_EDIT,
                PermissionID.USER_ROLE_DROP,
            ],
        });

        const items = [
            {
                name: 'General', icon: 'fas fa-bars', urlSuffix: '',
            },
            {
                name: 'Permissions', icon: 'fas fa-user-secret', urlSuffix: 'permissions',
            },
            {
                name: 'Roles', icon: 'fa-solid fa-user-group', urlSuffix: 'roles',
            },
        ];

        const toast = useToast();

        const handleUpdated = () => {
            toast.success('The user was successfully updated.');
        };

        const handleFailed = (e) => {
            toast.warning(e.message);
        };

        const nuxtPage = resolveComponent('NuxtPage');

        const route = useRoute();

        let entity : User;

        console.log(route.params.id);

        try {
            entity = await useAPI()
                .user
                .getOne(route.params.id as string, { fields: ['+email'] });
        } catch (e) {
            return navigateTo({ path: '/admin/roles' });
        }

        console.log(entity);

        return () => h('div', [
            h('h1', { class: 'title no-border mb-3' }, [
                h('i', { class: 'fa fa-user me-1' }),
                entity.name,
                h('span', { class: 'sub-title ms-1' }, [
                    'Details',
                ]),
            ]),
            h('div', { class: 'mb-2' }, [
                h(
                    'ul',
                    { class: 'nav nav-pills' },
                    items.map((item) => h('li', { class: 'nav-item' }, [
                        h(
                            NuxtLink,
                            {
                                class: 'nav-link',
                                to: `/admin/users/${entity.id}/${item.urlSuffix}`,
                            },
                            {
                                default: () => [
                                    h('i', { class: `${item.icon} pe-1` }),
                                    item.name,
                                ],
                            },
                        ),
                    ])),
                ),
            ]),

            h('div', [
                h(nuxtPage, {
                    onUpdated: handleUpdated,
                    onFailed: handleFailed,
                    entity,
                }),
            ]),

        ]);
    },
});

[id]/index.ts

import { User } from '@authelion/common';
import { PropType } from 'vue';
import {
    defineNuxtComponent, definePageMeta, resolveComponent,
} from '#imports';
import { LayoutKey } from '~/config/layout';

export default defineNuxtComponent({
    props: {
        entity: {
            type: Object as PropType<User>,
            required: true,
        },
    },
    emits: ['updated', 'failed'],
    async setup(props, { emit }) {
        definePageMeta({
            [LayoutKey.REQUIRED_LOGGED_IN]: true,
        });

        const handleUpdated = (e) => {
            emit('updated', e);
        };

        const handleFailed = (e) => {
            emit('failed', e);
        };

        const form = resolveComponent('UserForm');
        const passwordForm = resolveComponent('UserPasswordForm');

        return () => h('div', { class: 'row' }, [
            h('div', { class: 'col-7' }, [
                h('h6', { class: 'title' }, ['General']),
                h(form, {
                    entity: props.entity,
                    realmId: props.entity.realm_id,
                    onUpdated: handleUpdated,
                    onFailed: handleFailed,
                }),
            ]),
            h('div', { class: 'col-5' }, [
                h('h6', { class: 'title' }, ['Password']),
                h(passwordForm, {
                    id: props.entity.id,
                    onUpdated: handleUpdated,
                    onFailed: handleFailed,
                }),
            ]),

        ]);
    },
});

screen1
screen2

For some reason, the child component is already rendered before, the parent async call in the parent component is terminated, and the passed entity property is therefore not avaialable and is not passed to the child component.

Copy link
Member

Ah, likely that is because when you switch routes there's a moment when it's not resolved. You should set required: false and confirm that the prop is passed before using it. You can also follow a potentially related issue here: #14573.

@tada5hi
Copy link
Contributor Author

tada5hi commented Sep 27, 2022

Yeah i already worked around that, before like that, but that felt so wrong... and it was kinda annoying to repeat that in each child page.

// ...
setup(props) {
        // todo: remove this when this is fixed in vue/nuxt
        if (!props.entity) {
            return () => h('div', []);
        }

       // ......
},
// ....

Thank you for the reference ☺️ ✌️

PS: Non related question: Do you know how disable the notice for definePageMeta in the setup routine? I did not found another way to pass meta tags for the vue router.

@danielroe danielroe added the 3.x label Jan 19, 2023
@danielroe danielroe transferred this issue from nuxt/framework Jan 19, 2023
@tada5hi
Copy link
Contributor Author

tada5hi commented Apr 15, 2023

@danielroe any news on that ?

@tada5hi
Copy link
Contributor Author

tada5hi commented Apr 15, 2023

It would be really great if a required prop by a child page is not present, that the component is not rendered at all, instead of being rendered two times, one time undefined and one time with the actual value 🙁

@tada5hi
Copy link
Contributor Author

tada5hi commented Apr 16, 2023

solved by v3.4.1 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants