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

Provide a way to reload a failed async component #9788

Closed
Maorey opened this issue Mar 29, 2019 · 6 comments
Closed

Provide a way to reload a failed async component #9788

Maorey opened this issue Mar 29, 2019 · 6 comments

Comments

@Maorey
Copy link

Maorey commented Mar 29, 2019

What problem does this feature solve?

Local register a async component by a factory function that return an object to handling loading state. But when the promise is rejected, there will be always show error component and no any way to reload it.

What does the proposed API look like?

  1. Recall the factory function on parent's next active/update hook when rejected
  2. [Recommend]Accept more options to handle loading state such as provide an 'on' to
    listen replaced component's(loading/error) events and the event context allows to change loading state(rerend, of course)
@posva
Copy link
Member

posva commented Mar 29, 2019

I personally always felt the factory function was too high level for Vue. When using a user-side solution like vue-promised or a custom implementation, it's so much more flexible and you can deal with everything

@bogdanmatra
Copy link

bogdanmatra commented Jul 31, 2019

This can help to solve the problem, it's hacky but it works.
It wraps the async component in a functional component which recreates the async factory based on shouldReload. Calculating shouldReload might differ in your case.

const loadAsyncComponentWrapper = async () => {
    const createAsyncFactory = () => () => ({
        component: import('/your-component.vue'),
        loading: AppLoader,
        error: AppError,
        timeout: 15000
    });
    let componentHandler = createAsyncFactory();
    return {
        functional: true,
        render(h, { data, children }) {
            const shouldReload = navigator.onLine;  // Some criteria for reloading.
            if (shouldReload) {
                // The Vue component should be recreated completely if the component failed to load.
                // Issue: https://github.com/vuejs/vue/issues/9788
                componentHandler = createAsyncFactory();
            }
            return h(componentHandler, data, children);
        }
    };
};

@Maorey
Copy link
Author

Maorey commented Aug 12, 2019

@bogdanmatra I tried your solution, it's useless when async, and dead when sync (same code above). I modified your code to this:

// import...

/** get AsyncComponent with loading status
 * @param {Function: () => Promise<Component | { default: Component }>} promiseFactory eg: () => import('Component')
 *
 * @returns {Component} AsyncComponent
 */
function getAsync(promiseFactory, loading = LOADING, error = ERROR) {
  const asyncComponentFactory = () => () => ({
    error,
    loading,
    component: promiseFactory(),

    delay: 1,
    timeout: 15000,
  })

  const observe = Vue.observable({ c: asyncComponentFactory() })

  return {
    functional: true,
    render(createElement, { data, children }) {
      //  emit event $ to reloading
      if (data.on ? !data.on.$ : (data.on = {})) {
        data.on.$ = () => {
          observe.c = asyncComponentFactory()
          // parent.$forceUpdate()
        }
      }

      return createElement(observe.c, data, children)
    },
  }
}

And error.vue

<template>
  <div @click="$emit('$')">Loading failed, click me to reloading</div>
</template>

@RyanClementsHax
Copy link

@bogdanmatra I tried your solution, it's useless when async, and dead when sync (same code above). I modified your code to this:

// import...

/** get AsyncComponent with loading status
 * @param {Function: () => Promise<Component | { default: Component }>} promiseFactory eg: () => import('Component')
 *
 * @returns {Component} AsyncComponent
 */
function getAsync(promiseFactory, loading = LOADING, error = ERROR) {
  const asyncComponentFactory = () => () => ({
    error,
    loading,
    component: promiseFactory(),

    delay: 1,
    timeout: 15000,
  })

  const observe = Vue.observable({ c: asyncComponentFactory() })

  return {
    functional: true,
    render(createElement, { data, children }) {
      //  emit event $ to reloading
      if (data.on ? !data.on.$ : (data.on = {})) {
        data.on.$ = () => {
          observe.c = asyncComponentFactory()
          // parent.$forceUpdate()
        }
      }

      return createElement(observe.c, data, children)
    },
  }
}

And error.vue

<template>
  <div @click="$emit('$')">Loading failed, click me to reloading</div>
</template>

Good work on this example! I figured I would donate my modification to make it work with @vue/composition-api and typescript. Comments welcome!

// would import these directly from vue if not using the @vue/composition-api plugin
import {
  defineComponent,
  ref,
  defineAsyncComponent,
  h
} from '@vue/composition-api'

import AsyncError from './async-error.vue' // default error component
import AsyncLoading from './async-loading.vue' // default loading component

// this is a hack to get the type of the first parameter since it isn't exported from @vue/composition-api
export type AsyncComponentSource = Parameters<typeof defineAsyncComponent>[0]

export const withReload = (
  source: AsyncComponentSource,
  initialProps: Record<string, unknown> = {
    loadingMessage: 'Setting some things up for you...'
  }
) =>
  defineComponent({
    setup(props, ctx) {
      const asyncComponentFactory = () =>
        defineAsyncComponent({
          loadingComponent: AsyncLoading,
          errorComponent: AsyncError,
          ...('loader' in source ? source : { loader: source })
          // uncomment this if you want to test the $reload function by artifically adding latency to the resolution of the module
          // loader: () =>
          //   new Promise(resolve => setTimeout(resolve, 3000)).then(
          //     () => import('./async-error.vue')
          //   )
        })

      const asyncComponent = ref(asyncComponentFactory())
      return () =>
        // I'm not sure how to best pass through the props, data, and children, but this is my best guess given the current state of documentation
        h(asyncComponent.value, {
          ...ctx,
          props: {
            ...(props as Record<string, unknown>),
            ...initialProps
          },
          on: {
            ...ctx.listeners,
            $reload() {
              asyncComponent.value = asyncComponentFactory()
            }
          }
        })
    }
  })

Here is how you would call it

withReload(() => import('./path/to/component.vue')
// or
withReload({
  loader: () => import('./path/to/component.vue'),
  loadingComponent: SomeOtherLoadingComponent,
  errorComponent: SomeOtherErrorComponent,
  delay: 400 // other options supported too
})
<template>
  <div @click="$emit('$reload')">Loading failed, click me to reloading</div>
</template>

@posva
Copy link
Member

posva commented Aug 13, 2021

Closing as this should go through the RFC process in the rfcs repository

@posva posva closed this as completed Aug 13, 2021
@hieuemmm
Copy link

hieuemmm commented Jun 9, 2022

let array = this.$store.state.Admin.BookManager.CKEditor.Book;
this.$store.state.Admin.BookManager.CKEditor.Book = [];
setTimeout(
  function () {
    this.$store.state.Admin.BookManager.CKEditor.Book = array;
  }.bind(this),
  0
);

Woking with me.

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

No branches or pull requests

7 participants
@posva @bogdanmatra @Maorey @RyanClementsHax @hieuemmm and others