Paginação em página estática do Next com GraphQL
Aprenda a fazer a paginação no client-side utilizando Next + GraphQL.
Por Maurício Sousa | | JavascriptNextGraphQL
Para podermos fazer a paginação, é necessário obter os dados.
Podemos fazer isso com uma simples função loadPosts.
import { request } from 'graphql-request';
export const defaultLoadPostsVariables = {
sort: 'createdAt:desc',
start: 0,
limit: 6,
};
export const loadPosts = async (
viariables = {},
) => {
const data = await request(config.graphqlURL, GRAPHQL_QUERY, {
...defaultLoadPostsVariables,
...viariables,
});
return data;
};
Nela, nós importamos o ``request`` do graphql-request e passamos 3 argumentos:
- URL
- Query
- Query Variables
Depois é só retornar os dados obtidos pelo request.
Nós precisamos pegar o valor inicial das Query Variables, que estão sendo passadas na requisição, e repassar para o nosso componente responsável por renderizar a ṕagina.
Então, no getStaticProps, nos teremos que retornar o seguinte objeto:
return {
props: {
posts: data.posts,
variables: {
...defaultLoadPostsVariables,
},
},
};
No componente, serão necessários 4 estados para controlar a paginação.
- statePosts controlará os posts que estarão sendo mostrados em tela.
- stateVariables será responsável por atualizar o valor de start (Query Variable), que será utilizada na função loadPosts, criada anteriormente, para pegar os próximos posts.
- buttonDisabled irá desabilitar o botão quando não houver mais posts para carregar.
- noMorePosts será utilizado para mostrar uma mensagem quando não houver mais posts para carregar.
// posts e variables vem das props
const [statePosts, setStatePosts] = useState(posts);
const [stateVariables, setStateVariables] = useState(variables);
const [buttonDisabled, setButtonDisabled] = useState(false);
const [noMorePosts, setNoMorePosts] = useState(false);
Com todos os estados criados, vamos começar a construir uma função assíncrona, que nomeei de handleLoadMorePosts, responsável pela paginação.
Dentro da função, primeiro devemos setar o buttonDisabled como true, para que não seja possível chamar a função novamente enquanto os dados são buscados na API.
setButtonDisabled(true);
Essa função deverá atualizar a Query Variable start da seguinte forma:
- O start começa em 0 e o seu limite é 6, ou seja, 6 posts serão retornados inicialmente (do index 0 até o 5). Para que os próximos 6 posts sejam carregados, teremos que alterar o valor de start, somando-o com o limite.
- Assim, quando o start for 6, ele trará os itens do index 5 até o index 11, totalizando 6 posts, e assim por diante…
const newVariables = {
...stateVariables,
start: stateVariables.start + stateVariables.limit,
};
Com o valor de start atualizado, deveremos passá-lo para a função loadPosts, fazendo uma nova requisição que não retornará os posts anteriores, e sim apenas os posts a partir do valor do start.
const morePosts = await loadPosts(newVariables);
Com os dados da requisição em mãos, faremos uma verificação para saber se algum post foi retornado. Se não foi retornado, significa que todos os posts já foram carregados, então colocamos o estado noMorePosts como true e retornamos para que a execução da função acabe ali.
if (!morePosts || !morePosts.posts || !morePosts.posts.length) {
setNoMorePosts(true);
return;
}
Se passar da verificação significa que temos posts para carregar. Então, voltamos o buttonDisabled para false, atualizamos o valor do stateVariables - com o start modificado - e finalmente alteramos o statePosts, mesclando os posts já existentes com os posts retornados da API.
setButtonDisabled(false);
setStateVariables(newVariables);
// Passo uma função de callback no useState para pegar o valor atual do estado.
// Com esse valor, faço um spread para mesclar os posts já existentes com
// os posts novos que chegaram.
setStatePosts((p) => [...p, ...morePosts.posts]);
Construindo a interface do componente
Pronto! Agora que já possuímos os dados, falta apenas colocá-los em tela. Então, nosso componente deve retornar algo parecido com este trecho de código:
return (
<Posts>
// Repare que eu não repasso os posts recebidos via props
// e sim os posts que foram mesclados.
<PostGrid posts={statePosts} />
<ButtonContainer>
// Verifico se possui algum post no array de posts. Se não possuir,
// retorno uma mensagem informando que não há posts
{statePosts.length > 0 && (
<Button
onClick={handleLoadMorePosts}
disabled={buttonDisabled}
>
{noMorePosts ? 'Sem mais posts' : ' Carregar mais'}
</Button>
)}
</ButtonContainer>
</Posts>
);
Por fim, o código de nossa página ficará assim:
export default function Posts({ posts = [], variables }) {
const [statePosts, setStatePosts] = useState(posts);
const [stateVariables, setStateVariables] = useState(variables);
const [buttonDisabled, setButtonDisabled] = useState(false);
const [noMorePosts, setNoMorePosts] = useState(false);
const handleLoadMorePosts = async () => {
setButtonDisabled(true);
const newVariables = {
...stateVariables,
start: stateVariables.start + stateVariables.limit,
};
const morePosts = await loadPosts(newVariables);
if (!morePosts || !morePosts.posts || !morePosts.posts.length) {
setNoMorePosts(true);
return;
}
setButtonDisabled(false);
setStateVariables(newVariables);
setStatePosts((p) => [...p, ...morePosts.posts]);
};
return (
<Posts>
<PostGrid posts={statePosts} />
<ButtonContainer>
{statePosts.length > 0 && (
<Button
onClick={handleLoadMorePosts}
disabled={buttonDisabled}
>
{noMorePosts ? 'Sem mais posts' : ' Carregar mais'}
</Button>
)}
</ButtonContainer>
</Posts>
);
};
export const getStaticProps = async () => {
let data = null;
try {
data = await loadPosts();
} catch (error) {
data = null;
}
if (!data || !data.posts || !data.posts.length) {
return {
notFound: true,
};
}
return {
props: {
posts: data.posts,
variables: {
...defaultLoadPostsVariables,
},
},
};
};