Angular Query

The Ultimate Server State Synchronization

Tomasz Ducin  •  ducin.dev

Hi, I'm Tomasz

Independent Consultant & Software Architect

Trainer, Speaker, JS/TS Expert

ArchitekturaNaFroncie.pl (ANF)

Warsaw, PL

tomasz (at) ducin.dev

🦋 @ducin.dev

𝕏  @tomasz_ducin

Client state vs Server state

WHY bother?

WHY bother?

server-state fetching

cache invalidation

background:

fetching

re-fetching

pre-fetching

infinite queries

data-loading
state machine

automatic GC

error handling
& retries

caching & sharing

pagination

declarative API

Pareto Principle: 80 / 20

Roughly 80% of consequences come from 20% of causes

Roughly 80% of FEATURES     come from 20% of API

Roughly 80% of consequences come from 20% of causes

Queries: declarative API

class ComponentOrServiceWhatever {
  productsHTTP = inject(ProductsHTTPService)
  
  query = injectQuery(() => ({
    queryKey: ['products'],
    queryFn: () => this.productsHTTP.getProducts(),
  }))
}
<ul>
  @for (p of query.data(); track p.id) {
    <li>{{ p.name }}</li>
  }
</ul>

Queries: declarative API

class ComponentOrServiceWhatever {
  productsHTTP = inject(ProductsHTTPService)
  page = input(1)

  nextPage(){ // or whatever
    this.page.update(v => v+1)
  }  
  
  query = injectQuery(() => ({
    queryKey: ['products', { page: this.page() }],
    queryFn: ({ queryKey }) => {
      const [, criteria] = queryKey
      return this.productsHTTP.getProducts(criteria)
    },
  }))
}

big picture

Queries + Keys + Mutations

Mutations

Mutations: imperative API

HTML: <button (click)="mutation.mutate(p.id)">REMOVE</button>

TS: class ComponentOrServiceWhatever {
  productsHTTP = inject(ProductsHTTPService)
  // ... query

  mutation = injectMutation((client) => ({
    mutationFn: (id: Product['id']) => this.productsHTTP.delete(id),
    onSuccess: (result, id) => {
      // invalidate ALL queries with product list
      client.invalidateQueries({ queryKey: ['products', 'list'] })
	  // remove query with product details
      client.removeQueries({ queryKey: ['products', 'details', id] })
    },
  }))
}

Mutations: imperative API

onSuccess: () => {
  // invalidate MATCHING queries?
  client.invalidateQueries({ queryKey: ... })
                            
  // remove (entirely) MATCHING queries?
  client.removeQueries({ queryKey: ... })
                        
  // refetch (eagerly) MATCHING queries?
  client.refetchQueries({ queryKey: ... })
  
  // reset (to the default state) MATCHING queries?
  client.resetQueries({ queryKey: ... })
  
  // cancel MATCHING running queries?
  client.cancelQueries({ queryKey: ['products'] })
  
  // locally update (existing) MATCHING queries?
  client.setQueryData(queryKey, (oldValue) => newValue)
},

Query Keys
are resource identifiers

Query Keys

all products
  -> ['products']

product filtered by name
  -> ['products', { name: 'xyz' }]

product given by ID: 123
  -> ['products', 123]

Query Keys

all products (paginated)
  -> ['products', 'list']
  -> ['products', 'list', { page: 1 }]

product filtered by name
  -> ['products', 'list', { name: 'xyz' }]

product filtered and paginated
  -> ['products', 'list', { page: 1, name: 'xyz' }]

product given by ID: 123
  -> ['products', 'details', 123]

Query Key Pattern Matching

KEY: ['products']
KEY: ['products', 'list']
KEY: ['products', 'list', { page: 1 }]
KEY: ['products', 'list', { page: 1, name: 'xyz' }]
KEY: ['products', 'details', 123]

['products', 'list']
['products', 'list', { page: 1 }]
['products', 'list', { name: 'xyz' }]
['products', 'list', { page: 1, name: 'xyz' }]
['products', 'details', 123]
['products', 'details', 456]

WHY bother?

server-state fetching

cache invalidation

background:

fetching

re-fetching

pre-fetching

infinite queries

data-loading
state machine

automatic GC

error handling
& retries

caching & sharing

pagination

declarative API

Thank you

Tomasz Ducin  •  ducin.dev