Mutações no React Query
Falaa Dev Doido! Estamos aqui novamente agora em definitivo para mais um post sobre React Query. Nos últimos posts navegamos profundo sobre queries, e chegou a hora de estudarmos como funcionam as mutações na lib.
Introdução
Diferente das queries, mutações são tipicamente utilizada para criar/atualizar/deletar dados no servidor. Para este fim a lib disponibiliza a hook useMutation
. Segue o exemplo:
function App() {
const mutation = useMutation(newPokemon => axios.post('/pokemons', newPokemon))
return (
<div>
{mutation.isLoading ? (
'Adding pokemon...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Pokemon added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do thunder rain' })
}}
>
Create Pokemon
</button>
</>
)}
</div>
)
}
Uma mutação pode ter os seguintes estados possíveis:
isIdle
oustatus==='idle'
- A mutação está esperando pra ser executada num state de reset ou pré-renderização do componente na tela.
isLoading
oustatus ==='loading'
- A mutação está executando.
isError
oustatus === 'error'
- A mutação encontrou um erro.
isSuccess
oustatus==='success'
- A mutação retornou com sucesso e o dado modificado já está disponível.
Através desses estados primários, mais informação fica disponível dependendo do estado da mutação:
error
- Se a mutação tiver retornado erro o objeto
error
estará disponível contendo a informação.
data
- Se a mutação tiver dado certo, o objeto
data
estará disponível contendo os dados.
No exemplo acima, se você observar é possível passar variáveis para as suas mutações chamando a função mutate
com uma única variável ou objeto.
Até então não estamos vendo nada de extraordinário, exceto o fato de que quando usamos a opção onSuccess
, o método de QueryClient
invalidateQueries
e setQueryData
ficam disponíveis para uso, tornando a ferramenta poderosa.
const CreatePokemon = () => {
const mutation = useMutation(formData => {
return fetch('/api', formData)
})
const onSubmit = event => {
event.preventDefault()
mutation.mutate(new FormData(event.target))
}
return <form onSubmit={onSubmit}>...</form>
}
Resetando o state de mutation
Algumas vezes você precisa limpar os valores de error
ou data
de uma request mutation. Pra fazer isso basta usar a função reset
como mostrado abaixo:
const CreatePokemon = () => {
const [title, setTitle] = useState('')
const mutation = useMutation(createPokemon)
const onCreatePokemon = e => {
e.preventDefault()
mutation.mutate({ title })
}
return (
<form onSubmit={onCreatePokemon}>
{mutation.error && (
<h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
)}
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<br />
<button type="submit">Create Pokemon</button>
</form>
)
}
Efeitos colaterais
Se até remédio tem efeito colateral, porque mutation não vai ter, não é mesmo?
Pra isso existem funções disponibilizadas para serem implementadas dentro da hook useMutate
e que devemos usá-las quando for necessário.
useMutation(addTodo, {
onMutate: variables => {
// A mutation is about to happen!
// Optionally return a context containing data to use when for example rolling back
return { id: 1 }
},
onError: (error, variables, context) => {
// An error happened!
console.log(`rolling back optimistic update with id ${context.id}`)
},
onSuccess: (data, variables, context) => {
// Boom baby!
},
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
})
OBS:
onSuccess
e onError
executam antes de onSettled
.
Além de passar essas callbacks dentro de useMutation
, é possível defini-lás em mutate
também, olha só:
useMutation(addTodo, {
onSuccess: (data, variables, context) => {
// I will fire first
},
onError: (error, variables, context) => {
// I will fire first
},
onSettled: (data, error, variables, context) => {
// I will fire first
},
})
mutate(todo, {
onSuccess: (data, variables, context) => {
// I will fire second!
},
onError: (error, variables, context) => {
// I will fire second!
},
onSettled: (data, error, variables, context) => {
// I will fire second!
},
})
Promises
Quando a parada é assíncrona, basta utilizar o método mutateAsync
ao invés de mutate
.
const mutation = useMutation(addTodo)
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
Retries
Por padrão não temos retentativas no useMutation
, então devemos passar o parâmetro retry
caso desejamos este comportamento:
const mutation = useMutation(addTodo, {
retry: 3,
})
Mutations com persistência de dados
Para persistir os resultados das mutations em algum storage, basta usarmos algumas funções como no exemplo abaixo:
const queryClient = new QueryClient()
// Define the "addTodo" mutation
queryClient.setMutationDefaults('addTodo', {
mutationFn: addTodo,
onMutate: async (variables) => {
// Cancel current queries for the todos list
await queryClient.cancelQueries('todos')
// Create optimistic todo
const optimisticTodo = { id: uuid(), title: variables.title }
// Add optimistic todo to todos list
queryClient.setQueryData('todos', old => [...old, optimisticTodo])
// Return context with the optimistic todo
return { optimisticTodo }
},
onSuccess: (result, variables, context) => {
// Replace optimistic todo in the todos list with the result
queryClient.setQueryData('todos', old => old.map(todo => todo.id === context.optimisticTodo.id ? result : todo))
},
onError: (error, variables, context) => {
// Remove optimistic todo from the todos list
queryClient.setQueryData('todos', old => old.filter(todo => todo.id !== context.optimisticTodo.id))
},
retry: 3,
})
// Start mutation in some component:
const mutation = useMutation('addTodo')
mutation.mutate({ title: 'title' })
// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)
// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)
// Resume the paused mutations:
queryClient.resumePausedMutations()
Invalidando mutations
Invalidar queries já é um trabalho enorme. Saber quando invalidar essas queries é outro parto. Geralmente, quando uma mutação no seu app acontece, é MUITO provável que existam queries relacionadas que precisam ser invalidadas e chamadas novamente para as novas modificações causadas pela sua mutation. Por exemplo, vamos supor que fizemos uma mutation post:
const mutation = useMutation(postTodo)
Pra invalidar as queries relacionadas a essa mutation e chamá-las novamente fazemos:
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})
Simples não?
Objetos atualizados
Quando lidamos com mutações que atualizam objetos no servidor, é comum retornarmos o novo objeto na resposta vinda da API. Ao invés de recarregar quaisquer queries de busca para esse item e gastar mais uma chamada na rede nós já temos esse dado conosco, e com o React Query é possível fazer atualização do dado através do método setQueryData
. Veja:
const queryClient = useQueryClient()
const mutation = useMutation(editTodo, {
onSuccess: (data,variables) => {
queryClient.setQueryData(['todo', { id: variables.id }], data)
}
})
mutation.mutate({
id: 5,
name: 'Do the laundry',
})
// The query below will be updated with the response from the
// successful mutation
const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoById)
E quando uma mutation de update falha no servidor?
Queremos atualizar a lista depois de atualizar um elemento. Pra esse caso nós usamos o método onMutate
dentro de useMutation
da seguinte forma:
const queryClient = useQueryClient()
useMutation(updateTodo, {
// When mutate is called:
onMutate: async newTodo => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries('todos')
// Snapshot the previous value
const previousTodos = queryClient.getQueryData('todos')
// Optimistically update to the new value
queryClient.setQueryData('todos', old => [...old, newTodo])
// Return a context object with the snapshotted value
return { previousTodos }
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData('todos', context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries('todos')
},
})
Atualizando um objeto só
Basta buscar pelo id e guardar a informação antiga. Veja:
useMutation(updateTodo, {
// When mutate is called:
onMutate: async newTodo => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(['todos', newTodo.id])
// Snapshot the previous value
const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
// Optimistically update to the new value
queryClient.setQueryData(['todos', newTodo.id], newTodo)
// Return a context with the previous and new todo
return { previousTodo, newTodo }
},
// If the mutation fails, use the context we returned above
onError: (err, newTodo, context) => {
queryClient.setQueryData(
['todos', context.newTodo.id],
context.previousTodo
)
},
// Always refetch after error or success:
onSettled: newTodo => {
queryClient.invalidateQueries(['todos', newTodo.id])
},
})
Se você gosta de resolver tudo numa função só a biblioteca te dá a opção de enfiar toda a lógica dentro a função onSettled
, fica feio mas funciona. Veja:
useMutation(updateTodo, {
// ...
onSettled: (newTodo, error, variables, context) => {
if (error) {
// do something
}
},
})
Por hoje é só pessoal, até a próxima!