Fetch and Revalidate Data
You can fetching data for many ways. Befere you start you should read GraphGL section and learn how to generate types form GraphQL.
Server Side Rendering (SSR)
The best way and the most fastes way to fetching data. You can use it for fetching initial data for page.
Create page
Create new page in app
folder and define async component.
export default async function Page() {}
Function with fetcher
import { fetcher } from "@/graphql/fetcher";
import {
Forum_Forums__Show,
type Forum_Forums__ShowQuery,
type Forum_Forums__ShowQueryVariables
} from "@/graphql/hooks";
const getData = async (variables: Forum_Forums__ShowQueryVariables) => {
const { data } = await fetcher<
Forum_Forums__ShowQuery,
Forum_Forums__ShowQueryVariables
>({
query: Forum_Forums__Show,
variables,
cache: "force-cache" // this option is used for caching data for GraphQL
});
return data;
};
export default async function Page() {}
You can avoid using cache
option if you don't want to cache data for GraphQL. We don't recommend to use it in AdminCP.
Run query
export default async function Page() {
const data = await getData();
return <ForumsForumView data={data} />;
}
Inside view avoid using "use client"
component. Of course you can use it if you really need.
Query
Using TanStack Query (React Query) (opens in a new tab). You can use it for fetching data after page load for example after click on button.
To save the best performerce your app you should use React Lazy (opens in a new tab).
Create hook
Inside hooks/{module_name}
create new files with:
- suffix
api
, for examplequery-api.ts
(For Server Action), - prefix
use
, for exampleuse-use-short-show-groups.ts
Your schema should be similar like this:
- query-api.ts
- use-use-short-show-groups.ts
Server Action
Inside query-api.ts
you should create function for fetching data from server using fetcher
function.
"use server";
import { fetcher } from "@/graphql/fetcher";
import {
Admin__Core_Groups__Show_Short,
type Admin__Core_Groups__Show_ShortQuery,
type Admin__Core_Groups__Show_ShortQueryVariables
} from "@/graphql/hooks";
export const queryApi = async (
variables: Admin__Core_Groups__Show_ShortQueryVariables
) => {
const { data } = await fetcher<
Admin__Core_Groups__Show_ShortQuery,
Admin__Core_Groups__Show_ShortQueryVariables
>({
query: Admin__Core_Groups__Show_Short,
variables
});
return data;
};
useQuery
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { APIKeys } from "@/graphql/api-keys";
import { queryApi } from "./query-api";
export const useShortShowGroupsAdminAPI = () => {
const [textSearch, setTextSearch] = useState("");
const api = useQuery({
queryKey: [APIKeys.SHORT_GROUPS_MEMBERS, { textSearch }],
queryFn: async () =>
await queryApi({
first: 25,
search: textSearch
}),
refetchOnMount: true
});
return { ...api, setTextSearch };
};
refetchOnMount
- is used for refetching data when component is mounted
queryKey
have to be unique in your app. The best practise to name it is
module_name.method_name
.
Example use
export const ContentGroupsFiltersUsersMembersAdmin = () => {
const { data, isFetching } = useShortShowGroupsAdmin();
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
{isFetching && <div>Loading...</div>}
</div>
);
};
Deley fetching (Signal)
In Server Actions (opens in a new tab) doesn't exist signal
option. Insted of this you can use useDebouncedCallback
in client side.
Here is an example how to use it for search
input.
import { useDebouncedCallback } from "use-debounce";
const handleSearchInput = useDebouncedCallback((value: string) => {
setSearch(value);
}, 500);
return <Input onChange={e => handleSearchInput(e.target.value)} />;
Don't use prefetch for useQuery. If you need to use prefeth for useQuery that means you should use Server Side Rendering (SSR) fetching.
Infinite Query
Using TanStack Infinite Queries (React Query) (opens in a new tab). You can using Infinite Query for fetching data for infinite scroll.
Create hook
Inside hooks/{module_name}
create new file with prefix use
, for example use-use-topics-list.ts
.
- query-api.ts
- use-use-topics-list.ts
Server Action
Inside query-api.ts
you should create function for fetching data from server using fetcher
function.
"use server";
import { fetcher } from "@/graphql/fetcher";
import {
Forum_Forums__Show_Item_More,
type Forum_Forums__Show_Item_MoreQuery,
type Forum_Forums__Show_Item_MoreQueryVariables
} from "@/graphql/hooks";
export const queryApi = async (
variables: Forum_Forums__Show_Item_MoreQueryVariables
) => {
const { data } = await fetcher<
Forum_Forums__Show_Item_MoreQuery,
Forum_Forums__Show_Item_MoreQueryVariables
>({
query: Forum_Forums__Show_Item_More,
variables
});
return data;
};
useInfiniteQuery
import { useInfiniteQuery } from "@tanstack/react-query";
import { useParams } from "next/navigation";
import { APIKeys } from "@/graphql/api-keys";
import { fetcher } from "@/graphql/fetcher";
import {
Forum_Forums__Show_Item_More,
type Forum_Forums__Show_Item_MoreQuery,
type Forum_Forums__Show_Item_MoreQueryVariables,
type ShowTopicsForumsObj
} from "@/graphql/hooks";
export const useTopicsList = () => {
const { id } = useParams();
const query = useInfiniteQuery({
queryKey: [APIKeys.TOPICS_IN_FORUM, { id }],
queryFn: async ({ pageParam }) =>
await queryApi({
...pageParam,
forumId: getIdFormString(id)
}),
initialPageParam: {
first: 10
},
getNextPageParam: ({ forum_topics__show: { pageInfo } }) => {
if (pageInfo.hasNextPage) {
return {
first: 10,
cursor: pageInfo.endCursor
};
}
}
});
return {
...query
};
};
pageParam
- is used for passing variables to queryinitialPageParam
- is used for passing initial variables to querygetNextPageParam
- is used for passing variables to query when infinite scroll is triggered
Parse map data
export const useTopicsList = () => {
...
const data = useMemo(
() => query.data.pages.flatMap(page => page.forum_topics__show.edges) ?? [],
[query.data.pages]
);
return {
...query,
data
};
};
Example use
export const TopicsList = () => {
const { data, isFetching, fetchNextPage, hasNextPage } = useTopicsList();
return (
<div>
{data.map(item => (
<div key={item.node.id}>{item.node.title}</div>
))}
{isFetching && <div>Loading...</div>}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetching}>
Load more
</button>
)}
</div>
);
};
Prefetch data (SSR)
Compared to useQuery
in useInfiniteQuery
you can use prefetch.
Inside async component you can use prefetchInfiniteQuery (opens in a new tab) function.
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import { ForumsForumAdminView } from "@/admin/views/forum/forums/forums-forum-admin-view";
import getQueryClient from "@/functions/get-query-client";
import { fetcher } from "@/graphql/fetcher";
import { APIKeys } from "@/graphql/api-keys";
import {
Admin__Forum_Forums__Show,
type Admin__Forum_Forums__ShowQuery,
type Admin__Forum_Forums__ShowQueryVariables
} from "@/graphql/hooks";
export default async function Page() {
const queryClient = getQueryClient();
await queryClient.prefetchInfiniteQuery({
queryKey: [APIKeys.FORUMS_ADMIN],
queryFn: async ({ pageParam }) => {
const { data } = await fetcher<
Admin__Forum_Forums__ShowQuery,
Admin__Forum_Forums__ShowQueryVariables
>({
query: Admin__Forum_Forums__Show,
variables: pageParam
});
return data;
},
initialPageParam: {
first: 10
},
getNextPageParam: ({ Admin__Forum_Forums__Show: { pageInfo } }) => {
if (pageInfo.hasNextPage) {
return {
first: 10,
cursor: pageInfo.endCursor
};
}
},
pages: 1
});
const dehydratedState = dehydrate(queryClient);
return (
<HydrationBoundary state={dehydratedState}>
<ForumsForumAdminView />
</HydrationBoundary>
);
}
Remember to use the same queryKey
as in useInfiniteQuery
. Otherwise, the
data will be fetched two times and SSR make no sense.
Prefetch data (Client)
If you has already fetched data you can pass initialData
to useInfiniteQuery
.
const query = useInfiniteQuery({
...
initialData: {
pages: [{ forum_topics__show: initData }],
pageParams: [{ first: 10 }]
}
});
Mutation
NextJS allows you to use Server Actions (opens in a new tab).
Create action
Mutations should be placed in hooks
folder next to client hook with onSubmit
.
"use server";
export const mutationApi = async () => {};
Use fetcher
Our fetcher
should be wrapper with try/catch
block to handle errors using client side.
"use server";
import { fetcher } from "@/graphql/fetcher";
import {
Forum_Topics__Actions__Lock_Toggle,
type Forum_Topics__Actions__Lock_ToggleMutation,
type Forum_Topics__Actions__Lock_ToggleMutationVariables
} from "@/graphql/hooks";
export const mutationApi = async (
variables: Forum_Topics__Actions__Lock_ToggleMutationVariables
) => {
try {
const { data } = await fetcher<
Forum_Topics__Actions__Lock_ToggleMutation,
Forum_Topics__Actions__Lock_ToggleMutationVariables
>({
query: Forum_Topics__Actions__Lock_Toggle,
variables
});
return { data };
} catch (error) {
return { error };
}
};
Example use
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { mutationApi } from "./mutation-api";
export const useLockToggleActionsTopic = ({ id }: { id: number }) => {
const t = useTranslations("core");
const onClick = async () => {
const mutation = await mutationApi({ id });
if (mutation.error) {
toast.error(t("errors.title"), {
description: t("errors.internal_server_error")
});
}
};
return { onClick };
};
toast
- is used for displaying toast messages. You can read more here in Notifications Toast
Revalidate data (SSR)
NextJS allows you to use Revalidating data (opens in a new tab) in Server Actions.
This function only works when you're using Server Side Rendering (SSR) for fetching data.
'use server';
import { revalidatePath } from 'next/cache';
export const mutationApi = async (
) => {
try {
...
revalidatePath('/topic/[id]', 'page');
return { data };
} catch (error) {
return { error };
}
};
Revalidate data (Client)
Take a look at your mutation in Server Action. You have data
variable from fetcher
function and you return it. You can using this data to change data in cache using queryClient.setQueryData (opens in a new tab).
import {
useQuery,
useQueryClient,
type InfiniteData
} from "@tanstack/react-query";
import { APIKeys } from "@/graphql/api-keys";
import { type Forum_Topics__ShowQuery } from "@/graphql/hooks";
const queryClient = useQueryClient();
const onClick = async () => {
const mutation = await mutationApi({ id });
if (mutation.data) {
queryClient.setQueryData<InfiniteData<Forum_Topics__ShowQuery>>(
[APIKeys.TOPICS],
old => {
if (!old) return old;
return {
...old,
pages: old.pages.map(page => ({
...page,
forum_topics__show: {
...page.forum_topics__show,
edges: mutation.data
}
}))
};
}
);
return;
}
toast.error(t("errors.title"), {
description: t("errors.internal_server_error")
});
};
Combine Methods
You don't need to use only one method. You can combine them.
For example:
- Fetching first page for infinite query using Server Side Rendering (SSR)
Debugging for React Query
To enable debugging for React Query add NEXT_PUBLIC_DEBUG=true
to .env
in frontend folder.