As I have been trying to learn Angular Query, I discovered that there is not a lot of documentation on it yet, even though it is an amazing framework. Consequently, I have been going through a lot of React Query tutorials to try to learn its best practices and bring those over to how I use Angular Query. Some of these differences, however, are not obvious, and have tripped me up. Hopefully this list will help you out until there are more tutorials and examples specific to Angular.
Differences
1. injectQuery
in any injection context
In React, useQuery
follows the Rules of hooks, namely that there are specific places where useQuery
can be called, and it cannot be called anywhere else.
In Angular Query, injectQuery
does not have that limitation. Under normal circumstances, we can inject it whenever we are inside an injection context. Here are some examples where it would work:
Class variables and constructors
@Component({
standalone: true,
templateUrl: './bulbasaur.component.html',
})
export class BulbasaurComponent {
private readonly http = inject(HttpClient);
// π
readonly bulbasaurQuery = injectQuery(() => ({
queryKey: ['bulbasaur'],
queryFn: () =>
lastValueFrom(
this.http.get<PokeData>(
'https://pokeapi.co/api/v2/pokemon/bulbasaur'
),
),
}));
constructor() {
// π
const mewQuery = injectQuery(() => ({
queryKey: ['mew'],
queryFn: () =>
lastValueFrom(
this.http.get<PokeData>(
'https://pokeapi.co/api/v2/pokemon/mew'
),
),
})
);
}
}
Anywhere the library author says
NgRx signal store features are run in an injection context. Here is how to use a query inside a signals store:
type PokeId = 'bulbasaur' | 'ivysaur' | 'venusaur';
type PokeData = {...};
interface PokemonState {
pokeId: PokeId;
}
const initialState: PokemonState = {
pokeId: 'bulbasaur',
};
const PokemonStore = signalStore(
withState(initialState),
withComputed((store) => {
const http = inject(HttpClient);
const { status, data } = injectQuery(() => ({
queryKey: ['pokemon', store.pokeId()],
queryFn: () =>
lastValueFrom(
http.get<PokeData>(
`https://pokeapi.co/api/v2/pokemon/${store.pokeId()}`
),
),
}));
return { status, data };
}),
withMethods((store) => ({
setPokeId: (pokeId: PokeId) => patchState(store, { pokeId }),
})),
);
Anywhere else
Route guards (don't do it), resolvers, or anywhere if we include the current Injector
:
@Component({
standalone: true,
templateUrl: './poke-prefetch.component.html',
})
export class PokemenComponent {
private readonly http = inject(HttpClient);
private readonly injector = inject(Injector);
// https://bulbapedia.bulbagarden.net/wiki/Category:Male-only_Pok%C3%A9mon
pokemen = [
'nidoranβ',
'nidoking',
'hitmonlee',
'hitmonchan',
'tauros',
] as const;
async prefetchPokemen() {
const queryClient = injectQueryClient({ injector: this.injector });
await Promise.all(
this.pokemen.map((pokeman) => {
return queryClient.prefetchQuery({
queryKey: ['pokemon', pokeman],
queryFn: () =>
lastValueFrom(
this.http.get<PokeData>(
`https://pokeapi.co/api/v2/pokemon/${pokeman}`,
),
),
});
}),
);
}
}
2. useQuery({...})
versus injectQuery(() => {...})
This is the reason for this article. I messed this up so many times when I first started. The first parameter of injectQuery
is a function, not an object. But why?
It does not say anything about this in Angular Query's documentation, but it does in Solid Query:
Arguments to solid-query primitives (like createQuery, createMutation, useIsFetching) listed above are functions, so that they can be tracked in a reactive scope.
I don't know what a reactive scope is, so maybe not π
Just remember that injectQuery
's first parameter is a function and you'll be good to go.
3. Angular gets queryClient
for free
injectQuery
's first parameter actually passes queryClient
as its first parameter:
// ππ
const query = injectQuery((queryClient) => ({
queryKey, queryFn
}));
I have found this very useful with injectMutation
and optimistic updates.
// No need to also add injectQueryClient()
const mutation = injectMutation((queryClient) => ({
mutationFn: (updates) => this.service.updateThing(updates),
onMutate: async (updates) => {
await queryClient.cancelQueries({ queryKey });
const snapshot = queryClient.getQueryData(queryKey);
queryClient.setQueryData(queryKey, (prev) =>
prev.filter((p) => p.id !== updates.id),
);
return snapshot;
},
onError: (_error, _variables, snapshot) => {
queryClient.setQueryData(queryKey, snapshot);
},
onSettled: () => {
return queryClient.invalidateQueries({ queryKey });
},
}));
4. Superfluous differences
These may not even be worth mentioning, but here goes.
- React is
use*
and Angular isinject*
- Angular uses signals. To access
data
or any of the fields of the query object, add()
to the end of it
// React
const MyComponent = () => {
const { data } = useQuery({ queryKey, queryFn });
return <p>{ data ?? 'π¬' }</p>;
};
// Angular
@Component({
standalone: true, // π
template: `<p>{{ query.data() ?? 'π¬' }}</p>`,
}) // π
class MyComponent {
query = injectQuery({ queryKey, queryFn });
}
Notes
I use injectQuery
in all of the examples, but everything applies to injectMutation
, too. Once injectQueries
is ready, these will hopefully apply there, too.
Conclusion
That's it! Thanks for reading and I hope you learned something! Let me know if I missed anything.
Top comments (0)