Next 14 With React Query
Answered
Oriental chestnut gall wasp posted this in #help-forum
Oriental chestnut gall waspOP
https://www.youtube.com/watch?v=9kjc6SWxBIA
I'm following this tutorial to get live updates for my app but I have some questions. I have a page with a list of posts and it works just as shown. However, my page also has a header and member list that could potentially change. Does it make sense to have individual hooks, query clients, and hydration boundaries for each component? I would think that would cause more calls to the db than if I were to just get my page data once and pass it down as props to everything. I know one of the listed advantages to this approach was that you don't need to pass props, but are we not sacrificing performance by having all these separate calls (especially since it's happening every 6 seconds)?
I'm following this tutorial to get live updates for my app but I have some questions. I have a page with a list of posts and it works just as shown. However, my page also has a header and member list that could potentially change. Does it make sense to have individual hooks, query clients, and hydration boundaries for each component? I would think that would cause more calls to the db than if I were to just get my page data once and pass it down as props to everything. I know one of the listed advantages to this approach was that you don't need to pass props, but are we not sacrificing performance by having all these separate calls (especially since it's happening every 6 seconds)?
Answered by aardani
@Oriental chestnut gall wasp whats wrong with putting everything in a single query?
172 Replies
However, my page also has a header and member list that could potentially change. Does it make sense to have individual hooks, query clients, and hydration boundaries for each component?Yes
I would think that would cause more calls to the db than if I were to just get my page data once and pass it down as props to everything.Not if you properly dedupe them both in server and the client.
@Oriental chestnut gall wasp
first tips: cache your query client
const getQC = cache(() => new QueryClient())second tips: cache your db calls
const getPostFromSerber = cache(async ()=> await db.findMany(...))Use
cache from React. This is a way to dedupe calls in the server of a single request.So it make sure you only call the function once per request (even after calling them 100 times)
Oriental chestnut gall waspOP
So if I cached the result of a prisma findunique, any findunqiue made within a certain period would use that cached value, or any findunique with the same parameters?
I’m a bit confused as to how that works
@aardani
no it has nothing to do with time
the cache dedupes function within A NEXTJS SINGLE REQUEST
so if a single request to
- header
- footer
- content
and each of those compoent calls
/dashboard renders- header
- footer
- content
and each of those compoent calls
getData, it will only get called once, instead of 3 timesits not time based, but scoped within one single request
Oriental chestnut gall waspOP
I see, and how does that work for when the cache invalidates from react query
theres no invalidations
Oriental chestnut gall waspOP
I have it set to revalidate after 6 seconds in my query provider
the cache from react is different from cache of react query
Oriental chestnut gall waspOP
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 6,
refetchInterval: 1000 * 6,
},
},
})
);The cache from react is purged after we sent out a response to the client. And only by then the ClientQuery() takes care of the client-side cache
the cache from react simply means to dedupe 6 calls that you mentioned, to one single, efficient calls.
Recall the concept of partial rendering. If your header is in layout, it wont get rerendered, on soft navigation. Therefore, no server call is made to rerender <header>
Oriental chestnut gall waspOP
so as an example, I have these two components
The post feed already has this strategy in effect.
here's the hook
and here's the server action
I know it can definitely be cleaned up, but how would I go about doing the same for the member list using this caching stragegy to dedupe the calls?
<div className="max-w-2xl mx-auto flex flex-col gap-4">
<MemberList members={members} />
<HydrationBoundary state={dehydrate(queryClient)}>
<PostFeed eventId={eventId} />
</HydrationBoundary>
</div>The post feed already has this strategy in effect.
post-feed.tsx
export default function PostFeed({ eventId }: { eventId: string }) {
const { data: postData } = useGetPosts(eventId);
if (postData?.error) {
return <h1>{postData.error}</h1>;
}
if (postData?.success) {
const {
posts,
isMod,
userId,
}: { posts: PostWithAuthorInfo[]; isMod: boolean; userId: string } =
postData.success;
return (
<div>
<h2 className="text-xl font-heading">Posts</h2>
<div className="w-full flex flex-col items-center gap-3 py-2">
{posts
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
.map((post) => (
<PostCard
userId={userId}
isMod={isMod}
key={post.id}
post={post}
/>
))}
</div>
</div>
);
}
}here's the hook
export function useGetPosts(eventId: string) {
return useQuery({
queryFn: async () => fetchPosts(eventId),
queryKey: ["posts", eventId],
});
}and here's the server action
export async function fetchPosts(eventId: string) {
const event = await db.event.findUnique({
where: {
id: eventId
},
include: {
posts: {
include: {
replies: true
}
},
memberships: true
}
});
if (!event) return {error: "Event not found"}
const { userId }: { userId: string | null } = auth();
if (!userId) return {error: "User not found"}
const userRole = event.memberships.find((membership) => (membership.personId === userId ))?.role;
if(!userRole) return {error: "Role not found"}
const isMod = ["MODERATOR", "ORGANIZER"].includes(userRole);
const membershipUsers = await clerkClient.users.getUserList({
userId: event.memberships.map((membership) => membership.personId),
});
let { posts }: { posts: PostWithAuthorInfo[] } = event;
posts = posts.map((post) => {
const author = membershipUsers.find(
(author) => author.id === post.authorId
);
if (author) {
post.authorInfo = {
firstName: author.firstName,
lastName: author.lastName,
username: author.username,
avatar: author.imageUrl,
};
}
return {
...post,
};
});
if (!posts) return {error: "No posts"}
if (posts) return {success: {posts, isMod, userId}}
}I know it can definitely be cleaned up, but how would I go about doing the same for the member list using this caching stragegy to dedupe the calls?
export const fetchPosts = cache(async () => {
... put the fetch post logic here
})just like how i told you
also that the queryClient can be cached too so that you only create ONE QUERY CLIENT PER REQUEST
Oriental chestnut gall waspOP
And then do the same for the member list?
export const fetchMembers = cache(async () => {
... put the fetch post logic here
})yep
Oriental chestnut gall waspOP
Ok I see. That's definitely less involved than I had assumed
that makes sures, even if i do this
fetchMembers() in Header
fetchMembers() in Component
fetchMembers() in Sub-Sub-Sub-Server Component
ALL in 3 different component, it will be called once
This is DEDUPING in the SERVER. (so have nothing to do with React Query)
fetchMembers() in Header
fetchMembers() in Component
fetchMembers() in Sub-Sub-Sub-Server Component
ALL in 3 different component, it will be called once
This is DEDUPING in the SERVER. (so have nothing to do with React Query)
Oriental chestnut gall waspOP
That's super neat!
Thanks so much for your help
Note that this is scoped to a SINGLE REQUEST
so if you make another request, and you only call fetchMembers() in sub-sub-sub server component, it will still be called once
and those result will still get passed through the hydration boundaries
As of now React Query doesn't have a way to determine which to prioritise the data but IIRC all data passed to HydrationBoundary will override the client query just because its newer
Oriental chestnut gall waspOP
What do you mean by single request in the context of it updating in real time due to the provider?
Like when does it become 2 requests
When user go to
/dashboard, thats a single nextjs request
When NAVIGATES to (no hard refresh)
/dashboard/a, thats another single nextjs request. Next.js will render /dashboard/a/page.tsx, but not /dashboard/layout.tsx
/dashboard, thats a single nextjs request
When NAVIGATES to (no hard refresh)
/dashboard/a, thats another single nextjs request. Next.js will render /dashboard/a/page.tsx, but not /dashboard/layout.tsx
Oriental chestnut gall waspOP
so if I stay on the same page and don't touch anything, that is one request?
yep, unless provoked by the client-side ofcourse. But Im just defining the context
Oriental chestnut gall waspOP
Right now, if another user posts something, it will pop up within 6 seconds on my end
due to the refetching
will that still work
Yep, but thats because of RQ, not because you navigate from A -> B and made a Next.js Request to render new page
nor because you call router.refresh()
Oriental chestnut gall waspOP
alright
data fetching still gives me such a headache, but I'm trying my best haha
Also
you realize that when you useQuery in the server side and client side
you have to provide 2 queryFn
one to prefetch in the server,
and another one to re-fetch on the client
Oriental chestnut gall waspOP
they can't be the same function?
They cant. Prisma doesn't work in the client-side. Unless you made a seperate Endpoint to call to the server to invoke prisma from the client
in this case Server Actions or Route Handlers
If you use ServerAction to fetch both in server and client i guess thats okay but notice that its 2 separate process
Oriental chestnut gall waspOP
Right now I have
in my server component and
in my hook
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["posts", eventId],
queryFn: () => fetchPosts(eventId),
});in my server component and
export function useGetPosts(eventId: string) {
return useQuery({
queryFn: async () => fetchPosts(eventId),
queryKey: ["posts", eventId],
});
}in my hook
You can check it yourself, howmany times
fetchPosts will be called in the serverOriental chestnut gall waspOP
I copied this structure from the video that I posted originally
if you use useGetPosts() 10 times, RQ will dedupe the request to the server into 1 request.
But if somehow you have
useGetPosts()
useNotes()
useComments()
useUsers()
And every 6 seconds those 4 hooks refetches, it will make 4 distinct request to the server
But if somehow you have
useGetPosts()
useNotes()
useComments()
useUsers()
And every 6 seconds those 4 hooks refetches, it will make 4 distinct request to the server
Oriental chestnut gall waspOP
that's what I want to avoid
How can I prevent that
Well, make one hook that fetches all of them at once :v
and use useQuery's select prop to cut the cake into smaller cake pieces (in the client) (but dont provide the queryFn)
Oriental chestnut gall waspOP
so what would the hydration boundary be around? where would the hook be called?
would it be used in each component
so each one would have like useEventData() and there would be 3 instances of it
I prever having HydrationBoundary put in every component you need to pass from server to client
HydrationBoundary can be nested so dont worry about that
@Oriental chestnut gall wasp so each one would have like useEventData() and there would be 3 instances of it
no, there would still be 1 instance. But 3 listeners
Oriental chestnut gall waspOP
so in this example...
<div className="max-w-2xl mx-auto flex flex-col gap-4">
<MemberList members={members} />
<HydrationBoundary state={dehydrate(queryClient)}>
<PostFeed eventId={eventId} />
</HydrationBoundary>
</div> <div className="max-w-2xl mx-auto flex flex-col gap-4">
<HydrationBoundary state={dehydrate(queryClient)}>
<MemberList members={members} />
<PostFeed eventId={eventId} />
</HydrationBoundary>
</div>this?
Yeah
it will hydrate whatever you put in
queryClient. All prefetches at that given momentOriental chestnut gall waspOP
can I have a second one elsewhere if I need it?
Yeah
Oriental chestnut gall waspOP
ok
But you still need to rewrite the .prefetch() again
Oriental chestnut gall waspOP
and they all use 1 query client or no?
In the server if you cache it, yes
In the client, you only have one due to only having one <Provider>
the HydrationBoundary gets all of the stuff you prefetch in the server, bundle it up, and spread it into the client-side queryClient()
Oriental chestnut gall waspOP
so right now I have
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["posts", eventId],
queryFn: () => fetchPosts(eventId),
});caching it would be something like
like you said earlier
const getQC = cache(() => new QueryClient())like you said earlier
yeah
getQC or getQueryClient
its in the documentation
Then if you want to combine all into one single query, i would make queryKey to be: ['data'] or something
Oriental chestnut gall waspOP
do I need more then one .prefetchQuery call?
if I'm only gonna be using one hook?
also if I am using the getQC instead of just making a new query client, when does that actually get called?
// serber
await queryClient.prefetchQuery({
queryKey: ["data"],
queryFn: () => await fetchData(eventId),
});
// client
const dataQuery = useQuery({
queryKey: ["data"],
queryFn: () => await fetchData(eventId)
})@Oriental chestnut gall wasp also if I am using the getQC instead of just making a new query client, when does that actually get called?
whoever calls getQC first, will create new QueryClient() first. The rest will use the memoized result.
@Oriental chestnut gall wasp do I need more then one .prefetchQuery call?
you can call this in one or more page.js depending whether or not you want to refetch when user navigates from /A to /B (or any child semgent routes) to get new data, hydrates them back to the client
Oriental chestnut gall waspOP
In this case there's only one page.tsx I need to handle
So if
user visits /dashboard
calls fetchData() in 3 separate server compoennts, dedupe into 1 call (using React's cache())
page is rendered
RQ is activated, wait for 6 seconds
after 6 seconds then 3 useData() will dedupe queryFn into one request. (using React Query built-in dedupe feature)
the server will only receive one request only.
calls fetchData() once and return the server action.
User navigates from /dashboard to /dashboard/post
calls fetchData() in 2 separate components, deduped into 1 call. (using React's cache())
Its not yet 6 seconds but since you call prefetchQuery in /dashboard/post/page.js,
it will be hydrated and override the client query and start from 0 second again (i assume)
then wait for 6 seconds, then the whole cycle repeats
user visits /dashboard
calls fetchData() in 3 separate server compoennts, dedupe into 1 call (using React's cache())
page is rendered
RQ is activated, wait for 6 seconds
after 6 seconds then 3 useData() will dedupe queryFn into one request. (using React Query built-in dedupe feature)
the server will only receive one request only.
calls fetchData() once and return the server action.
User navigates from /dashboard to /dashboard/post
calls fetchData() in 2 separate components, deduped into 1 call. (using React's cache())
Its not yet 6 seconds but since you call prefetchQuery in /dashboard/post/page.js,
it will be hydrated and override the client query and start from 0 second again (i assume)
then wait for 6 seconds, then the whole cycle repeats
---
if you dont prefetch in /dashboard/post/page.js,
then if user DIRECTLY goes to /dashboard/post via hard refresh,
then theres no SSR on /dashboard/post/page.js resulting in no SEO. Since you relied on client query at client-side to get the data from useData
if you directly pass data from server component to client component in page.js without prefetch, then your post wont get updated since theres no client-component that takes care of the automatic updating :/
then if user DIRECTLY goes to /dashboard/post via hard refresh,
then theres no SSR on /dashboard/post/page.js resulting in no SEO. Since you relied on client query at client-side to get the data from useData
if you directly pass data from server component to client component in page.js without prefetch, then your post wont get updated since theres no client-component that takes care of the automatic updating :/
Oriental chestnut gall waspOP
going to /post presumably wont use the same fetch function since it is just querying a single post, though I guess it could?
well sure just bear in mind that RQ cant automatically "stitch" relevant data.
So if you have post data in
So if you have post data in
getData and you also have individual post data in getPost you have to "link" them somehow and update accordingly...so that they wont go out of sync
Oriental chestnut gall waspOP
So do you think I should just be using this one getData call for basically everything then?
Its hard to tell,
honestly, i would just allow multiple fetches for now
honestly, i would just allow multiple fetches for now
Oriental chestnut gall waspOP
For reference, here is my schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
model Person {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
memberships Membership[]
events Event[]
posts Post[]
replies Reply[]
}
model Event {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
owner Person @relation(fields: [ownerId], references: [id], onDelete: Cascade)
ownerId String
title String
description String
location String
chosenDateTime DateTime?
potentialDateTimes PotentialDateTime[]
posts Post[]
memberships Membership[]
}
model Membership {
id String @id @default(cuid())
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
personId String
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
role Role
rsvpStatus Status
availabilities Availability[]
}
model PotentialDateTime {
id String @id @default(cuid())
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
time DateTime @default(now())
availabilities Availability[]
}
model Availability {
membership Membership @relation(fields: [membershipId], references: [id], onDelete: Cascade)
membershipId String @unique
potentialDateTime PotentialDateTime @relation(fields: [potentialDateTimeId], references: [id], onDelete: Cascade)
potentialDateTimeId String
status Status
@@id([membershipId, potentialDateTimeId])
}
model Post {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
author Person @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
replies Reply[]
title String @db.VarChar(100)
content String @db.VarChar(3000)
}
model Reply {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
author Person @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
text String
}
enum Status {
YES
MAYBE
NO
}
enum Role {
ORGANIZER
MODERATOR
ATTENDEE
}atlesat im not fetching the same thing 4 times
Oriental chestnut gall waspOP
The problem is, I kind am
export async function fetchPosts(eventId: string) {
const event = await db.event.findUnique({
where: {
id: eventId
},
include: {
posts: {
include: {
replies: true
}
},
memberships: true
}
});
if (!event) return {error: "Event not found"}
const { userId }: { userId: string | null } = auth();
if (!userId) return {error: "User not found"}
const userRole = event.memberships.find((membership) => (membership.personId === userId ))?.role;
if(!userRole) return {error: "Role not found"}
const isMod = ["MODERATOR", "ORGANIZER"].includes(userRole);
const membershipUsers = await clerkClient.users.getUserList({
userId: event.memberships.map((membership) => membership.personId),
});
let { posts }: { posts: PostWithAuthorInfo[] } = event;
posts = posts.map((post) => {
const author = membershipUsers.find(
(author) => author.id === post.authorId
);
if (author) {
post.authorInfo = {
firstName: author.firstName,
lastName: author.lastName,
username: author.username,
avatar: author.imageUrl,
};
}
return {
...post,
};
});Ill get back to you once i have a more concrete examples
Oriental chestnut gall waspOP
Because I need to know if the person fetching owns a given post or is a mod and get data from clerk, I need to get the whole event along with everyone in it
So this fetchPosts function is really getting a lot of data from the event
Oriental chestnut gall waspOP
@aardani if it would be helpful, I could try to share my project for context?
idk if that's necessary or not though
@Oriental chestnut gall wasp idk if that's necessary or not though
Im am full aware of the context
But i dont have a fully polished answer
So its me not you xD
Theres no use in giving me more details if i cant answer at a fundamental level
Oriental chestnut gall waspOP
Ok that's fine!
@Oriental chestnut gall wasp whats wrong with putting everything in a single query?
Answer
subscribing to a single data
@aardani <@139184819090227201> whats wrong with putting everything in a single query?
Oriental chestnut gall waspOP
And this approach would benefit from the deduping you were talking about while letting me call it from each component and get the specific data I need?
not from RQ, nor React, its by design, like because its set up that way
Oriental chestnut gall waspOP
Ok that seems like it should work then. Instead of just passing the useTodosQuery, can I include brackets, call it in the other hooks, and do some specific data manipulation for each one in the hook?
I don’t see why not
Oriental chestnut gall waspOP
Update: I think I got it all working. I see the live updates for each component and my network tab only shows 1 request every 6 seconds!
The last thing I want to make sure of is that I am actually rendering everything server side initially. @aardani what's the simplest way to check this?
Oriental chestnut gall waspOP
I'm like 99% sure it's working cause when I remove my prefetch, it says "header data is undefined"
Though I'm currently not caching the query client
I'm just defining it once
Which is probably fine since I don't need it across multiple pages
Oriental chestnut gall waspOP
I just realized I think I'm doing two fetches when the page loads since I need to verify if a user is in an event. How can I simplify this?
export default async function Page({
params,
}: {
params: { eventId: string };
}) {
const { eventId } = params;
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["eventData"],
queryFn: async () => fetchEventData(eventId),
});
const event = await db.event.findUnique({
where: {
id: eventId,
},
include: {
memberships: { include: { person: true } },
posts: {
include: { replies: true },
},
},
});
if (!event) {
notFound();
}
const { userId }: { userId: string | null } = auth();
if (!userId) {
throw new Error();
}
if (!event.memberships.some((membership) => membership.personId === userId)) {
throw new Error("You are not a member of this event");
}
return (
<div className="container pt-6 pb-24 space-y-5">
<HydrationBoundary state={dehydrate(queryClient)}>
<EventHeader eventId={eventId} />
</HydrationBoundary>
<div className="max-w-2xl mx-auto flex flex-col gap-4">
<HydrationBoundary state={dehydrate(queryClient)}>
<MemberList eventId={eventId} />
<PostFeed eventId={eventId} />
</HydrationBoundary>
</div>
<NewPostButton />
</div>
);
}Oriental chestnut gall waspOP
I know I can handle those cases in my data fetch and just pass the error to the page, but do I call fetchEventData or can I get that data from the query client somehow after the prefetchQuery
Oriental chestnut gall waspOP
It seems like prefetchQuery just puts the result of the query into the cache so it's not like I can use that data right away, but I'm not sure how to have the query result available so I can throw an error if the user shouldn't have access to the page before it renders and also have that query cached
In this case do I not need the prefetchQuery?
could I just call my server action directly and would that work?
Oriental chestnut gall waspOP
I simplified it to this, but idk if it could be better
export default async function Page({
params,
}: {
params: { eventId: string };
}) {
const { eventId } = params;
const eventResponse = await fetchEventData(eventId);
if (eventResponse.error) {
throw new Error(eventResponse.error);
}
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["eventData"],
queryFn: async () => fetchEventData(eventId),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="container pt-6 pb-24 space-y-5">
<EventHeader eventId={eventId} />
<div className="max-w-2xl mx-auto flex flex-col gap-4">
<MemberList eventId={eventId} />
<PostFeed eventId={eventId} />
</div>
<NewPostButton />
</div>
</HydrationBoundary>
);
}Oriental chestnut gall waspOP
This still seems to do the query twice :/
Oriental chestnut gall waspOP
Update: I figured it out!!
export default async function Page({
params,
}: {
params: { eventId: string };
}) {
const { eventId } = params;
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["eventData"],
queryFn: async () => fetchEventData(eventId),
});
const data: EventData | undefined =
queryClient.getQueryData(["eventData"]);
if (!data) {
notFound();
}
if (data.error) {
throw new Error(data.error);
}
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="container pt-6 pb-24 space-y-5">
<EventHeader eventId={eventId} />
<div className="max-w-2xl mx-auto flex flex-col gap-4">
<MemberList eventId={eventId} />
<PostFeed eventId={eventId} />
</div>
<NewPostButton />
</div>
</HydrationBoundary>
);
}this seems to be exactly what I was looking for
and see the response of the first request
Oriental chestnut gall waspOP
it looks kinda blank
the one above it is actually blank
@aardani
wwait
wait
the actual html has more
yeah it has post data in it
which means it has to be rendered on the server
Oriental chestnut gall waspOP
yeah
The html has the bodies of the posts
if you dedupe your
fetchEventData() then calling twice is not a problem, remember :v@aardani just put the DB result directly in queryFn
const eventData = await getEventData(eventID)
queryFn: () => eventData
Oriental chestnut gall waspOP
oh this is cleaner than my approach
just when I thought I figured it out haha
well all three ways of yours is valid
just needed to dedupe it
Oriental chestnut gall waspOP
yeah, I do like the
approach more though just from a cleanliness perspective
const eventData = await getEventData(eventID)
queryFn: () => eventDataapproach more though just from a cleanliness perspective
@Oriental chestnut gall wasp Ok that seems like it should work then. Instead of just passing the useTodosQuery, can I include brackets, call it in the other hooks, and do some specific data manipulation for each one in the hook?
yes, in the example above, the
useTodosCount and useTodo hooks uses the original useTodosQuery to only "select" part of the data.and this already comes with structural sharing
meaning if the object is updated and nothing changes, those smaller hooks wont be rerendered
Oriental chestnut gall waspOP
nice! my code is looking a lot better than before
Ok seems like the original query is answered
Feel free to open another forum post if you have any more questions or helps