Render as you fetch with Angular lifecycle hooks
Published: 2022-04-25 10:59:17

What if you could load asynchronous data right inside your component without the need for async pipes, subscribe, or even RxJS? Let's jump right in.

## Fetching Data

To start with, we need some data to display in our app. We'll define a service that fetches some todos for us via HTTP.

```ts
const endpoint = "https://jsonplaceholder.typicode.com/todos"

@Injectable({ providedIn: "root" })
class TodosResource implements Fetchable<Todo[]> {
   fetch(userId: number) {
      return this.http.get(endpoint, {
         params: { userId }
      })
   }

   constructor(private http: HttpClient) {}
}
```

The fetch function itself doesn't *have* to return an observable. It could just as easily return any observable input, such as a promise. We'll make all resources adhere to this `Fetchable` interface.

For now this is just a normal Angular service. We'll come back to it later.

## The Resource Interface

At its core, a resource can do two things:

```ts
interface Resource<T extends Fetchable<any>> {
   fetch(...params: FetchParams<T>): any
   read(): FetchType<T> | undefined
}
```

**fetch** Tell the resource to fetch some data. This could come from a HTTP or GraphQL endpoint, a websocket or any another async source.

**read** Attempt to read the current value of the resource, which might be `undefined` because no value has arrived yet.

With this interface defined we can write a class that implements it.

## Implementation

The example below is truncated for the sake of brevity. A more concrete example can be [found here](https://gist.github.com/mmuscat/3db85b0e287213884e8849f859742a03)

```ts
import { EMPTY, from, Subscription } from "rxjs"

export class ResourceImpl<T extends Fetchable> 
   implements Resource<T> {

   value?: FetchType<T>
   params: any
   subscription: Subscription
   state: string

   next(value: FetchType<T>) {
      this.value = value
      this.state = "active"
      this.changeDetectorRef.markForCheck()
   }

   read(): FetchType<T> | undefined {
      if (this.state === "initial") {
         this.connect()
      }
      return this.value
   }

   fetch(...params: FetchParams<T>) {
      this.params = params
      if (this.state !== "initial") {
         this.connect()
      }
   }

   connect() {
      const source = this.fetchable.fetch(...this.params)
      this.state = "pending"
      this.unsubscribe()
      this.subscription = from(source).subscribe(this)
   }

   unsubscribe() {
      this.subscription.unsubscribe()
   }

   constructor(
      private fetchable: T,
      private changeDetectorRef: ChangeDetectorRef
   ) {
      this.source = EMPTY
      this.subscription = Subscription.EMPTY
      this.state = "initial"
   }
}
```

The resource delegates the actual data fetching logic to the `fetchable` object which is injected in the constructor. The resource will always return the latest value when it is read.

You'll also notice that we don't immediately fetch data if we are in an initial state. For the first fetch we wait until `read` is called. This is necessary to prevent unnecessary fetches when a component is first mounted.

Let's also write another service to help us manage our resources.

```ts
import { 
   Injectable, 
   InjectFlags, 
   Injector, 
   ChangeDetectorRef
} from "@angular/core"

@Injectable()
export class ResourceManager {
   private cache: Map<any, ResourceImpl<Fetchable>>

   get<T extends Fetchable>(token: Type<T>): Resource<T> {
      if (this.cache.has(token)) {
         return this.cache.get(token)!
      }
      const fetchable = this.injector.get(token)
      const changeDetectorRef = this.injector
         .get(ChangeDetectorRef, undefined, InjectFlags.Self)
      const resource = new ResourceImpl(
         fetchable, 
         changeDetectorRef
      )
      this.cache.set(token, resource)
      return resource
   }

   ngOnDestroy() {
      for (const resource of this.cache.values()) {
         resource.unsubscribe()
      }
   }

   constructor(private injector: Injector) {
      this.cache = new Map()
   }
}
```

## Usage

Now that we have built our resource services, let's see it in action!

```html
<!-- todos.component.html -->

<div *ngFor="let todo of todos">
  <input type="checkbox" [value]="todo.complete" readonly />
  <span>{{ todo.title }}</span>
</div>

<button (click)="loadNextUser()">
  Next user
</button>
```

```ts
import {
   Component,
   OnChanges,
   DoCheck,
   Input,
   ChangeDetectionStrategy
} from "@angular/core"

import { 
   Resource,
   ResourceManager
} from "./resource-manager.service"

import { Todos, TodosResource } from "./todos.resource"

@Component({
   selector: "todos",
   templateUrl: "./todos.component.html",
   providers: [ResourceManager],
   changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent implements OnChanges, DoCheck {
   @Input()
   userId: number

   resource: Resource<TodosResource>

   todos?: Todos[]

   ngOnChanges() {
      this.loadNextUser(this.userId)
   }

   ngDoCheck() {
      this.todos = this.resource.read()
   }

   loadNextUser(userId = this.userId++) {
      this.resource.fetch(userId)
   }

   constructor(manager: ResourceManager) {
      this.userId = 1
      this.resource = manager.get(TodosResource)
      this.resource.fetch(this.userId)
   }
}
```

Finally, we can see that `fetch` is called twice; once in the constructor and again during the `ngOnChanges` lifecycle hook. That's why we need to wait for a `read` before subscribing to the data source for the first time.

All the magic happens in `ngDoCheck`. It's normally a bad idea to use this hook, but it's perfect for *rendering as you fetch*! The `read` function simply returns the current value of the resource and assigns it to `todos`. If the resource hasn't changed since the last read, it's a no-op.

If you're wondering why this works, scroll back to the `next` function in `ResourceImpl`. 

```ts
next() {
   // ...
   this.changeDetectorRef.markForCheck()
}
```

This marks the view dirty every time the resource receives a new value, and eventually triggers `ngDoCheck`. If a resource happens to be producing synchronous values very quickly we also avoid additional change detection calls. Neat!

## Summary

We can render as we fetch by taking advantage of Angular's change detection mechanism. This makes it easy to load multiple data streams in parallel without blocking the view, and with a little more effort we can also show a nice fallback to the user while the data is loading. The approach is data agnostic and should complement your existing code.

Happy coding!