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

New Async Component API #148

Merged
merged 5 commits into from
Apr 9, 2020
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions active-rfcs/0000-async-component-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
- 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 { defineAsyncComponent } from "vue"

// simple usage
const AsyncFoo = defineAsyncComponent(() => import("./Foo.vue"))

// with options
const AsyncFooWithOptions = defineAsyncComponent({
loader: () => import("./Foo.vue"),
loadingComponent: LoadingComponent,
errorComponent: 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 { defineAsyncComponent } from "vue"

// simple usage
const AsyncFoo = defineAsyncComponent(() => import("./Foo.vue"))
```

`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.

- **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 = defineAsyncComponent(() => new Promise((resolve, reject) => {
/* ... */
}))
```

## Options Usage

```js
import { defineAsyncComponent } from "vue"

const AsyncFooWithOptions = defineAsyncComponent({
loader: () => import("./Foo.vue"),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 100, // default: 200
timeout: 3000, // default: Infinity
suspensible: false, // default: true
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
retry()
} else {
fail()
}
}
})
```

- 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.

In 2.x, an async component with options is defined as

```ts
() => ({
component: Promise<Component>
// ...other options
})
```

Whereas in 3.x it is now:

```ts
defineAsyncComponent({
loader: () => Promise<Component>
// ...other options
})
```

- 2.x `loading` and `error` options are renamed to `loadingComponent` and `errorComponent` respectively to be more explicit.

## Retry Control

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()
}
}
})
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyberAP that doesn't seem to be much different from retryWhen, and it doesn't have the flexibility @Morgul wanted.

I agree with @backbond87 that a separate onError option might be the most straightforward.

I am ok with that idea as well. Could onError arguments be contained in a single destructurable object? So there's no need to remember the exact order in which they're passed to a callback.

Suggested change
``` 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()
}
}
})
```
``` 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()
}
}
})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, the order feels pretty natural to me. With destructure instead of the order, you'll have to remember the exact names. It's not much different imo.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its less about remembering imho, but more about needing only a few of the args most of the time, depending on the use case. See my examples


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

Async component in 3.x are *suspensible* by default. This means if it has a `<Suspense>` in the parent chain, it will be treated as an async dependency of that `<Suspense>`. In this case, the loading state will be controlled by the `<Suspense>`, 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.