Lidar com requisições HTTP em componentes React é uma tarefa comum e muitas vezes repetitiva. Sempre que precisamos buscar dados de uma API, acabamos escrevendo o mesmo padrão de useEffect, estados de loading e tratamento de erro.
Para evitar essa repetição e centralizar a lógica de requisição, podemos criar um hook customizado chamado useFetch.
O que esse hook resolve
-
Indicador de carregamento (loading)
-
Tratamento de erro
-
Execução condicional (pode pular a requisição se quiser)
-
Atualização dinâmica da URL ou dos parâmetros
-
Requisições manuais com refetch()
Normalmente, fazemos um fetch assim:
import React, { useState, useEffect } from 'react'
const Foo = () => {
const url = 'alguma url'
const [data, setData] = useState(null)
useEffect(() => {
const fetchData = async () => {
fetch(url)
.then((response) => response.json())
.then((result) => {
setData(result)
})
}
fetchData()
}, [])
if(!data) return null
return (
<div>
{data.map((item, itemIndex) => (
...
))}
</div>
)
}
export default Foo
Agora, vamos criar uma versão inicial simples do nosso hook:
import { useState, useEffect } from 'react'
const useFetch = (url) => {
const [data, setData] = useState(null)
useEffect(() => {
const fetchData = async () => {
fetch(url)
.then((response) => response.json())
.then((result) => {
setData(result)
})
}
fetchData()
}, [])
return { data }
}
export default useFetch
Este é um exemplo bem básico que possui várias limitações: não trata erros, nem mostra um indicador de carregamento, entre outros pontos.
Nosso componente poderia usá-lo assim:
const url = 'alguma url'
const { data } = useFetch(url)
Vamos começar a adicionar as funcionalidades mencionadas acima.
Loading
Saber que a requisição ainda está em andamento nos permite exibir um componente de loading, um ícone ou algo visual para melhorar a experiência do usuário. Vamos definir um estado específico para controlar isso.
import { useState, useEffect } from 'react'
const useFetch = (url) => {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
fetch(url)
.then((response) => response.json())
.then((result) => {
setData(result)
setIsLoading(false)
})
}
fetchData()
}, [])
return { data, isLoading }
}
export default useFetch
Nosso componente poderia usá-lo assim:
const { data, isLoading } = useFetch(url)
if (isLoading) return <LoadingComponent />
Tratamento de erros
Vamos criar dois estados: um booleano para indicar se houve erro, e outro para armazenar a mensagem de erro. Também vamos usar try/catch para capturar e lidar com qualquer exceção durante a requisição.
import { useState, useEffect } from 'react'
const useFetch = (url) => {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
try {
const response = await fetch(url)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [])
return { data, isLoading, hasError, errorMessage }
}
export default useFetch
Nosso componente poderia usá-lo assim:
const url = 'alguma url'
const { data, isLoading, hasError, errorMessage } = useFetch(url)
if (hasError) return <ErrorComponent message={errorMessage} />
Skip
E se quisermos executar o hook depois do render inicial, e não imediatamente no componentDidMount? Podemos fazer isso com uma prop chamada skip, que permite pular a chamada da API. Por padrão, essa flag será false, ou seja, a requisição será feita logo ao montar o componente.
import { useState, useEffect } from 'react'
const useFetch = (url, skip = false) => {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(url)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [])
return { data, isLoading, hasError, errorMessage }
}
export default useFetch
Nosso componente poderia usá-lo assim:
const url = 'alguma url'
const { data, isLoading, hasError, errorMessage } = useFetch(url, (skip = true))
URL dinâmica
E se quisermos alterar a URL dinamicamente e fazer uma nova requisição?
Vamos transformar a prop url em um estado interno chamado initialUrl, que será o valor inicial.
Também adicionaremos esse estado na lista de dependências do useEffect para que o hook execute novamente quando ele mudar.
Por fim, vamos expor a função updateUrl para que o componente possa alterar essa URL de fora.
import { useState, useEffect } from 'react'
const useFetch = (initialUrl, skip = false) => {
const [url, updateUrl] = useState(initialUrl)
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(url)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [url])
return { data, isLoading, hasError, errorMessage, updateUrl }
}
export default useFetch
Nosso componente poderia usá-lo assim:
const url = 'alguma url'
const { data, isLoading, hasError, errorMessage, updateUrl } = useFetch(url)
if(...) updateUrl('alguma outra url')
Parâmetros dinâmicos
E os parâmetros? E se quisermos filtrar dados dinamicamente, por exemplo?
Os parâmetros serão passados como um objeto.
Em seguida, transformamos esse objeto em uma string de query usando encodeURIComponent.
Depois, adicionamos os parâmetros como dependência do useEffect, para que qualquer mudança acione uma nova requisição.
import { useState, useEffect } from 'react'
const useFetch = (initialUrl, initialParams = {}, skip = false) => {
const [url, updateUrl] = useState(initialUrl)
const [params, updateParams] = useState(initialParams)
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const queryString = Object.keys(params)
.map(
(key) => encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
)
.join('&')
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(`${url}${queryString}`)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [url, params])
return { data, isLoading, hasError, errorMessage, updateUrl, updateParams }
}
export default useFetch
Nosso componente poderia usá-lo assim:
const url = 'alguma url'
const { data, isLoading, hasError, errorMessage, updateUrl, updateParams } = useFetch(
url,
(initialParams = {
id: 123456789,
query: 'Lorem Ipsum'
}) // vai virar 'id=123456789&query=Lorem%20Ipsum'
)
if(...) {
updateParams({
id: 4815162342,
query: 'Heber Leonard'
}) // vai virar 'id=4815162342&query=Heber%20Leonard'
}
Manual Refetch
Uma funcionalidade que costumo precisar é a capacidade de refazer a requisição manualmente. Para isso, basta manter um número no estado, usado como dependência do useEffect. A cada vez que o incrementamos, forçamos uma nova requisição.
import { useState, useEffect } from 'react'
const useFetch = (initialUrl, initialParams = {}, skip = false) => {
const [url, updateUrl] = useState(initialUrl)
const [params, updateParams] = useState(initialParams)
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [refetchIndex, setRefetchIndex] = useState(0)
const queryString = Object.keys(params)
.map(
(key) => encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
)
.join('&')
const refetch = () =>
setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1)
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(`${url}${queryString}`)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [url, params, refetchIndex])
return {
data,
isLoading,
hasError,
errorMessage,
updateUrl,
updateParams,
refetch,
}
}
export default useFetch
Nosso componente poderia usá-lo assim:
const url = 'alguma url'
const {
data,
isLoading,
hasError,
errorMessage,
updateUrl,
updateParams,
refetch,
} = useFetch(url)
return <button onClick={refetch} />
Finally
Você percebeu que estamos usando setIsLoading(false) tanto no response.ok, quanto no else e no catch?
Isso pode ser simplificado com o uso do bloco finally, que será sempre executado após o try ou catch.
Além de deixar o código mais limpo, evita duplicação desnecessária.
import { useState, useEffect } from 'react'
const useFetch = (initialUrl, initialParams = {}, skip = false) => {
const [url, updateUrl] = useState(initialUrl)
const [params, updateParams] = useState(initialParams)
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [refetchIndex, setRefetchIndex] = useState(0)
const queryString = Object.keys(params)
.map(
(key) => encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
)
.join('&')
const refetch = () =>
setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1)
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(`${url}${queryString}`)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
} finally {
setIsLoading(false)
}
}
fetchData()
}, [url, params, refetchIndex])
return {
data,
isLoading,
hasError,
errorMessage,
updateUrl,
updateParams,
refetch,
}
}
export default useFetch
Conclusão
Neste artigo, vimos como criar um hook useFetch reutilizável com a Fetch API nativa, lidando com carregamento, erros, parâmetros dinâmicos, atualização manual e muito mais.
Para projetos simples ou onde você quer controle direto sobre as requisições, essa abordagem funciona bem. Mas se você estiver usando Next.js, vale a pena considerar o SWR da Vercel que é uma alternativa mais robusta, com suporte a cache, revalidação e sincronização automática.