Skip to content

Commit d6f1cfb

Browse files
committed
More updates
1 parent 820ae58 commit d6f1cfb

File tree

1 file changed

+101
-68
lines changed
  • packages/lit-dev-content/site/docs/v3/data

1 file changed

+101
-68
lines changed

packages/lit-dev-content/site/docs/v3/data/task.md

Lines changed: 101 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,49 +15,42 @@ eleventyNavigation:
1515

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

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

2020
The `@lit-labs/task` package provides a `Task` reactive controller to help manage this async data workflow.
2121

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+
2224
### Example
2325

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

2628
{% switchable-sample %}
2729

2830
```ts
2931
import {Task} from '@lit-labs/task';
3032

3133
class MyElement extends LitElement {
32-
@property()
33-
productId?: number;
34+
@property() productId?: number;
3435

35-
private _apiTask = new Task(
36-
this,
37-
{
38-
task: async ([productId]) =>
39-
const response = await fetch(`http://example.com/product/${productId}`);
40-
if (!response.ok)
41-
throw new Error(response.status);
42-
}
43-
return response.json();
44-
),
45-
args: () => [this.productId]
46-
}
47-
);
36+
private _productTask = new Task(this, {
37+
task: async ([productId], {signal}) => {
38+
const response = await fetch(`http://example.com/product/${productId}`, {signal});
39+
if (!response.ok) { throw new Error(response.status); }
40+
return response.json();
41+
},
42+
args: () => [this.productId]
43+
});
4844

4945
render() {
50-
return html`
51-
${this._apiTask.render({
52-
pending: () => html`
53-
<div class="loading">Loading product...</div>
54-
`,
55-
complete: (product) => html`
46+
return this._productTask.render({
47+
pending: () => html`<p>Loading product...</p>`,
48+
complete: (product) => html`
5649
<h1>${product.name}</h1>
5750
<p>${product.price}</p>
5851
`,
59-
})}
60-
`;
52+
error: (e) => html`<p>Error: ${e}</p>`
53+
});
6154
}
6255
}
6356
```
@@ -70,32 +63,24 @@ class MyElement extends LitElement {
7063
productId: {},
7164
};
7265

73-
private _apiTask = new Task(
74-
this,
75-
{
76-
task: async ([productId]) =>
77-
const response = await fetch(`http://example.com/product/${productId}`);
78-
if (!response.ok)
79-
throw new Error(response.status);
80-
}
81-
return response.json();
82-
),
83-
args: () => [this.productId]
84-
}
85-
);
66+
private _productTask = new Task(this, {
67+
task: async ([productId], {signal}) => {
68+
const response = await fetch(`http://example.com/product/${productId}`, {signal});
69+
if (!response.ok) { throw new Error(response.status); }
70+
return response.json();
71+
},
72+
args: () => [this.productId]
73+
});
8674

8775
render() {
88-
return html`
89-
${this._apiTask.render({
90-
pending: () => html`
91-
<div class="loading">Loading product...</div>
92-
`,
93-
complete: (product) => html`
76+
return this._productTask.render({
77+
pending: () => html`<p>Loading product...</p>`,
78+
complete: (product) => html`
9479
<h1>${product.name}</h1>
9580
<p>${product.price}</p>
9681
`,
97-
})}
98-
`;
82+
error: (e) => html`<p>Error: ${e}</p>`
83+
});
9984
}
10085
}
10186
```
@@ -112,6 +97,7 @@ Task takes care of a number of things needed to properly manage async work:
11297
- Triggers a host update when the task changes status
11398
- Handles race conditions, ensuring that only the latest task invocation completes the task
11499
- Renders the correct template for the current task status
100+
- Aborting tasks with an AbortController
115101

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

@@ -127,14 +113,11 @@ Async data is usually returned from an async API, which can come in a few forms:
127113

128114
## What is a task?
129115

130-
At the core of the Task controller,
116+
At the core of the Task controller is the concept of a "task" itself.
131117

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

139122
## Installation
140123

@@ -171,38 +154,52 @@ As a class field, the task status and value are easily available:
171154
this._task.value
172155
```
173156

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

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

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:
179164

180165
```ts
181166
new Task(this, {
182-
task: async ([arg1, arg2]) => {
167+
task: async ([arg1, arg2], {signal}) => {
183168
// do async work here
184169
},
185170
args: () => [this.field1, this.field2]
186171
})
187172
```
188173

174+
The task function's args array and the args callback should be the same length.
175+
189176
{% aside "positive" %}
190177

191178
Write the `task` and `args` functions as arrow function so that the `this` reference points to the host element.
192179

193180
{% endaside %}
194181

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

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

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.
203198
- `render()`: A method that chooses a callback to run based on the current status.
204199

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

207204
`render()` takes a config object with an optional callback for each task status:
208205
- `initial()`
@@ -227,10 +224,14 @@ You can use `task.render()` inside a Lit `render()` method to render templates b
227224

228225
### Running tasks
229226

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
231230

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

233+
#### Manual mode
234+
234235
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:
235236

236237
```ts
@@ -277,20 +278,51 @@ Tasks can be in one of four states:
277278

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

281+
Task status is available at the `status` field of the Task controller, and is represented by the `TaskStatus` object.
282+
280283
It's important to understand the status a task can be in, but it's not usually necessary to access it directly.
281284

282-
Task status is available at the `status` field of the Task controller, and is represented by the `TaskStatus` object.
283285

284286
```ts
285287
import {TaskStatus} from '@lit-labs/task';
286288

287289
// ...
288-
289290
if (this.task.status === TaskStatus.ERROR) {
290291
// ...
291292
}
292293
```
293294

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 = new Task(this, {
303+
task: async (args, {signal}) => {
304+
const response = await fetch(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 = new Task(this, {
316+
task: async ([arg1], {signal}) => {
317+
const firstResult = await doSomeWork(arg1);
318+
signal.throwIfAborted();
319+
const secondResult = await doMoreWork(firstResult);
320+
signal.throwIfAborted();
321+
return secondResult;
322+
},
323+
});
324+
```
325+
294326
### Task chaining
295327

296328
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 {
327359

328360
### Race conditions
329361

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

0 commit comments

Comments
 (0)