Skip to content

Resource API Refactor

Now let’s take a look at how we might improve this application with resource and linkedSignal.

The interesting thing about this refactor is that we aren’t going to refactor to resource or rxResource everywhere in the application. We are going to hit scenarios that show the current limitations of resource.

This refactor is also much more straight forward than the refactor we did for Giflist. For this refactor I will help get things started, and then I will leave the rest of the refactor for you to do as an exercise. Remember that if you get stuck the source code is available.

The MessageService

Let’s start with the service we won’t be refactoring, and why. The reason we won’t be using resource or rxResource to load our messages is this:

messages$ = this.getMessages().pipe(
// restart stream when user reauthenticates
retry({
delay: () => this.authUser$.pipe(filter((user) => !!user)),
}),
);

Although the retry is a critical part for us as well, that’s not the specific reason why we can’t use rxResource here.

Our getMessages gives us a stream of messages coming from our backend. Whenever there is a new message, this emits.

The problem with rxResource is that, even though it allows us to use a stream to pull in data, it only actually uses the first value from that stream. We would get an initial set of messages, but they would never update.

We can see this if we inspect the internals of the rxResouce implementation:

export function rxResource<T, R>(opts: RxResourceOptions<T, R>): ResourceRef<T> {
opts?.injector || assertInInjectionContext(rxResource);
return resource<T, R>({
...opts,
loader: (params) => {
const cancelled = new Subject<void>();
params.abortSignal.addEventListener('abort', () => cancelled.next());
// Note: this is identical to `firstValueFrom` which we can't use,
// because at the time of writing, `core` still supports rxjs 6.x.
return new Promise<T>((resolve, reject) => {
opts
.loader(params)
.pipe(take(1), takeUntil(cancelled))
.subscribe({
next: resolve,
error: reject,
complete: () => reject(new Error('Resource completed before producing a value')),
});
});
},
});
}

This is actually the entire implementation of rxResource, and you may notice that it is basically just a resource with some extra stuff set up automatically for us.

It will use the stream we pass in and basically convert it into a Promise that only returns the first value, we can see that here:

.pipe(take(1), takeUntil(cancelled))

It takes one value and unsubscribes from the stream, because it immediately resolves the promise with that first value:

next: resolve,

This is why we can’t use it for our messages.

Refactoring the AuthService

Although the AuthService itself does not use resource or linkedSignal, we are going to make some changes here so that our other services (specifically the LoginService and RegisterService can).

@Injectable({
providedIn: 'root',
})
export class AuthService {
private auth = inject(AUTH);
// sources
private authState$ = authState(this.auth);
// state
user = toSignal(this.authState$);
async login(credentials: Credentials | undefined) {
if (!credentials) return null;
return signInWithEmailAndPassword(
this.auth,
credentials.email,
credentials.password,
);
}
logout() {
signOut(this.auth);
}
async createAccount(credentials: Credentials | undefined) {
if (!credentials) return null;
return createUserWithEmailAndPassword(
this.auth,
credentials.email,
credentials.password,
);
}
}

We are also just converting the user directly to a signal, but the main difference here is what we have done with the login and createAccount methods.

Now we are just returning Promises directly that will be consumed by resource rather than returning observables. However, an important point here is that we also just return null if the credentials are undefined.

When we try to use this with resource and supply that resource with a signal, the initial value is going to be undefined. So we need to make sure are handling that case.

Refactor the LoginService

This change I found quite ammusing.

@Injectable()
export class LoginService {
private authService = inject(AuthService);
// sources
login$ = new Subject<Credentials>();
login = toSignal(this.login$);
userAuthenticated = resource({
params: this.login,
loader: ({ params }) => this.authService.login(params),
});
}

That is a ridiculous amount of boiler plate that has just evaporated. We still have our login$ source, but now all we do is convert it to a signal, and use that to launch our request to the AuthService through a resource.

We still have access to all of the same stuff as before, and more, and with barely any code. The status and error signals are just tucked away inside of userAuthenticated now.

…and then the rest

I am going to leave the rest of the refactor to you to finish off now. Just as with the other applications we will need to make some changes in places where the application is consuming the LoginService.

And, there is stil the RegisterService that needs to be refactored. It is almost exactly the same as the LoginService. I would recommend trying to do it without referencing the LoginService first, then reference that if you need help, and finally you can reference the source code if you get stuck.

Summary

It is somewhat of a shame that we aren’t able to use resource everywhere in this application, as we saw with the MessagesService. Ideally, I prefer to stick to one paradigm that can be used universally across the application, but given the sheer power of resource and how much boilerplate it gets rid of, it’s a little too hard to ignore.