Progressively Enhanced Mutations with Next.js Server Actions
Aug 15, 2023
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