DEV Community

Jared Robertson
Jared Robertson

Posted on

TanStack Query: Angular/React differences

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'
          ),
        ),
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }),
  })),
);
Enter fullscreen mode Exit fullscreen mode

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}`,
              ),
            ),
        });
      }),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}));
Enter fullscreen mode Exit fullscreen mode

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 });
  },
}));
Enter fullscreen mode Exit fullscreen mode

4. Superfluous differences

These may not even be worth mentioning, but here goes.

  • React is use* and Angular is inject*
  • 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 });
}
Enter fullscreen mode Exit fullscreen mode

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)