Now let's create a patch request inside of our projects API so that we can implement the autosave functionality. So let's have instead of source, instead of app, API, route, projects. And inside of here let's chain a .patch method which will begin the same way our .get method began with an ID parameter and now inside of here let's add verify out and let's add a z validator here. Now the z validator will have two options So first let's go ahead and let's add the simple param option which is z.object and it very simply accepts id which is a string. So this is the simple one.
And now we can add another one which will validate our JSON which basically means what we allow to get passed in the body of the patch method. So this will be projects.insertSchema as it was before. We already have this. But I never want the user to have to insert the entire thing in order to update the project. So for creation, Yes, these are required.
But for updating I don't really care what you update. So what we can do is we can chain partial like this. But I also care about what they should not be able to update from the frontend. So that I'm gonna go ahead and omit like this. They should never be able to update the ID by sending a new ID.
They should not be able to change the user relation. They should not be able to control created at nor updated at. So those fields are something we control ourselves. Great! And now let's add the last parameter which is our asynchronous controller.
So inside of here as always we can actually copy these two things from here and we can also copy this check here I have a misspelling of a constant here so let's prepare this and let's go ahead and also add const values to be cRequestValid.json so we are not going to destructure the values because we don't know what they might be, right? It might just be JSON, it might just be width or height. So now that we have that and we've checked this, let's go ahead and create our updated project by doing data await database dot update projects set and now we will spread the values and you can see because we use the proper types here project insert schema and we omitted these types and we added the dot partial we don't care what the values are so we're just going to spread all of these possibilities to be updated. So let's go ahead and change this to be updated at and new date so that's what we're gonna control ourselves and of course we have to add where and then we have to do the same thing so two equals in the first equal it will be that the projects.id matches the id from our params which with the structure right here which is basically part of the URL.
And the second thing is that the person trying to update this is actually the author. So projects.userId matches the auth token ID, which we have. There we go. And let's also chain returning. And now if data.length is zero, we can return back an error, unauthorized, and a 401 status code.
Otherwise, simply return c.json data and a first item in the array, like this. Great, and let me just see, Can data be undefined? Looks like data cannot be undefined so we don't need to add question marks here. So in my get ID I used a question mark here but looks like we don't need it at all. Great!
So now that we have that let's go ahead and let's turn this into a use mutation. So we can copy the use create project which we have inside of our source features projects. We have use create project so let's copy and paste this and let's call this use update project like this. Let me expand this as much as I can. So this time we are not working on the post request but on the patch request, but not directly, right?
Because we have to first write ID like this. So this is how it looks like in one line and the same change is needed here. So first we change ID and the patch request for that. Right, so that's the equivalent of slash API slash project slash ID and then a patch request. And of course we are only looking for success response types because we are going to handle errors ourselves.
Let's rename this to use update project and let's change this again to be projects ID and then a patch method here And now let's see what do we have to change here. We have to go ahead and accept JSON here, but we also need to accept an ID of the project to update. So let's go ahead and pass an ID string in the useUpdateProject. And besides the JSON, we're gonna pass param and our ID. There we go.
Let's go ahead and simply say failed to update project like this and on error it's going to be failed to update project. This will be project updated and actually we don't need to do this. Yeah, let's not have this query here because we're gonna have an indicator right here inside of the navbar. So we are going to invalidate the projects query once this happens and we also want to invalidate something we can already do. So our use get project, right?
We can invalidate this so that it fetches again, so that it gets the new updated values for this project ID. So let's go ahead and do that. Let's add from 10 stack query, use query client. That's the one we need. And then inside of the use update project we can define the query client here to be used query client and then inside of the own success here, let's go ahead and write queryClient.invalidateQueries, queryKey, project, and then go ahead and use the ID, which we have in the main useUpdate method here.
So we are also going to have this, but since we don't have this query yet, I don't want to invalidate it because this way we're going to have to remind ourselves and refresh our logic and the flow of how these invalidations work. So basically once we update a project ID we want to invalidate this useGetProject. This would be this one. Let me find it. Where is it?
Project. This one. So this useGetProject params project ID should get re-invalidated and refetched again. Right? Great.
We now have that and now we are ready to implement the auto save functionality so let's see what do we have inside of our navbar for features editor components so it's inside of source features editor components navbar I believe we've kind of set some things up here even though it's just you know made up. Yes, we have this. The BS Cloud check and saved. So this is hard-coded at the moment. So inside of here we're gonna have to implement some logic for saving.
And we're gonna do the indicator last. We actually first want to do the actual logic for this. So for that we're gonna go inside of features, inside of editor and let's go directly inside of editor right here where we just passed the initial data last time. So now let's add that mutation here. So I'm gonna go ahead and call mutation useUpdateProject like this and make sure you've added the import from features project So let me move that alongside the get project here.
And remember, use update project needs initial data dot ID. That's how we know what project are we going to well call the updates for. And now I want to create the the bounce save method which will use callback like this. And now inside of here what we can do for now is simply call mutation dot mutate and we can now pass in the values. So let's go ahead and define the values inside of here.
And let's make the values, well we can hard code them for now. JSON will be a string, height will be a number, and width will be a number. So we could technically use our insert project schema type or something like inferring the request type from Hono, but it's not exactly true because from the editor component itself, I only want to send this possible updates. So we are either updating the JSON or the height and the width. That's the only thing I expect here.
And here's another thing. If we now, we're gonna have to add the mutation to our dependency array. This will cause this use callback to be constantly updated. So what I want you to do is to destructure the mutate method directly from here. And then you will just call the mutate method and pass in the mutate method like this.
And now let's go ahead and let's pass in values inside. So the values are defined as this object right here great so we now have the bounce save which obviously is not yet the bounced but we are gonna add it later so what I want to do now is the following I want to pass this the bounced save when we initialize our editor here. So we have clear selection callback. Let's implement a save callback. Which we'll call the bounce save.
So we don't have the save callback. So let's go ahead and implement the save callback here. So inside of use editor, we're now going to have save callback. So we have to properly type it inside of editor hook props, which are located in the features editor types file. So find the clear selection callback and now we're gonna have an optional save callback which will of course return a void but also we have to define the types here.
So the values for this will be JSON height and width. Like this. Great. Now we can safely pass the save callback method inside. Now we just have to find a proper way of using the save callback method and I can already think of one place which is constantly being saved on every change that we do and that is, where is it?
Our use history. So let's go ahead and extend this by not only passing the canvas but let's also add a save callback inside. Let's go inside of our use history hook here and let's just properly type the save callback here. So I think that we have already used this a couple of times so I can just copy it here like this and save callback will be optional of course there we go So save callback has values of JSON height and width like that and let's extract the save callback inside of here and now we're gonna go and find our save method here and in here we have to do save callback. So this is what we are going to do.
Every time we have to save we are gonna have to also pass in the height and the width just in case that was updated. So let's get the workspace to be canvas, get objects bind, object and let's do object.name is clip so if you want to you can collapse this so it's more readable like this. We have the workspace and now let's get the height to be workspace height or 0. And let's also add a question mark here. This will be the width.
So workspace width 0 like this. And now that we have all information we can initiate the save callback with a question mark because it is optional and simply pass in the JSON the height and the width. There we go and passing save callback right here and remove the todo. So we already have the JSON right here. We already created and we push it inside of canvas history.
So now we just easily reuse that and this time we're gonna save it inside of our database. So what I want to do now is go inside of the use editor, my apologies inside of the editor here and go inside of the debounce to save method here and console.log saving. Let's see if this is now working. So I'm gonna go ahead and try this out now. Let's see, saving here.
And let me first of all refresh. Let's load this. Let's add a shape. There we go. Saving.
And more importantly, let's look at the Network tab. And I'm gonna look for projects. And let me clear everything and let's try moving this. There we go. We have fired an event.
So we call the useUpdate project and then we also get the useGet project because we re-invalidated. And there we go! You can see that the new useGet project this time has a JSON object inside. We have the width, the height, everything. And now let's go ahead and let's see if we're sending the proper data in the payload here.
There we go. So let me try for example updating the height and the width and let's see if that will be reflected in the new request. So I will change this to 900 and I will click resize and let's see I'm not sure if I preserve my data yes I don't preserve my data so let me try it like this I'm gonna change this to 500 and click resize and let's see if I change there we go height 900 with 500 perfect So we are now updating our stuff and let's also confirm one more thing. So I'm going to go ahead and go and make sure you run your database studio. So let's do it like that.
So inside of here I'm going to do. Oh, I already run around it. Okay. So I can do a bun run dev here and visit your Drizzle Studio here. And what I'm gonna do is I'm gonna remove all of my projects.
So make sure you're inside of a project and remove all of the projects which exist here refresh your app here just make sure it loads I have a little indicator here I'm gonna click start creating and now I should have a completely empty project inside of here Let's just wait a second for this to compile. Let's make sure that there are no errors so you can see it's compiling the editor page here. There we go. It should be here any second. Perfect.
So it's completely empty and if I go inside of my Drizzle Studio now inside of my project well I probably should have done this yes and now yes I've messed it up so for you the JSON might be null That's completely normal and I expected this to be null. The reason mine is not null or just an empty string, right? One of those. The reason mine is not is because I manually pressed the command command S which saved and automatically triggered the network request because I used command S to open the sidebar great! But let's try something else if I just randomly draw something here this is now in my database but if I refresh after I load the exact same thing, where is my drawing?
Well, we don't have that implemented. That's where we stopped, if you remember. We added the initial data, but we only use it for this. So now we're gonna have to implement the functionality to actually load the initial data JSON. So let's go back inside of the editor here and let's use the initial data.
We can also now remove this and I will just add a little to do, add debounce. Let's use this initial data now by going inside of the use editor here and besides these callbacks let's also define default state which will be initial data JSON, default width which will be initial data width and default height which will be initial data height like this. Now we also have to update our use editor hook props here. So let's add the default state to be optional and a string default width to be optional and a number. And same thing will be for default height.
There we go. So now inside of our editor here none of this cause any errors. We can now safely work inside of the use editor here. So now let's go ahead and let's actually destructure those things. So default state, default height and default width like this.
Since this will actually get updated as a prop let's find a way to preserve them so that we don't care about them updating unless we explicitly want to do so. So we're going to reinitialize these by adding the initial state to be useRef from React. Make sure you add useRef here from React. And inside of here, we're gonna simply pass the default state. Then we're going to initialize initialWidth, useRef, defaultWidth and then initialHeight, useRef, defaultHeight.
There we go. So now regardless if this default state, default height and default width get updated, which they will, because this is just a prop. The initial data is a prop and if you take a look at our page with the project ID it is ReactQuery, right? So this will get updated every time it is re-invalidated, which is a good thing for us, but then it will send new data here in the initial data and then that will retrigger the entire use editor initialization, which is not something we want because we're going to lose progress, there's going to be conflicts, it's just going to be unusable. So that's why we are going to store this again in a useRef and we are now not gonna care what happens with these things right here because we will simply rely on the initial values of the refs here.
So let's use the initial width and the initial height in a proper way. Let's scroll down to our init method here and inside of here as you can see we manually define the width and the height of our object. But what we're gonna do now is we're gonna use the initial width and the initial height.current. Like this. So now you can programmatically start a new editor with any resolution you want and you can also preserve the resolution you know from your any new project which you've changed right.
So this way we can now programmatically from for example home page provide the user with different options you know start in a 500 by 500 or start in a orientation mode landscape mode right you can now completely control that with these values which you are going to pass because now you can use this... Where is it? Use editor and you can simply you know you can skip the default state you don't have to pass that you can simply change the default width to whatever you want right this can be on click on something else you can simply change this you know a thousand it doesn't have to be related to initial data at all so that's what we achieved now we programmatically improved our use editor hook great and now we have to implement an important hook which we are going to call use load state. So let's go ahead and do that. Let's go inside of hooks use-load-state.ts.
Let's go ahead and let's import useEffect, useState, my apologies useRef. Let's import JSON keys from types. I'm gonna change this to features editor types. And let's define a type, useLoadStateProx, which will have auto zoom, which is a void. This can be an interface.
We're gonna need a canvas, which is fabric.canvas or null. And I'm gonna go ahead and simply add fabric from fabric. Besides the canvas, we're gonna have the initial state, which will be a type of react mutable ref object, string or undefined. We're gonna have canvas history, which is a react mutable ref object again and it's gonna be an array of strings and we are gonna need the set history index which is our react dispatch method. So react set state action which controls a number.
Now let's export const use load state with a lowercase u and let's assign the use load state props here. We can now destructure all of those so canvas.autoZoom, initialState, canvas.history and setHistoryIndex. And now let's go ahead and let's define initialized to be use ref false by default. So we are just going to ensure that this hook never fires twice because we don't want to interrupt the user if we are loading the state, right? We don't want to overwrite their work by accident.
So let's add a useEffect here. First of all, if we did not initialize, so if not initialized.current and if we have initialState.current like this and if we have the canvas property, Only then can we begin doing canvas load from JSON and inside we are going to simply load json.parse initialState.current and then we have a callback method here which we are going to do the following. We're going to get the current state. So JSON stringify canvas to JSON and pass in JSON keys like this. And now that we have the current state of what we just initialized to the canvas history we are going to push well not push but we are going to reset the entire history and only have the first and current state in the array.
And we are going to reset the history index back to zero. And then we're going to auto zoom. And finally, then we can go ahead and call initialized.current to be true, Like this. So now we have to add some stuff in the dependency array here And then we're gonna explain what we are doing here. So we have canvas we have auto zoom Those are the actual required fields, but now we also have the initial state which technically is no need.
This is a ref right and same is true for our canvas history also add a comma here not at the end of the comment the comment doesn't do much so canvas history is the same thing we don't need it because it's a ref and also set history index this is a dispatch so again I want you to know that there is no difference in the code if this three don't exist right But because we have this linter on, we have to follow the rules. Great. Now let's just prettify the code just a little bit here. So for example, we can add a JSON to load can be JSON parse right here, right? Simply so we separate some logic Or data to load.
Let's call it data, right? So we pass in the data here. And this is OK, I guess. Yeah, OK, I just wanted to separate that. Great, so basically what we're doing here is we are simulating what happens in the init method here.
So similar thing happens inside of here. You get the current state which is literally this. So this uses the initial canvas. Literally the first time we load the editor we get the current state even though it's empty and we kind of prepare the canvas history to begin from that state but now we have to do the same thing but this time we first have to load it from JSON. Right?
So that's why we're doing this And we're also doing an auto zoom here. So it's perfectly centered once it loads. So let's save this. Let's go inside of our use editor now and Let's add it somewhere. So I recommend that you add it above the editor here.
So let's do use load state and now we have to pass in everything inside. So just ensure that you have this in here. So features, editor hooks. And this is a very important hook. So just make sure that you put an exclamation point here.
You have a question mark here. All of these things are quite important. So this works properly. You can always double check with the source code. Great, so we have the use load state here and now let's pass in the canvas, let's pass in the auto zoom method, let's pass in the initial state, the canvas history and the set history index.
All of those things. Great, we now have that. And now, as you can see, it automatically refreshed for me and it stayed here. So now I'm gonna change this and say, you know, auto save. I will wait a second.
I assume the network request has now passed and I will refresh this entire thing. And let's see. There we go. We implemented autosave. And we can load it.
Amazing. Let's see if our changing of the width and the height works because that can be kind of tricky. So I assume the network request has passed. Let's refresh again and let's see if that is preserved as well. It is amazing!
So Now what I want to do is I want to wrap up the chapter by also finishing the indicator in the navbar and let's not forget we have to actually debounce it because right now it's initializing way too many network requests. So let's go back inside of source, features, first in the editor directly. So editor components first in the editor but you can also prepare and open the navbar component from here. So inside of the editor component we're gonna find where we have the navbar and we're going to extend it by adding another prop which will be the initial data.id and now in the navbar inside let's go ahead and simply add that id prop here so that will be a required string. So why do we need the id in our navbar?
Well, that's because inside of our useUpdate project right here, we are going to define... Oh, looks like we did not define this, but we're gonna have to add a query key here, I believe, by which we are going to recognize this mutation and then display its status on the top here. So let's go ahead and do that. So above the mutation function, let's add mutation key, which will use project ID like this. So we are working in the use update project and this is the mutation key.
So yeah I think that because in here you can see that we are invalidating the query project id which is exactly the same but this is the query key so I don't think they will conflict one another. We can easily you know change this to projects if needed but I think this is completely fine because this is a mutation here. So make sure you do that. And now in the navbar what I thought I was going to be able to do was just add the use update project in here as a mutation and then simply check you know mutation is pending but that's not how it works so every time that you add the useUpdateProject anywhere or I mean generally the useMutation it creates a new instance of itself So you won't be able to share this hook the same way you share the query. So useQuery can be shared across many different components and they will all share the same loading status and stuff like that.
But useMutation does not work like that. So they do have a solution for that and that is called useMutationState. So let's get the data and useMutationState here from TanStack React query. So let me just move that with my global inverse here. And let's go ahead and give it some props.
So filters are going to be the following. We're going to filter by a mutation key project and then id. So make sure that it is oh and we also have to extend the ID here. There we go. So just make sure that it is exactly the same as your mutation key in the use update project, project ID.
And exact will be true. And then let's also add select here to further filter down what we are looking for. So we are only looking for mutation state status. That's what we want to see. And then let's define the current status to be data and simply, well, here's the thing.
Every time that autosave gets triggered gets fired this data is an array which will simply have the you know if we autosave three times it's going to have three items in the array So what we have to do to have the most up-to-date status is to always look at the last in the array. That's how we're going to know what the current status is. And then let's define our own. IsError to be currentStatus. Let's call this currentStatus.
So currentStatus is error. And then we're gonna have is pending. Current status is pending, right? And we don't need to explicitly define is success we are simply going to assume that if there is no error and if there is no pending it can only be success right so yeah this API if you ask me is a kind of weird I don't know maybe I don't understand exactly you know what else it's been used for but for such a simple thing that I'm trying to do here, I did kind of have to, you know, scratch my head to figure out how it's working, right? Of course, you can, you know, search for this term and then stack to back query and you're gonna find the documentation on how to properly use it.
But they will show you pretty much the same thing as this. Great! And now that we have that, let's find our hardcoded saved keyword here. So this right here. When will this appear?
This will only appear if we are not pending and if we don't have an error. So only then are we going to display this saved thing. So we can now copy and paste this. And now what happens if we have an error? So again, if we are not pending, but this time remove the exclamation for is error.
In that case, we're gonna do vs cloud slash from react icons, bs. So we're gonna continue to import from here and we're going to write failed to save. And then you can go ahead and copy this one and this one will simply be if is pending and nothing else and for that we're gonna be using the loader from Lucid React so make sure you import this And you can give this a size of 4 instead because it comes from a different import So the sizes are just a bit different and inside of here. Let's add saving and three dots So I think we're now ready to try this out if we've done it correctly. So right now, let me refresh everything.
And yeah, don't be afraid if you look away and your screen becomes empty. That's because of hot reload. So hot reload triggered a some weird, you know, I don't know. It triggered one of our hooks with its strict mode but it's not something that will happen in production. So now it says saved but if I move this it says saving.
Perfect one thing we forgot to do where is my loader here I forgot to add animate spin. Here. There we go. So now if I go ahead and move this, there we go, it says saving and then saved. And now let's try and do a fun little thing.
Let's go inside of source, app, API, routes, project and find the patch method and just inside of the controller try and return an error here so I'm gonna return an error and add 400 like this So let me expand so you can see. So just try and have an error, for example, in your patch ID method here. So now if we've done this correctly, If I try and move this, there we go. It says failed to save. And let me go ahead and remove the error now.
And let's see if, will this, there we go. Now it's back enabled on save. Amazing. So we now have an indicator for our auto save. And the problem is it's obviously saving too much now.
You can see how much I can trigger it and it pretty much gets slow and this just spammed our server. We don't want that to happen. So let's go inside of the editor here and let's properly debounce this debounced save method. So let's go ahead and go inside of the terminal and let's do bun and loadash. You can add the entire thing but I think we're only going to use one thing from here so you can add loader.debounce.
So we don't add the entire dependency but just this one which we need. And then run runDev again. You can refresh your page here and before we implement this I want us to actually compare how this looks like. You can of course use any other debounce method. You can develop your own if you want to.
You don't have to add a dependency if that's not something you like for your projects. So let's just wait for this a bit. And now I'm going to open my network request here because I want us to focus on how many times this will autosave, right? So I'm gonna go ahead and move this a lot of times. You can see how many requests I've made just by triggering, but just by moving this in a very short period of time right so by debounce what we're going to do is we're going to reduce the amount of calls being made very simply let's go ahead and let's import debounce now so import debounce from low dash debounce oh we need a So import debounce from load-debounce.
Oh, we need a declaration file for this. So let's just add that as well. My apologies, I forgot. BunAdd, d, addTypes, load-.debounce, bunRunDev. There we go.
Is the debounce method the method here, right? There's no, yes. So it's a default export like this And now let's go ahead and let's use it inside of the debounce to save in a very simple way. So what we're going to do here is you kind of have to write the non-orthodox syntax. So directly inside of useCallback, inside of this arrow function, right?
This will now become our method. So wrap the balance inside of this like that. And then you're gonna go ahead and wrap the end of the parentheses here like this. So let me go ahead and maybe collapse this even further so it's clearer to see let me go ahead and indent this there we go and this should be like this there we go and then when this debounce here ends or maybe I did something wrong just a second so the debounce starts here in the values the debounce returns the mutate method and then yeah so then we add a comma 500 right here so when this method ends inside of debounce we add a comma 500 and this is ending the entire debounce method, right? But first part of the debounce is the thing we want to actually, you know, debounce, which also includes, you know, this entire thing.
And then 500 is by how much. And now here's the thing, we're gonna have this error and I've googled this and specifically for this case of debouncing and basically the agreed solution was to disable this rule because our dependencies are correct, right? But the linter simply cannot, you know, process this exact thing. So let's just go ahead and maybe style this a bit better. No, or however you want it, right?
It was fine the way it was great so we have this and now we should be able to see the clear difference of what we've just created so let's try it out now inside of here I'm gonna go ahead and open my network tab again. Like this and let's see it now. Maybe I need to refresh first. Definitely. Yes, because I've shut down my app and now I need to refresh it.
Yeah, every time you shut down your localhost, you need to refresh your app. Otherwise, it's going to have, you know, loaded your last saved code change. Let's try this again. So I'm going to go ahead and close my networks here and as you can see nothing is happening but if I let it go then it happens so nothing will happen for this very short periods of time so unless half a millisecond passes only then do we fire an update and then re-invalidation comes. So that's what the 500 does.
It represents half a second. So time here's are in milliseconds. So that's what you want to do. If you want to increase that you can increase it right so that's how we are going to protect our server from being spammed feel free to have this deep on the bounce written in a nicer way but yeah but also be careful right so you can't do debounce like this right you need to immediately have the debounce inside of a use callback and then in the first argument of debounce you are adding your function here and then In the second argument of debounce you're adding the milliseconds. Great.
So you now have a functional autosave, you now have functional debounce, you can load the project, you know I can refresh and this will now be loaded. And let's also check whether you know undo also does the same thing. So if I go ahead and add now a shape, let's just confirm this is now saved and I refresh this. The shape is here. Perfect.
And let's see if I click undo, the shape should now be in the center. I think at least or perhaps I didn't give it enough time but looks like it works just fine. And you can also manually press command S and that as you can see also triggers the save event so I'm not changing anything I'm just pressing command S and that also triggers the save event amazing amazing job