Unlike queries, mutations are used to create, update, delete, or otherwise perform server side effects. In Lit, use createMutationController.
import { LitElement, html } from 'lit'
import {
QueryClient,
QueryClientProvider,
createMutationController,
createQueryController,
} from '@tanstack/lit-query'
const queryClient = new QueryClient()
class AppQueryProvider extends QueryClientProvider {
constructor() {
super()
this.client = queryClient
}
}
customElements.define('app-query-provider', AppQueryProvider)
class TodosView extends LitElement {
private readonly todos = createQueryController(this, {
queryKey: ['todos'],
queryFn: fetchTodos,
})
private readonly addTodo = createMutationController(this, {
mutationFn: createTodo,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
render() {
const query = this.todos()
const mutation = this.addTodo()
const todos = query.data ?? []
return html`
${mutation.isError ? html`<p>${mutation.error.message}</p>` : null}
${mutation.isSuccess ? html`<p>Todo added</p>` : null}
<button
?disabled=${mutation.isPending}
@click=${() => this.addTodo.mutate({ title: 'Write mutation docs' })}
>
${mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
<ul>
${todos.map((todo) => html`<li>${todo.title}</li>`)}
</ul>
`
}
}
customElements.define('todos-view', TodosView)import { LitElement, html } from 'lit'
import {
QueryClient,
QueryClientProvider,
createMutationController,
createQueryController,
} from '@tanstack/lit-query'
const queryClient = new QueryClient()
class AppQueryProvider extends QueryClientProvider {
constructor() {
super()
this.client = queryClient
}
}
customElements.define('app-query-provider', AppQueryProvider)
class TodosView extends LitElement {
private readonly todos = createQueryController(this, {
queryKey: ['todos'],
queryFn: fetchTodos,
})
private readonly addTodo = createMutationController(this, {
mutationFn: createTodo,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
render() {
const query = this.todos()
const mutation = this.addTodo()
const todos = query.data ?? []
return html`
${mutation.isError ? html`<p>${mutation.error.message}</p>` : null}
${mutation.isSuccess ? html`<p>Todo added</p>` : null}
<button
?disabled=${mutation.isPending}
@click=${() => this.addTodo.mutate({ title: 'Write mutation docs' })}
>
${mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
<ul>
${todos.map((todo) => html`<li>${todo.title}</li>`)}
</ul>
`
}
}
customElements.define('todos-view', TodosView)Render the element under the provider so the controllers can resolve the same QueryClient from Lit context:
<app-query-provider>
<todos-view></todos-view>
</app-query-provider><app-query-provider>
<todos-view></todos-view>
</app-query-provider>A mutation can be in one of these primary states:
Pass variables to the mutation function by calling mutate:
this.addTodo.mutate({
title: this.nextTitle,
})this.addTodo.mutate({
title: this.nextTitle,
})mutate throws synchronously if the controller cannot resolve a QueryClient, such as when the element is not under a connected QueryClientProvider and no explicit client was passed. mutateAsync reports the same setup problem as a rejected promise.
Use mutateAsync when you want a promise:
try {
const created = await this.addTodo.mutateAsync({ title: this.nextTitle })
this.nextTitle = created.title
} catch (error) {
this.errorMessage = String(error)
}try {
const created = await this.addTodo.mutateAsync({ title: this.nextTitle })
this.nextTitle = created.title
} catch (error) {
this.errorMessage = String(error)
}The accessor includes reset:
html`
${mutation.isError
? html`<button @click=${() => this.addTodo.reset()}>Clear error</button>`
: null}
`html`
${mutation.isError
? html`<button @click=${() => this.addTodo.reset()}>Clear error</button>`
: null}
`Mutation options support onMutate, onError, onSuccess, and onSettled. The pagination example passes an explicit queryClient to the controller and uses the same in-scope client for optimistic updates and rollback:
private readonly favoriteMutation = createMutationController(
this,
{
mutationKey: ['toggle-project-favorite'],
mutationFn: async (input) => {
const response = await toggleProjectFavoriteOnServer(input)
return response.project
},
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: ['projects'] })
const snapshots = queryClient.getQueriesData<ProjectsPageResponse>({
queryKey: ['projects'],
})
for (const [key, existing] of snapshots) {
if (!existing) continue
queryClient.setQueryData<ProjectsPageResponse>(key, {
...existing,
projects: existing.projects.map((project) =>
project.id === variables.id
? { ...project, isFavorite: variables.isFavorite }
: project,
),
})
}
return { snapshots }
},
onError: (_error, _variables, context) => {
for (const [key, snapshot] of context?.snapshots ?? []) {
queryClient.setQueryData(key, snapshot)
}
},
onSettled: async () => {
await queryClient.invalidateQueries({ queryKey: ['projects'] })
},
},
queryClient,
)private readonly favoriteMutation = createMutationController(
this,
{
mutationKey: ['toggle-project-favorite'],
mutationFn: async (input) => {
const response = await toggleProjectFavoriteOnServer(input)
return response.project
},
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: ['projects'] })
const snapshots = queryClient.getQueriesData<ProjectsPageResponse>({
queryKey: ['projects'],
})
for (const [key, existing] of snapshots) {
if (!existing) continue
queryClient.setQueryData<ProjectsPageResponse>(key, {
...existing,
projects: existing.projects.map((project) =>
project.id === variables.id
? { ...project, isFavorite: variables.isFavorite }
: project,
),
})
}
return { snapshots }
},
onError: (_error, _variables, context) => {
for (const [key, snapshot] of context?.snapshots ?? []) {
queryClient.setQueryData(key, snapshot)
}
},
onSettled: async () => {
await queryClient.invalidateQueries({ queryKey: ['projects'] })
},
},
queryClient,
)For the exact runnable flow, see the Pagination example.