optimistic update for complex data structure

I am making a recipe website with the t3-stack and I made something that "works" but is not very fast. I am kind of new to web dev with the t3 stack and I need some help
112 Replies
SharpieMaster
SharpieMasterOP•14mo ago
this is my prisma schema
SharpieMaster
SharpieMasterOP•14mo ago
This is kind of the structure I want, but I want it to be optimistic how would I do this
No description
SharpieMaster
SharpieMasterOP•14mo ago
SharpieMaster
SharpieMasterOP•14mo ago
this is how I started but I dont know what I'm doing wrong... and I want to learn when to use query vs mutation each recipe has a title and desc and a collection of steps and ingredients, and to instantly add the new steps or ingredients please help y no one help me poohheh
tomoS
tomoS•14mo ago
Need bit more details here … slow doing what ? Is it initial page load, anything that’s to do with making API calls, or just unreal when you click on buttons? I’m suspecting the last one, in that case it’s usually unnecessary re-rendering happening with useEffect or sth…
SharpieMaster
SharpieMasterOP•14mo ago
I mean the time it takes for the api call to update the data, since I mostly test on school wifi the delay is unbearable. I want it to be optimistic if you want to see the original recipe form, please tell me (its a mess) for this one I followed the optimistic data code snippet on t3 but I dont exactly understand it i tried looking for videos explaining it but I dont know how to apply it to my own use case
ArjunBroEpic
ArjunBroEpic•14mo ago
Medium
Creating Optimistic Updates in UI with React.js’s
Optimistic updates are a powerful technique in modern web development, especially when it comes to improving the user experience. By…
SharpieMaster
SharpieMasterOP•14mo ago
id rather not use useOptimistic, and im not usnig server actions, I am using trpc + app router
tomoS
tomoS•14mo ago
Got you, what is your backend? Assuming you are using prisma and calling planescale or sth?
SharpieMaster
SharpieMasterOP•14mo ago
I am using prisma + planetscale and trpc, thats what I know of
SharpieMaster
SharpieMasterOP•14mo ago
No description
SharpieMaster
SharpieMasterOP•14mo ago
No description
tomoS
tomoS•14mo ago
Right… have you checked if you are making a connection every time you make a call?
SharpieMaster
SharpieMasterOP•14mo ago
this is generated with the create-t3-app
No description
SharpieMaster
SharpieMasterOP•14mo ago
and this is how it is used
No description
tomoS
tomoS•14mo ago
Looks good to me ..
SharpieMaster
SharpieMasterOP•14mo ago
I mean that shouldnt be an issue since I didnt touch it
tomoS
tomoS•14mo ago
When you say slow, is it like a few seconds for 300ms+?
SharpieMaster
SharpieMasterOP•14mo ago
SharpieMaster
SharpieMasterOP•14mo ago
I am doing this locally but on a vercel deployment its a bit slower, and the way I do it makes it inconsistent it said there were 15 but actually there were 16
SharpieMaster
SharpieMasterOP•14mo ago
No description
SharpieMaster
SharpieMasterOP•14mo ago
yk what forget it
tomoS
tomoS•14mo ago
It looks quite snappy to me … 😅
SharpieMaster
SharpieMasterOP•14mo ago
yes but not instant, i'll show you an example real quick of what I mean
SharpieMaster
SharpieMasterOP•14mo ago
SharpieMaster
SharpieMasterOP•14mo ago
you can see the number updating instantl, while the state of the mutation is still loading
SharpieMaster
SharpieMasterOP•14mo ago
No description
SharpieMaster
SharpieMasterOP•14mo ago
No description
tomoS
tomoS•14mo ago
Right so it is optimistic update then .
SharpieMaster
SharpieMasterOP•14mo ago
yes
tomoS
tomoS•14mo ago
It looks like you can use this same pattern? Instead of onSuccese, update the state onMutate and validate the state again onSettled and re-render ui if it failed
SharpieMaster
SharpieMasterOP•14mo ago
No description
SharpieMaster
SharpieMasterOP•14mo ago
this is how Ive tried to do it just for the title but it doesnt work
SharpieMaster
SharpieMasterOP•14mo ago
No description
SharpieMaster
SharpieMasterOP•14mo ago
No description
SharpieMaster
SharpieMasterOP•14mo ago
this is the collapsed thing btw
tomoS
tomoS•14mo ago
What’s this await call? That’s blocking for sure, and you are getting prevRecipie from backend . Probably better to hold that state within the component ..?
SharpieMaster
SharpieMasterOP•14mo ago
im doing the same thing like here
tomoS
tomoS•14mo ago
Ok.. doesn’t async await just defeat the purpose of onMutate? You don’t want to block any process with await but just to run the process as fast as it can..?
SharpieMaster
SharpieMasterOP•14mo ago
bro, idk what im doing, i just coppied from this age, https://create.t3.gg/en/usage/trpc#optimistic-updates
Create T3 App
tRPC 🚀 Create T3 App
The best way to start a full-stack, typesafe Next.js app.
Josh
Josh•14mo ago
Why are you using onMutate here? You shouldn't need this at all
SharpieMaster
SharpieMasterOP•14mo ago
idk bro
tomoS
tomoS•14mo ago
Haha , I’m not react query wizard either so idk either …
SharpieMaster
SharpieMasterOP•14mo ago
lol
Josh
Josh•14mo ago
You need to go do a lot more research and effort into learning this stuff yourself Go read the docs on trpc mutation ls, optimistic queries, etc. I see where you got that code from, but you're using it correctly to do what you want. We also can't see your entire component so it's impossible to know what's actually going on
SharpieMaster
SharpieMasterOP•14mo ago
that is the entrie component
SharpieMaster
SharpieMasterOP•14mo ago
No description
SharpieMaster
SharpieMasterOP•14mo ago
oh you mean the old one its too large to send as text message or image if not use mutations how do I onClick call backend useQuery must be in body of component
Josh
Josh•14mo ago
If you want to manually trigger a query, use a mutation If you want the query to run automatically, use query
tomoS
tomoS•14mo ago
Can I also c utils.recipie function?…
SharpieMaster
SharpieMasterOP•14mo ago
its a trpc route yes, thats why im using mutation
Josh
Josh•14mo ago
If you're okay with a query being called automatically and want to also manually trigger it, then yes you can use a utils function Also You're using useEffect horribly wrong
SharpieMaster
SharpieMasterOP•14mo ago
how so
Josh
Josh•14mo ago
Go read the documentation on useEffect
SharpieMaster
SharpieMasterOP•14mo ago
ok but how am I using it wrong how else would I update the title when the recipeData loads
Josh
Josh•14mo ago
Your using it to set state from a stateful action Put it in the onSuccess of the query
tomoS
tomoS•14mo ago
Where are you updating a state of recipieData? Can’t seem to find here
SharpieMaster
SharpieMasterOP•14mo ago
No description
Josh
Josh•14mo ago
Yep This is as instant as your going to get btw
SharpieMaster
SharpieMasterOP•14mo ago
SharpieMaster
SharpieMasterOP•14mo ago
ik
Josh
Josh•14mo ago
Oop, not that video but you know what I mean
SharpieMaster
SharpieMasterOP•14mo ago
yep but even without the use effect that doesnt explain why the update is not wokring
Josh
Josh•14mo ago
Wym not working
tomoS
tomoS•14mo ago
Wld it make sense to run setTitle () under onMutate ?
SharpieMaster
SharpieMasterOP•14mo ago
No description
Josh
Josh•14mo ago
No cause he's doing that anyways with ... recipes
SharpieMaster
SharpieMasterOP•14mo ago
when I press the button it should run the updateRecipe
SharpieMaster
SharpieMasterOP•14mo ago
No description
tomoS
tomoS•14mo ago
But isn’t he making a TRPC call and call backend to block the process before he returns value from onMutate? Thought that’s where the delay is from …
SharpieMaster
SharpieMasterOP•14mo ago
this is the new one the old one is different and is what I showed in the video with the delay delay is just server delay
Josh
Josh•14mo ago
Bruh your going to need to learn how to debug
SharpieMaster
SharpieMasterOP•14mo ago
im explaining to @omotto
Josh
Josh•14mo ago
No I'm talking about in general
SharpieMaster
SharpieMasterOP•14mo ago
ok
Josh
Josh•14mo ago
Start putting in some console logs, see what you're backend is getting back, verify your backend is working as Intended, and just keep narrowing down the problem You want to figure out what little piece is not doing what you expect Rn we have no idea if your server is getting the wrong data, setting the wrong data, returning the wrong data, etc All of which you can figure out trivialy
SharpieMaster
SharpieMasterOP•14mo ago
No description
SharpieMaster
SharpieMasterOP•14mo ago
the backend is working as it did work with the previous version of the recipe form, this is causing the problem
No description
Josh
Josh•14mo ago
There is your answer to this whole thread Your not passing what key you want the data from You don't never need that though lmao
tomoS
tomoS•14mo ago
lol …
Josh
Josh•14mo ago
You already have it
SharpieMaster
SharpieMasterOP•14mo ago
is there not supposed to be a type error
Josh
Josh•14mo ago
Don't use it regardless You literally have the data like 3 lines up
SharpieMaster
SharpieMasterOP•14mo ago
works flawlesly now
SharpieMaster
SharpieMasterOP•14mo ago
No description
Josh
Josh•14mo ago
recipieData IS previousRecipe
SharpieMaster
SharpieMasterOP•14mo ago
yea
Josh
Josh•14mo ago
Okay still don't use that lmao
SharpieMaster
SharpieMasterOP•14mo ago
what else do you recommend I use
Josh
Josh•14mo ago
Directly reference recipe data
SharpieMaster
SharpieMasterOP•14mo ago
?
tomoS
tomoS•14mo ago
That’s what I’ve been trying to say too ! lol
Josh
Josh•14mo ago
🤦 Previous data === recipe data So don't define a previous data variable Don't call your getdata function Just replace previousRecipe with recipeData
SharpieMaster
SharpieMasterOP•14mo ago
prev recipe is just for the id userId and opened at so that typescript doesnt yell at me
Josh
Josh•14mo ago
My brother in Christ Send your onMutate as a code snippet Please for the love of all things good stop sending code screenshots And send inline code snippets
SharpieMaster
SharpieMasterOP•14mo ago
const updateRecipe = api.recipe.update.useMutation({
onMutate: async (recipe) => {
await utils.recipe.getQ.cancel();

const newRecipe = {
...recipe,
id: recipeData!.id,
userId: recipeData!.userId,
openedAt: recipeData!.openedAt,
};

console.log(newRecipe);

utils.recipe.getQ.setData({ id: recipeId }, newRecipe);

return { recipeData };
},

onSettled: () => {
utils.recipe.getQ.invalidate();
},
});
const updateRecipe = api.recipe.update.useMutation({
onMutate: async (recipe) => {
await utils.recipe.getQ.cancel();

const newRecipe = {
...recipe,
id: recipeData!.id,
userId: recipeData!.userId,
openedAt: recipeData!.openedAt,
};

console.log(newRecipe);

utils.recipe.getQ.setData({ id: recipeId }, newRecipe);

return { recipeData };
},

onSettled: () => {
utils.recipe.getQ.invalidate();
},
});
Josh
Josh•14mo ago
There you go You got it now
SharpieMaster
SharpieMasterOP•14mo ago
but I need to keep the prevRecipe if an error occurs to rollback since the getQ.setData() updates the recipeData
Josh
Josh•14mo ago
You still are No
SharpieMaster
SharpieMasterOP•14mo ago
in the cache
Josh
Josh•14mo ago
Nope Try it Your still returning the old data This is where you need to read react documentation and understand what's going on under the hood
SharpieMaster
SharpieMasterOP•14mo ago
const MyComponent = () => {
const listPostQuery = api.post.list.useQuery();

const utils = api.useContext();
const postCreate = api.post.create.useMutation({
async onMutate(newPost) {
// Cancel outgoing fetches (so they don't overwrite our optimistic update)
await utils.post.list.cancel();

// Get the data from the queryCache
const prevData = utils.post.list.getData();

// Optimistically update the data with our new post
utils.post.list.setData(undefined, (old) => [...old, newPost]);

// Return the previous data so we can revert if something goes wrong
return { prevData };
},
onError(err, newPost, ctx) {
// If the mutation fails, use the context-value from onMutate
utils.post.list.setData(undefined, ctx.prevData);
},
onSettled() {
// Sync with server once mutation has settled
utils.post.list.invalidate();
},
});
};
const MyComponent = () => {
const listPostQuery = api.post.list.useQuery();

const utils = api.useContext();
const postCreate = api.post.create.useMutation({
async onMutate(newPost) {
// Cancel outgoing fetches (so they don't overwrite our optimistic update)
await utils.post.list.cancel();

// Get the data from the queryCache
const prevData = utils.post.list.getData();

// Optimistically update the data with our new post
utils.post.list.setData(undefined, (old) => [...old, newPost]);

// Return the previous data so we can revert if something goes wrong
return { prevData };
},
onError(err, newPost, ctx) {
// If the mutation fails, use the context-value from onMutate
utils.post.list.setData(undefined, ctx.prevData);
},
onSettled() {
// Sync with server once mutation has settled
utils.post.list.invalidate();
},
});
};
this is the snippet that is on the create.t3.gg website and which I followed
tomoS
tomoS•14mo ago
Or console.log everywhere to understand what’s happening 😅
Josh
Josh•14mo ago
Lol When you call setData, it's calling a setState under the hood So recipe data is changed NEXT RENDER not during the current execution of your function
SharpieMaster
SharpieMasterOP•14mo ago
oh yeaaa
Josh
Josh•14mo ago
The reason those tutorials use the getData method is that their assuming you have the state at a higher level in the component tree and don't actually have access to it directly like you do Or/and because it's much simpler to show
SharpieMaster
SharpieMasterOP•14mo ago
so is this how I would go about implementing adding and updating steps and ingredients?
Josh
Josh•14mo ago
Now, since you're using your current renders data, you don't even need to use the await. You can just use void and let that whole chain run faster I suppose yeah Ehhhhh Actually scratch that You still need the await since it's technically a race condition
SharpieMaster
SharpieMasterOP•14mo ago
imma return to this thread when I have more time to implement the steps, and after that adding ingredients is just copy-paste thx for the help and explanations @Josh @omotto
const newStep = api.step.create.useMutation({
onSuccess: (s) => {
if (s) {
setSteps([...steps, s]);
toast.success("Step created");
}
},

onSettled: () => {
utils.recipe.getQ.invalidate();
},

onMutate: async (step) => {
await utils.recipe.getQ.cancel();

const fRecipe = {
id: recipeData!.id,
openedAt: recipeData!.openedAt,
title: recipeData!.title,
description: recipeData!.description,
userId: recipeData!.userId,
steps: [...recipeData!.steps, step],
ingredients: recipeData!.ingredients,
};

utils.recipe.getQ.setData({ id: recipeId }, fRecipe);

return { recipeData };
},

onError: (error) => {
toast.error(error.message);
},
});
const newStep = api.step.create.useMutation({
onSuccess: (s) => {
if (s) {
setSteps([...steps, s]);
toast.success("Step created");
}
},

onSettled: () => {
utils.recipe.getQ.invalidate();
},

onMutate: async (step) => {
await utils.recipe.getQ.cancel();

const fRecipe = {
id: recipeData!.id,
openedAt: recipeData!.openedAt,
title: recipeData!.title,
description: recipeData!.description,
userId: recipeData!.userId,
steps: [...recipeData!.steps, step],
ingredients: recipeData!.ingredients,
};

utils.recipe.getQ.setData({ id: recipeId }, fRecipe);

return { recipeData };
},

onError: (error) => {
toast.error(error.message);
},
});
To optimistically update the recipeData the new step must have an id, but that id is generated after it is created; but this onMutate is running before it is created, is there any way to do this or do I have to start generating the ids on the client side? this is the step.create route btw:
create: protectedProcedure
.input(z.object({ recipeId: z.string(), step: z.string() }))
.mutation(async ({ ctx, input }) => {
const recipe = await ctx.db.recipe.findUnique({
where: {
id: input.recipeId,
},
include: {
steps: true,
},
});

if (!recipe) {
return null;
}

if (recipe.userId !== ctx.session.user.id) {
return null;
}

if (recipe.steps.length >= 20) {
throw new Error("Max steps reached");
}

const step = await ctx.db.step.create({
data: {
recipeId: input.recipeId,
step: input.step,
},
});

return step;
}),
create: protectedProcedure
.input(z.object({ recipeId: z.string(), step: z.string() }))
.mutation(async ({ ctx, input }) => {
const recipe = await ctx.db.recipe.findUnique({
where: {
id: input.recipeId,
},
include: {
steps: true,
},
});

if (!recipe) {
return null;
}

if (recipe.userId !== ctx.session.user.id) {
return null;
}

if (recipe.steps.length >= 20) {
throw new Error("Max steps reached");
}

const step = await ctx.db.step.create({
data: {
recipeId: input.recipeId,
step: input.step,
},
});

return step;
}),
usually prisma generates it when the step is created
model Step {
id String @id @default(cuid())
step String
recipeId String
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)

@@index([recipeId])
}
model Step {
id String @id @default(cuid())
step String
recipeId String
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)

@@index([recipeId])
}
SharpieMaster
SharpieMasterOP•14mo ago
with the thing I added it is not optimistic, the new step is rendered when the succes toast is triggered which is on the on succes here
Josh
Josh•14mo ago
You shouldn't generate it client side, as that's just got a good idea from a security standpoint You could use a temporary id and on success overwrite the temporary one with the real one
SharpieMaster
SharpieMasterOP•14mo ago
yea, the temporairy one is techincally blank, and ill make it that you cant edit it untill it has a valid id, but I want it to show up instantly so that I can run a check and make it uneditable am I using utils.recipe.getQ.setData() wrongly here?
Want results from more Discord servers?
Add your server