From 00c9a9831f2ecdfec2508e44d55c3b9c94b50767 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 25 Mar 2020 11:41:52 -0400 Subject: [PATCH 1/5] async component --- active-rfcs/0000-async-component-api.md | 118 ++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 active-rfcs/0000-async-component-api.md diff --git a/active-rfcs/0000-async-component-api.md b/active-rfcs/0000-async-component-api.md new file mode 100644 index 00000000..510058c5 --- /dev/null +++ b/active-rfcs/0000-async-component-api.md @@ -0,0 +1,118 @@ +- Start Date: 2020-03-25 +- Target Major Version: 3.x +- Reference Issues: https://github.com/vuejs/rfcs/pull/28 + +# Summary + +Introduce a dedicated API for defining async components. + +# Basic example + +```js +import { createAsyncComponent } from "vue" + +// simple usage +const AsyncFoo = createAsyncComponent(() => import("./Foo.vue")) + +// with options +const AsyncFooWithOptions = createAsyncComponent({ + loader: () => import("./Foo.vue"), + loading: LoadingComponent, + error: ErrorComponent, + delay: 200, + timeout: 3000 +}) +``` + +# Motivation + +Per changes introduced in [RFC-0008: Render Function API Change](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0008-render-function-api-change.md), in Vue 3 plain functions are now treated as functional components. Async components must now be explicitly defined via an API method. + +# Detailed design + +## Simple Usage + +```js +import { createAsyncComponent } from "vue" + +// simple usage +const AsyncFoo = createAsyncComponent(() => import("./Foo.vue")) +``` + +`createAsyncComponent` can accept a loader function that returns a Promise resolving to the actual component. + +- If the resolved value is an ES module, the `default` export of the module will automatically be used as the component. + +- **Difference from 2.x:** Note that the loader function no longer receives the `resolve` and `reject` arguments like in 2.x - a Promise must always be returned. + + For code that relies on custom `resolve` and `reject` in the loader function, the conversion is straightforward: + + ```js + // before + const Foo = (resolve, reject) => { + /* ... */ + } + + // after + const Foo = createAsyncComponent(() => new Promise((resolve, reject) => { + /* ... */ + })) + ``` + +## Options Usage + +```js +import { createAsyncComponent } from "vue" + +const AsyncFooWithOptions = createAsyncComponent({ + loader: () => import("./Foo.vue"), + loading: LoadingComponent, + error: ErrorComponent, + delay: 100, // default: 200 + timeout: 3000, // default: Infinity + suspensible: false // default: true +}) +``` + +Options except `loader` and `suspensible` works exactly the same as in 2.x. + +**Difference from 2.x:** + +The `component` option is replaced by the new `loader` option, which accepts the same loader function as in the simple usage. + +In 2.x, an async component with options is defined as + +```ts +() => ({ + component: Promise + // ...other options +}) +``` + +Whereas in 3.x it is now: + +```ts +createAsyncComponent({ + loader: () => Promise + // ...other options +}) +``` + +## Using with Suspense + +Async component in 3.x are *suspensible* by default. This means if it has a `` in the parent chain, it will be treated as an async dependency of that ``. In this case, the loading state will be controlled by the ``, and the component's own `loading`, `error`, `delay` and `timeout` options will be ignored. + +The async component can opt-out of Suspense control and let the component always control its own loading state by specifying `suspensible: false` in its options. + +# Adoption strategy + +- The syntax conversion is mechanical and can be performed via a codemod. The challenge is in determining which plain functions should be considered async components. Some basic heuristics can be used: + + - Arrow functions that returns dynamic `import` call to `.vue` files + - Arrow functions that returns an object with the `component` property being a dynamic `import` call. + + Note this may not cover 100% of the existing usage. + +- In the compat build, it is possible to check the return value of functional components and warn legacy async components usage. This should cover all Promise-based use cases. + +- The only case that cannot be easily detected is 2.x async components using manual `resolve/reject` instead of returning promises. Manual upgrade will be required for such cases but they should be relatively rare. From 7eac42cd9c91e433d92b02162dd3c3e5d003efd4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 26 Mar 2020 11:49:57 -0400 Subject: [PATCH 2/5] createAsyncComponent -> defineAsyncComponent --- active-rfcs/0000-async-component-api.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/active-rfcs/0000-async-component-api.md b/active-rfcs/0000-async-component-api.md index 510058c5..50ae31b1 100644 --- a/active-rfcs/0000-async-component-api.md +++ b/active-rfcs/0000-async-component-api.md @@ -9,13 +9,13 @@ Introduce a dedicated API for defining async components. # Basic example ```js -import { createAsyncComponent } from "vue" +import { defineAsyncComponent } from "vue" // simple usage -const AsyncFoo = createAsyncComponent(() => import("./Foo.vue")) +const AsyncFoo = defineAsyncComponent(() => import("./Foo.vue")) // with options -const AsyncFooWithOptions = createAsyncComponent({ +const AsyncFooWithOptions = defineAsyncComponent({ loader: () => import("./Foo.vue"), loading: LoadingComponent, error: ErrorComponent, @@ -33,13 +33,13 @@ Per changes introduced in [RFC-0008: Render Function API Change](https://github. ## Simple Usage ```js -import { createAsyncComponent } from "vue" +import { defineAsyncComponent } from "vue" // simple usage -const AsyncFoo = createAsyncComponent(() => import("./Foo.vue")) +const AsyncFoo = defineAsyncComponent(() => import("./Foo.vue")) ``` -`createAsyncComponent` can accept a loader function that returns a Promise resolving to the actual component. +`defineAsyncComponent` can accept a loader function that returns a Promise resolving to the actual component. - If the resolved value is an ES module, the `default` export of the module will automatically be used as the component. @@ -54,7 +54,7 @@ const AsyncFoo = createAsyncComponent(() => import("./Foo.vue")) } // after - const Foo = createAsyncComponent(() => new Promise((resolve, reject) => { + const Foo = defineAsyncComponent(() => new Promise((resolve, reject) => { /* ... */ })) ``` @@ -62,9 +62,9 @@ const AsyncFoo = createAsyncComponent(() => import("./Foo.vue")) ## Options Usage ```js -import { createAsyncComponent } from "vue" +import { defineAsyncComponent } from "vue" -const AsyncFooWithOptions = createAsyncComponent({ +const AsyncFooWithOptions = defineAsyncComponent({ loader: () => import("./Foo.vue"), loading: LoadingComponent, error: ErrorComponent, @@ -92,7 +92,7 @@ In 2.x, an async component with options is defined as Whereas in 3.x it is now: ```ts -createAsyncComponent({ +defineAsyncComponent({ loader: () => Promise // ...other options }) From 16c6988369a2c0f83efddaa1d58fb25cd44bcf9e Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 26 Mar 2020 20:46:41 -0400 Subject: [PATCH 3/5] rename loading/error options, update retry usage --- active-rfcs/0000-async-component-api.md | 52 +++++++++++++++---------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/active-rfcs/0000-async-component-api.md b/active-rfcs/0000-async-component-api.md index 50ae31b1..1f956441 100644 --- a/active-rfcs/0000-async-component-api.md +++ b/active-rfcs/0000-async-component-api.md @@ -17,8 +17,8 @@ const AsyncFoo = defineAsyncComponent(() => import("./Foo.vue")) // with options const AsyncFooWithOptions = defineAsyncComponent({ loader: () => import("./Foo.vue"), - loading: LoadingComponent, - error: ErrorComponent, + loadingComponent: LoadingComponent, + errorComponent: ErrorComponent, delay: 200, timeout: 3000 }) @@ -66,37 +66,47 @@ import { defineAsyncComponent } from "vue" const AsyncFooWithOptions = defineAsyncComponent({ loader: () => import("./Foo.vue"), - loading: LoadingComponent, - error: ErrorComponent, + loadingComponent: LoadingComponent, + errorComponent: ErrorComponent, delay: 100, // default: 200 timeout: 3000, // default: Infinity - suspensible: false // default: true + suspensible: false, // default: true + retryWhen: error => error.message.match(/fetch/) // default: () => false + maxRetries: 5 // default: 3 }) ``` -Options except `loader` and `suspensible` works exactly the same as in 2.x. +- The `delay` and `timeout` options work exactly the same as 2.x. **Difference from 2.x:** -The `component` option is replaced by the new `loader` option, which accepts the same loader function as in the simple usage. +- The `component` option is replaced by the new `loader` option, which accepts the same loader function as in the simple usage. -In 2.x, an async component with options is defined as + In 2.x, an async component with options is defined as -```ts -() => ({ - component: Promise - // ...other options -}) -``` + ```ts + () => ({ + component: Promise + // ...other options + }) + ``` -Whereas in 3.x it is now: + Whereas in 3.x it is now: -```ts -defineAsyncComponent({ - loader: () => Promise - // ...other options -}) -``` + ```ts + defineAsyncComponent({ + loader: () => Promise + // ...other options + }) + ``` + +- 2.x `loading` and `error` options are renamed to `loadingComponent` and `errorComponent` respectively to be more explicit. + +## Retry Control + +The new `retryWhen` option expects a function that returns a boolean indicating whether the async component should retry when the loader promise rejects. The function receives the rejection error as the argument so it can conditionally retry only on certain types of errors. + +The `maxRetries` option determines how many retries are allowed (default: `3`). When max times of retries have been attempted, the component will go into error state (render the `errorComponent` if provided) with the last failed error. ## Using with Suspense From 2793672dd1c10db715ef181c87ee5651ec661669 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 7 Apr 2020 14:33:34 -0400 Subject: [PATCH 4/5] update retry control per feedback --- active-rfcs/0000-async-component-api.md | 27 +++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/active-rfcs/0000-async-component-api.md b/active-rfcs/0000-async-component-api.md index 1f956441..27fea787 100644 --- a/active-rfcs/0000-async-component-api.md +++ b/active-rfcs/0000-async-component-api.md @@ -71,8 +71,13 @@ const AsyncFooWithOptions = defineAsyncComponent({ delay: 100, // default: 200 timeout: 3000, // default: Infinity suspensible: false, // default: true - retryWhen: error => error.message.match(/fetch/) // default: () => false - maxRetries: 5 // default: 3 + onError(error, retry, fail, attempts) { + if (error.message.match(/fetch/) && attempts <= 3) { + retry() + } else { + fail() + } + } }) ``` @@ -104,9 +109,23 @@ const AsyncFooWithOptions = defineAsyncComponent({ ## Retry Control -The new `retryWhen` option expects a function that returns a boolean indicating whether the async component should retry when the loader promise rejects. The function receives the rejection error as the argument so it can conditionally retry only on certain types of errors. +The new `onError` option provides a hook to perform customized retry behavior in case of a loader error: + +``` js +const Foo = defineAsyncComponent({ + // ... + onError(error, retry, fail, attempts) { + if (error.message.match(/fetch/) && attempts <= 3) { + // retry on fetch errors, 3 max attempts + retry() + } else { + fail() + } + } +}) +``` -The `maxRetries` option determines how many retries are allowed (default: `3`). When max times of retries have been attempted, the component will go into error state (render the `errorComponent` if provided) with the last failed error. +Note that `retry/fail` are like `resolve/reject` of a promise: one of them must be called for the error handling to continue. ## Using with Suspense From b0e002a4d36cfaad6635bfb2e9f97164dc47c9cb Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 9 Apr 2020 10:23:10 -0400 Subject: [PATCH 5/5] Rename 0000-async-component-api.md to 0026-async-component-api.md --- .../{0000-async-component-api.md => 0026-async-component-api.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename active-rfcs/{0000-async-component-api.md => 0026-async-component-api.md} (100%) diff --git a/active-rfcs/0000-async-component-api.md b/active-rfcs/0026-async-component-api.md similarity index 100% rename from active-rfcs/0000-async-component-api.md rename to active-rfcs/0026-async-component-api.md