You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sometimes a component needs to render data that is only available _asynchronously_. Such data might be fetched from a server, a database, or in general retrieved or computed from an async API.
17
17
18
-
While Lit's reactive update lifecycle is batched and asynchronous, Lit templates always render _synchronously_. The data used in a template must be readable at the time of rendering. To render async data in a Lit component, you must wait for the data to be ready, store it so that's it's readable synchronously, then trigger a new render.
18
+
While Lit's reactive update lifecycle is batched and asynchronous, Lit templates always render _synchronously_. The data used in a template must be readable at the time of rendering. To render async data in a Lit component, you must wait for the data to be ready, store it so that's it's readable, then trigger a new render which can use the data synchronously.
19
19
20
20
The `@lit-labs/task` package provides a `Task` reactive controller to help manage this async data workflow.
21
21
22
+
`Task` is a controller that takes an async task function and runs it either manually or automatically when its arguments change. Task stores the result of the task function and updates the host element when the task function completes so the result can be used in rendering.
23
+
22
24
### Example
23
25
24
-
This is an example of using `Task` to call an HTTP API via `fetch()`. The API is called whenever the `productId` parameter changes, and the component renders a loading message when the data is being fetched.
26
+
This is an example of using `Task` to call an HTTP API via [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). The API is called whenever the `productId` parameter changes, and the component renders a loading message when the data is being fetched.
if (!response.ok) { thrownewError(response.status); }
70
+
returnresponse.json();
71
+
},
72
+
args: () => [this.productId]
73
+
});
86
74
87
75
render() {
88
-
returnhtml`
89
-
${this._apiTask.render({
90
-
pending: () =>html`
91
-
<divclass="loading">Loading product...</div>
92
-
`,
93
-
complete: (product) =>html`
76
+
returnthis._productTask.render({
77
+
pending: () =>html`<p>Loading product...</p>`,
78
+
complete: (product) =>html`
94
79
<h1>${product.name}</h1>
95
80
<p>${product.price}</p>
96
81
`,
97
-
})}
98
-
`;
82
+
error: (e) =>html`<p>Error: ${e}</p>`
83
+
});
99
84
}
100
85
}
101
86
```
@@ -112,6 +97,7 @@ Task takes care of a number of things needed to properly manage async work:
112
97
- Triggers a host update when the task changes status
113
98
- Handles race conditions, ensuring that only the latest task invocation completes the task
114
99
- Renders the correct template for the current task status
100
+
- Aborting tasks with an AbortController
115
101
116
102
This removes most of the boilerplate for correctly using async data from your code, and ensures robust handling of race conditions and other edge-cases.
117
103
@@ -127,14 +113,11 @@ Async data is usually returned from an async API, which can come in a few forms:
127
113
128
114
## What is a task?
129
115
130
-
At the core of the Task controller,
116
+
At the core of the Task controller is the concept of a "task" itself.
131
117
132
-
* async function, returns promise
133
-
* parameters
134
-
* can throw error
135
-
* can throw initial state
136
-
* request/response: Task helps with APIs where you make a request or function call, and then wait for a response.
137
-
* status: initial, pending, complete, or error
118
+
A task is an async operation which does some work to produce data. A task can be in a few different states (initial, pending, complete, and error) and can take parameters.
119
+
120
+
A task is a generic concept and could represent any async operation. They apply best when there is a request/response structure, such as a network fetch, database query, or waiting for a single event in response to some action. They're less applicable to spontaneous or streaming operations like an open-ended stream of events, a streaming database response, etc.
138
121
139
122
## Installation
140
123
@@ -171,38 +154,52 @@ As a class field, the task status and value are easily available:
171
154
this._task.value
172
155
```
173
156
174
-
### Arguments and the task function
157
+
### The task function
158
+
159
+
The most critical part of a task declaration is the _task function_. This is the function that does the actual work.
175
160
176
-
The most critical part of a task declaration is the _task function_. This is the function that does the actual work. The Task controller will automatically call the task function with arguments, which have to be supplied with a separate callback. Arguments are separate so they can be checked for changes and the task function is only called if the arguments have changed.
161
+
The task function is given in the `task` option. The Task controller will automatically call the task function with arguments, which are supplied with a separate `args`callback. Arguments are checked for changes and the task function is only called if the arguments have changed.
177
162
178
-
The task function takes the task arguments as an _array_ passed as the first parameter
163
+
The task function takes the task arguments as an _array_ passed as the first parameter, and an options argument as the second parameter:
179
164
180
165
```ts
181
166
newTask(this, {
182
-
task: async ([arg1, arg2]) => {
167
+
task: async ([arg1, arg2], {signal}) => {
183
168
// do async work here
184
169
},
185
170
args: () => [this.field1, this.field2]
186
171
})
187
172
```
188
173
174
+
The task function's args array and the args callback should be the same length.
175
+
189
176
{% aside "positive" %}
190
177
191
178
Write the `task` and `args` functions as arrow function so that the `this` reference points to the host element.
192
179
193
180
{% endaside %}
194
181
195
-
### Handling results
182
+
### Task status
183
+
184
+
Tasks can be in one of four states:
185
+
- INITIAL: The task has not been run
186
+
- PENDING: The task is running and awaiting a new value
187
+
- COMPLETE: The task completed successfully
188
+
- ERROR: The task errored
189
+
190
+
The Task status is available at the `status` field of the Task controller, and is represented by the `TaskStatus` object enum-like object, which has properties `INITIAL`, `PENDING`, `COMPLETE`, and `ERROR`.
196
191
197
-
When a task completes or otherwise changes status, it triggers a host update so the host can handle the new task status and render if needed.
192
+
Usually a Task will proceed from INITIAL to PENDING to one of COMPLETE or ERROR, and then back to PENDING if the task is re-run. When a task changes status it triggers a host update so the host element can handle the new task status and render if needed.
198
193
199
-
There are a few members on Task that represent the state of the task:
200
-
-`value`: the current value of the task, if it has completed
201
-
-`error`: the current error of the task, if it has errored
202
-
-`status`: the status of the task. See [Task status](#task-status)
194
+
There are a few members on Task that related to the state of the task:
195
+
-`status`: the status of the task.
196
+
-`value`: the current value of the task, if it has completed.
197
+
-`error`: the current error of the task, if it has errored.
203
198
-`render()`: A method that chooses a callback to run based on the current status.
204
199
205
-
The simplest and most common API to use is `render()`, since it chooses the right code to run and provides it the relevant data.
200
+
### Rendering Tasks
201
+
202
+
The simplest and most common API to use to render a task is `task.render()`, since it chooses the right code to run and provides it the relevant data.
206
203
207
204
`render()` takes a config object with an optional callback for each task status:
208
205
-`initial()`
@@ -227,10 +224,14 @@ You can use `task.render()` inside a Lit `render()` method to render templates b
227
224
228
225
### Running tasks
229
226
230
-
By default, Tasks will run any time the arguments change. This is controlled by the `autoRun` option, which default to `true`.
227
+
By default, Tasks will run any time the arguments change. This is controlled by the `autoRun` option, which defaults to `true`.
228
+
229
+
#### Auto-run
231
230
232
231
In auto-run mode, the task will call the `args` function when the host has updated, compare the args to the previous args, and invoke the task function if they have changed.
233
232
233
+
#### Manual mode
234
+
234
235
If `autoRun` is set to false, the task will be in _manual_ mode. In manual mode you can run the task by calling the `.run()` method, possibly from an event handler:
235
236
236
237
```ts
@@ -277,20 +278,51 @@ Tasks can be in one of four states:
277
278
278
279
Usually a Task will proceed from INITIAL to PENDING to one of COMPLETE or ERROR, and then back to PENDING if the task is re-run.
279
280
281
+
Task status is available at the `status` field of the Task controller, and is represented by the `TaskStatus` object.
282
+
280
283
It's important to understand the status a task can be in, but it's not usually necessary to access it directly.
281
284
282
-
Task status is available at the `status` field of the Task controller, and is represented by the `TaskStatus` object.
283
285
284
286
```ts
285
287
import {TaskStatus} from'@lit-labs/task';
286
288
287
289
// ...
288
-
289
290
if (this.task.status===TaskStatus.ERROR) {
290
291
// ...
291
292
}
292
293
```
293
294
295
+
### Aborting tasks
296
+
297
+
The second argument to the `Task` constructor is an options object that carries an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) in the `signal` property. This can be used to cancel a pending task because a new task run has started.
298
+
299
+
The easiest way to use a signal is to forward it to an API that accepts it, like `fetch()`.
300
+
301
+
```ts
302
+
private_task=newTask(this, {
303
+
task: async (args, {signal}) => {
304
+
const response =awaitfetch(someUrl, {signal});
305
+
// ...
306
+
},
307
+
});
308
+
```
309
+
310
+
Forwarding the signal to `fetch()` will cause the browser to cancel the network request if the signal is aborted.
311
+
312
+
You can also check if a signal has been aborted in your task function. It's often useful to race a Promise against a signal:
313
+
314
+
```ts
315
+
private_task=newTask(this, {
316
+
task: async ([arg1], {signal}) => {
317
+
const firstResult =awaitdoSomeWork(arg1);
318
+
signal.throwIfAborted();
319
+
const secondResult =awaitdoMoreWork(firstResult);
320
+
signal.throwIfAborted();
321
+
returnsecondResult;
322
+
},
323
+
});
324
+
```
325
+
294
326
### Task chaining
295
327
296
328
Sometimes you want to run one task when another task completes. To do this you can use the value of a task as an argument to the other:
@@ -327,4 +359,5 @@ class MyElement extends LitElement {
327
359
328
360
### Race conditions
329
361
330
-
_TODO: A task function can be called while previous task calls are still pending..._
362
+
A task function can be called while previous task calls are still pending. In this case the AbortSignal passed to previous runs will be aborted and the Task controller will ignore the result of the previous task runs and only handle the most recent run.
0 commit comments