Progressively Enhanced Mutations with Next.js Server Actions

Progressively Enhanced Mutations with Next.js Server Actions


Progressively Enhanced Mutations with Next.js Server Actions

🚀 UPDATED VERSION:    • Cookie-based Auth and the Next.js 13 …   🚀

Server Actions are the new way to do server-side mutations with the Next.js App Router! 🚀

In this video, Jon Meyers demonstrates three ways to invoke these Server Actions - action, formAction and Custom Invocation - and discusses the tradeoffs of each! 🧠

00:00 Introduction
00:15 Creating a Next.js app with App Router
01:09 Install supabase-js library
02:08 Configure a Supabase client
03:43 Querying data from Supabase
06:41 Option 1: action prop on form
09:16 Option 2: formAction prop on button
12:54 Option 3: Manual Invocation with JS
15:40 Ideas for the future

👇 Learn more about Supabase 👇

🕸 Website: https://supabase.com/
🏁 Get started: https://app.supabase.com/
📄 Docs: https://supabase.com/docs


Content

0 -> next.js have just launched a brand new way to  do server-side mutations called server actions  
5.34 -> there are three different ways you can trigger  these functions so let's have a look at how each  
9 -> can help clean up your code and make it more  secure probably servers are more secure aren't  
14.64 -> they so let's get into it firstly let's run MPX  create Dash next Dash app and then the name of  
20.58 -> our application so in this case reading we're  going to build an app to help us keep track of  
24.54 -> all the things we want to read like blogs Etc so  we can come back to them later and this is going  
29.04 -> to step us through some questions we can leave  the defaults for all of these but feel free to  
33.24 -> opt into JavaScript if you feel more comfortable  with that I'm going to use typescript I'm going  
37.14 -> to use eslint and tailwind and no Source directory  I'm going to use the app router and I don't want  
43.08 -> to customize the default import Alias and once  that's finished we can change into that directory  
48.06 -> and open it up in vs code now server actions are  currently in Alpha so we need to opt into using  
53.58 -> them let's open up the next dot config.js file  and under experimental Oh weird app directory  
59.76 -> is is now stable but the experimental flag is  still set to true I'm probably just gonna leave  
64.86 -> this one here and just append server actions and  set it to true now we can install super bass so  
71.1 -> we can query some data so let's run npm install  or I at Supabase supabase-js and now that that's  
78.78 -> installed we can just run npm run Dev to start  our development environment and I already have  
83.34 -> a Supabase project up and running again a super  simple schema I have one table called readings and  
89.64 -> for each of our readings it's a good name we have  a title a URL for the article or whatever kind of  
96.3 -> resource we want to link to whether or not we have  read it yet so this defaults to false and then  
101.58 -> some upvotes so we can rank its importance against  the other readings we also have RLS enabled for  
107.46 -> the readings table but for this example we've  just written a policy to allow all actions so  
112.5 -> any request to select insert update or delete will  automatically be allowed so make sure you click  
117.78 -> that link above if you want to learn a little  bit more about RLS and how to write authorization  
122.22 -> correctly so back over in our next JS application  under app and then page.tsx we can get rid of all  
129 -> the boilerplate that we're returning here so from  Main all the way down to the end of Main and we  
134.7 -> can also get rid of this import for our image  and we're just going to return a H1 which says  
140.58 -> readings and we can see that's working so far by  going back over to the browser and navigating to  
145.2 -> localhost over Port 3000 and we see our heading  for readings so let's get some data displaying if  
150.66 -> we go back over to our application and create  a new Tils and then in that a file called  
155.16 -> superbass.ts and in here we're going to export a  default call to the create client function which  
161.58 -> comes in from at Super Bass Super Bass JS and this  function is going to give us a client da allowing  
168.72 -> us to send queries to Super Bass so this function  call takes in our project's URL and our non-key so  
175.26 -> let's create a DOT env.localfile at the root most  part of our project and in here we're going to  
181.44 -> create a next underscore public underscore Super  Bass URL and also a Super Bass a non-key and we  
187.62 -> can get these values from our Super Bass project  by going to settings and then API and then this  
193.02 -> is our URL and our non or public key and then back  over in our super bass.ts file we can replace this  
199.38 -> URL with process.m DOT next underscore public  underscore Super Bass underscore URL and then  
206.46 -> the same for our non underscore key and we'll  see typescript is not happy here because these  
212.34 -> environment variables might not exist so we just  need to use an exclamation mark to say that this  
216.9 -> value will exist in every environment this code  is running in now we can save this one and head  
221.88 -> back over to page.tsx and now because this is a  server component we can make it an async function  
227.28 -> and then we can get some data which is going  to be our readings we'll get this by awaiting a  
234 -> call to Supabase which comes in from our utils  Supabase file we want to fetch data from the  
241.08 -> readings table and we want to select all columns  we can now update our return statement to return a  
247.5 -> fragment without H1 in it and a pre-tag using this  json.stringify trick to pretty print our readings  
254.1 -> if we go back to the browser we can see that each  of our readings are displaying correctly now we  
259.14 -> can add a match filter to our query to Supabase  to make sure we're only getting back the readings  
264.9 -> where is red is set to false and we're still going  to see our three rows here but that's because is  
271.32 -> red is set to false on all of our records so if  we go back over to Supabase and go to the table  
276.24 -> editor and then click on our readings table and  look at the row for this next.js blog and change  
281.88 -> the value for the is red column from false to true  and then we can save that record and see that it  
288.54 -> has changed in the database now if we go back to  our application and refresh and you can see it's  
293.28 -> gone from our list now I'm going to set this one  back to false again because I don't want to run  
298.14 -> out of data too quickly now we're going to iterate  over each of these and display them in a list so  
302.76 -> we probably only care about the ID the title so  we can display it in our list and then the URL to  
309 -> link to so that we can actually go off and read  this article so we can specify those columns in  
314.1 -> our select statement here so we can say ID title  and URL so now we just see the data we actually  
320.16 -> need for rendering rather than going and just  fetching everything so back over in our code  
324.84 -> rather than just pretty printing this out to the  page let's actually map over each of our readings  
329.94 -> and then for each reading we want to render a div  and we'll set the key prop to our reading dot ID  
337.8 -> then we'll have an a tag with a href set to our  reading dot URL and as the text for our link we  
346.44 -> want to display our reading dot title now when  we save this and check where typescript is not  
351.84 -> happy and that's because this might be null and  so we can just use optional chaining here to say  
355.92 -> only map over it if we actually have an array of  readings now if we save this and go back to the  
360.36 -> browser we can see we have each of our readings  printed out to the page so this is looking a  
364.98 -> little confusing without styling so let's take  advantage of the fact that we already have  
368.46 -> Tailwind configured and do some basic styling I'm  just going to copy and paste this one to speed it  
373.5 -> up a little bit but with our H1 we're just making  it a little bit bigger add a little bit Bolder and  
377.94 -> then pushing anything underneath it down a little  bit with Mudge and bottom and then with each of  
382.08 -> our reading divs we're adding some padding and a  bottom border just to give it a line underneath  
387.36 -> each one with a gray that's fairly close to our  background color and if we go back to the browser  
392.4 -> we can see that looks much better and if we click  any of these articles it's going to take us off  
396.78 -> to that URL awesome so let's add the ability to  actually add an item to our reading list and we  
402.12 -> can do that with this slightly styled form again  nothing too crazy we're just adding some margin  
407.4 -> top to push this form down we have two inputs so  one for our title and one for our URL and both of  
413.58 -> them just have some padding a background color and  some margin right to add a little bit of space and  
418.74 -> then we have a submit button to actually submit  our form so let's wire this up to Supabase now  
423.54 -> in the old world we would have had to add an  on submit Handler here calling some function  
427.98 -> like handle submit this would take an event as a  parameter which we would immediately call prevent  
433.56 -> default on just to stop the web working the way  that the web works and since this on submit is an  
439.38 -> event handler we would need to make this component  a client component by saying use client at the top  
445.8 -> of our file and this will ensure that it hydrates  with client-side JavaScript blah blah blah but not  
453.12 -> anymore so let's remove this use client statement  and then we can make this function a server action  
459.12 -> by saying use server and this will now just  magically run on the server meaning that we  
464.4 -> don't need event handlers which means it doesn't  need to be a client component and so we can remove  
468.3 -> this event dot prevent default and now rather than  getting past an event our server action gets past  
473.82 -> some form data which is of type form data and  we can get our Title by calling formdata.get  
479.64 -> and passing it the string title but we can also  fancy this up a little bit and instead create  
484.8 -> an object from entries we can then pass it our  formdata DOT entries which is a function we can  
491.34 -> then destructure any of the fields that we have an  input for so title and URL but we get to do it in  
497.58 -> a handy dandy little one-liner so now we can await  a call to Supabase to say from the readings table  
504.9 -> we want to insert a new reading with our title and  URL and because we're using a weight here we need  
512.04 -> to turn this into an async function and then we  can tell next.js that we've performed some kind  
517.14 -> of mutation and we need to refresh the data by  calling re-validate path which is a function that  
522.72 -> comes in from next slash cache and then this takes  in a path so in this case we're just going to do  
527.28 -> slash which is our landing page and lastly rather  than this being an on submit Handler on our form  
532.5 -> we want to use the action prop and now if we save  and go back over to the browser we can refresh and  
539.04 -> test this as work talking with a test title and  a test URL and click save and we'll see our new  
544.62 -> test record appears in the list yay and that's  the first method for how we can trigger these  
549.66 -> server actions to show you the second option let's  add a mark as red button to each of the items in  
555.42 -> our list so back over in our code after our link  which is printing out each of our readings we're  
560.34 -> going to render a button the text for this one is  just going to be the check mark emoji and now we  
565.98 -> can specify a form action prop which is set to a  function that we want to call so in this case Mark  
571.26 -> as red and then we can declare this function above  it's actually going to be very similar to this  
576.3 -> handle submit so let's just duplicate that one but  this one will be called Mark as red we're going to  
582.18 -> need an ID and so we'll sort that out soon and  then we're going to await a call to Super Bass  
587.52 -> from the readings table but rather than inserting  we want to Now update and the column we want to  
592.86 -> update is is underscore red we want to set this  to true and we only want to do this where there  
598.2 -> is a match on the I ID column and that ID we're  passing across here so this is conceptually what  
603.9 -> we're trying to do when we click this button we  want to run this function which marks the reading  
608.4 -> as red but this form action prop implies we're  submitting a form so we need to wrap our button  
613.8 -> in a form element and now to get this ID passed  in without form data it needs to be in an input  
619.86 -> within our form so we can say input and we can set  the type to be hidden and so this means this input  
625.74 -> field won't actually show up in the UI we can set  the name to be ID and the value to be our reading  
632.46 -> dot ID and so now if we go back to the browser we  can see our new button for each of our items and  
638.16 -> if we click this one for test it disappears from  the list and that's because in our code we're only  
643.02 -> actually selecting the readings where is red is  set to false so this is working correctly but it  
649.02 -> looks pretty similar to our first option here  really we've just moved this action prop from  
653.88 -> the form down to our button so why would we want  to do it this way well this button doesn't just  
659.46 -> submit the form it now dictates where the  form is submitted to so this form could still  
664.44 -> have its own action so let's set this to default  action and then we can declare that one above by  
670.44 -> just duplicating this one and setting its name to  default action we then don't actually care about  
675.78 -> this form data and we can just get rid of all  of this and just console.log sent from default  
681.9 -> action and then we can declare a new button with  the type of submit and this one is going to be  
688.02 -> responsible for actually submitting the form and  now if we save this and go back to the browser  
692.04 -> let's create another test record and then when  I click submit this test record isn't going to  
697.2 -> disappear from the list and if we have a look at  our server console we can see sent from default  
702.12 -> action so when we click this submit button this  form is being sent to this default action function  
707.34 -> so this one that just console logs out our message  but if we click our other button it's marking our  
712.62 -> reading as red so we can confirm this is still  working by coming across here and clicking our  
716.82 -> tick and then test disappears excellent and now  that's how we Implement option 2 of server actions  
722.1 -> now something to call out here is both Option  1 and 2 give us Progressive enhancement out of  
726.42 -> the box and this means if JavaScript is disabled  or fails to load for whatever reason our form is  
731.52 -> still going to work and our server actions are  still going to run everything we've implemented  
735.54 -> so far happens on the server and so we don't  actually have any client-side JavaScript yet  
739.98 -> so let's get some client-side JavaScript running  and up the stakes but firstly I'm just going to  
744.72 -> comment out this button and just add a little bit  of styling to our form here to give it some margin  
750.54 -> left and also make it in line and now this looks  much better with our markers read buttons sitting  
755.76 -> alongside each of our items in the list and so  now let's look at the third option for calling  
760.74 -> a server action which is manual invocation and  so this is where we can choose when we want to  
765.66 -> call this function with JavaScript so this could  be in like an event handler in a client component  
770.82 -> rather than just being from submitting a form  or clicking a button and so we're going to add  
775.92 -> another button here which we're going to use to  upvote our resources so let's start by adding  
780.96 -> this column to our query from Supabase so here  where we're specifying ID title and URL we also  
786.96 -> want to get back our upvotes and then we're going  to chain on a DOT order so that we can sort our  
792.06 -> results by that upvotes column and we're going  to set ascending to be false so the one with the  
798.18 -> most upvotes will appear first and to implement  the upvote functionality we're going to declare  
803.46 -> a new server action called handle upvote and  again this will be an async function this one  
808.68 -> will explicitly take an ID which is a string  and we're going to make this a server action  
813.78 -> by declaring the use server statement and then we  can await another call to Super Bass but this time  
819.6 -> we're going to use RPC and so RPC just allows  us to call a postgres function from supabase.js  
825.48 -> and the reason we need to do that here is because  incrementing that upvotes column is actually two  
830.28 -> maybe three statements in postgres first we need  to select the current value for upvotes we need to  
835.68 -> increment it by one and then we need to write  it back to the column for that particular row  
839.76 -> so we can wrap all of this up in a postgres  function and just call it with this really  
843.66 -> simple API from Super Bass JS by just saying RPC  and then giving it the name of our function and  
849.18 -> so in this case that's upvotes we then just need  to tell it which ID we would like to increment  
853.8 -> the upvotes for and so that's this one that we  passed into the function and lastly we want to  
858.18 -> call that revalidate path function for our  landing page and so now we can render out  
863.4 -> our client component which is going to actually  run this function and so that will be after our  
867.6 -> markers red button our client component is going  to be called upvote it's going to take an ID prop  
872.64 -> which is set to our reading.id a upvotes prop so  that it knows how many upvotes to display so this  
878.88 -> is going to be set to our reading Dot upvotes  and then we have our handle upvote prop which  
883.92 -> is set to our handle upvote function and so now  we need to actually create this client component  
889.08 -> for upvote and so we can do that here under  the app directory we'll have a new file called  
893.52 -> upvote.tsx and then here's one that I prepared  earlier so again it takes our three props so our  
899.7 -> D our upvotes to display and then our handle  upvote function to increment that value by 1  
905.1 -> and then we're just rendering out a button which  when clicked is going to call that handle upvote  
910.08 -> function and pass it our current ID and then  we're displaying a nice little up emoji and  
914.94 -> then our number of upvotes and so now back in  our server component we can import this client  
920.52 -> component from dot slash upvote and now if we save  this and go back to the browser and refresh we can  
925.86 -> see we have these nice little buttons to increment  our value and if we click it it increments and  
930.78 -> because of our sorting it's now jumped up here  but we can click it again and increment again  
934.56 -> and again and we can increment this one and  we can make this one jump above this one so  
939.66 -> much fun but the cool thing is now that we're over  in JavaScript land we can do anything we want we  
945.12 -> could disable this button while we're actually  updating that value or we could Implement an  
949.08 -> optimistic UI pattern so we could update the value  for upvotes as soon as we click the button so let  
953.94 -> us know in the comments if that's an example  you want us to put together if you want to go  
957.48 -> deeper with the next JS app router and learn about  authentic education and authorization I recommend  
961.92 -> you check out this video right here we Implement  cookie based auth from scratch that's available  
966.54 -> across both the client and server components  but until next time keep building cool stuff

Source: https://www.youtube.com/watch?v=Qc8_y9irMP4