Node.js in React with Server Actions with Next.js with Cloudinary

Node.js in React with Server Actions with Next.js with Cloudinary


Node.js in React with Server Actions with Next.js with Cloudinary

Use Server Components and Server Actions to add image search to a Next.js app with the Cloudinary Node.js SDK.

Sign up for my newsletter: https://www.colbyfayock.com/newsletter/

---

# Resources

Tutorial: https://spacejelly.dev/posts/image-ga
Code: https://github.com/colbyfayock/my-clo
Demo: https://my-cloudinary-image-search.ve

https://twitter.com/colbyfayock
https://twitch.tv/colbyfayock
https://www.colbyfayock.com/uses

#colbyfayock #nextjs #cloudinary #nextjs13 #webdevelopment


Content

0 -> one of my favorite ways of interacting
1.92 -> with cloudnary is using the node.js SDK
4.38 -> but how do you use node inside of a
6.24 -> react app well up until now you really
8.58 -> couldn't but the games changed here's
10.62 -> how we do it with server components
13.4 -> so we're going to use the node SDK in
16.02 -> order to build a little image gallery
17.46 -> where we're going to just list out all
18.72 -> the images inside of one of our accounts
20.64 -> where we're going to extend that by
22.38 -> giving it the ability to search such as
24.359 -> if I wanted to search space inside of
25.92 -> all my images we can see those results
27.779 -> all using the SDK now better yet we're
30.599 -> going to see how we can make a little
31.859 -> bit of magic happen using server actions
34.02 -> where we can submit that form for the
36 -> search query and push that through to
37.92 -> the server request in order to render
39.84 -> those different results based off that
41.52 -> search now to start up this project I
43.62 -> created a little skeleton here where I
45.3 -> have a search form I have a clear which
47.04 -> we're going to dynamically add depending
48.66 -> on if we have a query and we're going to
50.399 -> have a bunch of images listed out inside
51.96 -> of a grid in order to easily scaffold
54.059 -> this I used Tailwind elements which
55.92 -> allowed me to copy and paste some
57.3 -> different elements including my image
58.8 -> gallery itself as as well as a search
60.84 -> bar which has a few different options
62.34 -> that we can work with we can see that if
64.199 -> we look in the code I'm going to be
65.339 -> working out of page.tsx which is under
67.68 -> the root of the app directory which is
69.9 -> going to be our home page but we can see
71.58 -> that inside of the file there's really
73.02 -> nothing else in here right now except a
75.24 -> bunch of HTML and the class names for
77.159 -> the Tailwind now inside of my clouding
79.02 -> account I uploaded a bunch of images
80.46 -> already that I have from other different
82.02 -> projects but I'm going to particularly
83.64 -> work inside of my my image gallery
85.56 -> folder you can really work inside of
87.299 -> whatever folder or location that you
89.22 -> want but ultimately you're going to want
91.14 -> to have a list of different images so go
93 -> ahead and upload those to your account
94.259 -> but ultimately I want to take all these
95.939 -> and I want to add them onto my page and
98.159 -> to do that we're going to use the node
99.36 -> SDK which allows us to easily work with
101.46 -> cloudery inside of a node environment
103.02 -> including being able to do things like
104.759 -> manage and upload and really whatever
106.68 -> you need to do with your cloudinary
108.18 -> assets but to start off we're going to
110.04 -> want to install this SDK so we're going
112.02 -> to run npm install cloudinary right
114.18 -> inside of our terminal and once it's
115.86 -> done we can spin back up our development
117.24 -> server but once it's installed the first
119.04 -> thing we want to do is actually
119.88 -> configure cloudinery inside of our
121.979 -> project so I'm going to use this import
123.96 -> statement to import V2 as cloudinary and
126.78 -> I'm going to paste it in right at the
127.92 -> top of my project and then I need to
129.78 -> configure my instance of Cloud area so
131.58 -> I'm going to run a cloudery.config the
133.92 -> inside I'm going to want to add my cloud
135.959 -> name my API key and my API secret now
141.18 -> rather than writing these right inside
142.8 -> of the configuration I'm going to store
144.36 -> these as environment variables so inside
146.34 -> of the root of my project I'm going to
147.84 -> create a new file
149.48 -> called.env.local and inside I'm
151.379 -> ultimately going to want to create three
152.7 -> different variables I'm going to create
154.02 -> my cloudery cloud name my API key and my
156.599 -> API secret and if we'll notice that I'm
158.7 -> appending next public to my cloud name
160.92 -> because later as we'll see we want to
162.84 -> have that accessible inside of the
164.459 -> application which could also be
166.08 -> client-side so we want to make sure that
168 -> this is available to use throughout the
169.98 -> app but in order to get those values we
171.72 -> can navigate over to our programmable
173.28 -> media dashboard where here we're going
175.2 -> to be able to see our Cloud name our API
177 -> key and our API secret so we can just
178.92 -> start copying and pasting these values
180.48 -> in including the cloud name and all the
182.64 -> other keys to ultimately make sure we
184.5 -> have all of our environment variables
185.879 -> set and once those are set we can head
187.62 -> back into page.tsx where we can
189.72 -> configure each of those in order to grab
191.879 -> it from the environment variable file so
193.62 -> at this point we're ready to actually
194.76 -> start using the SDK where we have a
196.92 -> couple options for how we get all of our
198.599 -> resources including the admin API where
201.18 -> we can actually hit the resources
202.379 -> endpoint but we're going to actually use
204.06 -> the search API because we're going to
205.86 -> eventually add a search to this now if
207.84 -> we actually go down to the search method
209.34 -> we can see that we can do this by easily
211.2 -> running cloudinery.search we can then
213.54 -> execute that command which we can then
215.159 -> use as a promise to get those results so
217.8 -> at the top of my home component I'm
219.12 -> going to set up my results which is
220.92 -> going to be equal to awaitlinary dot
223.62 -> search we're going to also pass in
225.48 -> expression which for now we're going to
227.519 -> just leave blank but then we're going to
228.959 -> run execute at the end of that which is
231.239 -> going to trigger that search but as we
232.98 -> can see we actually have a little
234.12 -> squiggly line here and that's because
235.44 -> we're currently trying to use a weight
236.94 -> inside of a normal function where what
239.58 -> we want want to do is we want to set up
241.319 -> our home component to actually be in a
243.84 -> synchronous function that way we can use
245.7 -> that nice async await syntax but to see
248.34 -> how this works let's actually console
249.9 -> log out the results and if we refresh
252.12 -> the page and ignore some of these errors
253.799 -> where this is just because I must have
255.239 -> not have updated the SVG that I was
256.979 -> pasting in but what will actually not
259.56 -> see is the console log and that's
261.78 -> intentional as we're not running that
264.06 -> console log in the client we're running
265.8 -> that on the server as this is a server
268.139 -> component so what we need to actually do
270.18 -> is look inside of our terminal where we
272.58 -> should now be able to see our asset data
274.8 -> from making that search but that means
277.139 -> we do have all that image data where we
279.6 -> can now start to add that to the page
281.04 -> now to start I want to destructure the
282.78 -> resources just to make it a little bit
284.04 -> easier for me to access so let's
286.62 -> destructure our results to resources
289.08 -> where now when once we have these
291.6 -> resources we're going to scroll down
293.16 -> until we get to where we want to display
295.02 -> our images where particularly I'm going
297.24 -> to add my image inside of each and every
299.1 -> one of these little divs that I got from
301.259 -> that Tailwind elements so I'm going to
302.88 -> say
303.62 -> resources.map and then for each free
305.699 -> source I'm going to ultimately return
307.82 -> one of these divs and I can also go
311.46 -> ahead and get rid of those other divs
313.86 -> since I don't need them anymore but then
316.44 -> I can create a new image tag
319.44 -> inside of that where my source is going
321.479 -> to be resource dot secure your url I'm
326.699 -> going to have an ALT which I'm just
328.62 -> going to leave blank for now since we
329.759 -> don't really have an appropriate way of
332.16 -> adding that
333.36 -> where you want to make sure that we have
335.34 -> our width which is going to be
337.46 -> resource.width as well as our height and
340.979 -> then we also see that we're going to get
342.24 -> a little typescript error and I'm not
344.16 -> going to go in too deep into typescript
345.9 -> with this so I'm just going to say
347.639 -> object since we're ultimately or I
350.52 -> probably want to just say any so I don't
352.44 -> have to worry about this as it probably
354.78 -> should be an object anyways we have our
357 -> resource where we're going to be able to
358.38 -> get all that information now if we
359.94 -> reload the page we can start to see all
361.62 -> those resources are now showing up on my
363.66 -> page but these aren't necessarily the
365.34 -> ones that I was looking for remember I
366.96 -> wanted to show a folder which might or
368.759 -> might not be your use case but I want to
370.62 -> show a folder because I like to try to
372 -> keep all my assets organized so this is
374.16 -> where we can start to work with the
375.6 -> actual expression and form our search
378.3 -> query for working with the search
380.16 -> endpoint so what I'm going to first do
381.78 -> is I'm going to add folder and I'm going
383.699 -> to set that to my image
385.979 -> Gallery where as soon as the page
388.08 -> reloads we can see that I get a lot of
389.58 -> those nice images right on the page now
391.5 -> one thing when working with these images
393 -> directly from our calendar account is
394.919 -> it's going to give us those raw images
396.419 -> which is fine because that's ultimately
398.34 -> what we're requesting but if we start to
400.139 -> look at these images this one for
401.699 -> instance is six thousand by four
403.44 -> thousand wide that's because this is
405.419 -> just a bunch of unsplash images that I
406.979 -> dumped inside of my account but
408.6 -> realistically we're not going to have
410.1 -> the images inside of our account at the
411.96 -> exact size that we want which is where
414 -> the power of cloudery comes in where we
415.62 -> can start to dynamically transform our
417.419 -> images to only the sizes that we need
419.46 -> now we could probably use the node SDK
422.039 -> and add some transformations to our
423.66 -> images that way which is certainly a
425.34 -> good way if you want to just stick with
426.539 -> that but instead we're going to use next
428.34 -> cloudinary which is a way that we can
430.199 -> get some first class support for some
431.94 -> amazing plenary features we're inside an
433.74 -> xjs so to get started we want to npm
436.199 -> install next Cloud energy so I'm going
437.94 -> to run that command inside of my
439.139 -> terminal but as we can see under
440.52 -> configuration we need an environment
442.259 -> variable but we already set this one up
443.88 -> because as I mentioned before we were
445.56 -> going to want to have that next public
446.94 -> cloudinary Cloud name in order to access
448.86 -> that using client components now this
451.38 -> Library comes with a few different
452.52 -> features in order to make it easy to
454.139 -> work with Cloud energy inside an xjs
455.639 -> including an upload widget and a video
457.5 -> player but we're going to stick to the
459.18 -> CLD image component which is going to
460.919 -> allow us to just take some advantage of
462.599 -> some of those image transformation
464.16 -> features so we're going to first import
466.08 -> CLD image from next Cloud Mary so I'm
468.479 -> going to paste that in at the top of my
469.86 -> file and we can see in order to use CLD
471.96 -> image we have a few required parameters
474.18 -> including width height source and alts
476.88 -> where sizes is optional but of course we
478.919 -> all always want to have responsive
480.24 -> sizing but really all we need to do is
482.46 -> change this image tag to CLD image now
485.699 -> once we refresh the page though we're
487.139 -> going to see an error where we're
488.639 -> currently trying to use a client
490.319 -> component inside of a server component
493.02 -> and I want to make sure that that is
495.12 -> clear where we have that separation of a
497.46 -> server component and a client component
499.68 -> now in order to use client components
502.02 -> inside of next.js all we really need to
503.94 -> do is add use client to the the top of
506.58 -> the file but I don't want to do that
508.08 -> inside of my home page that's going to
509.639 -> turn this into a client component which
511.319 -> first of all I don't want to do I want
513.18 -> to be able to use the node SDK inside of
515.279 -> a server component so what we're going
516.839 -> to do is we're going to create a wrapper
518.459 -> around the CLD image component so inside
520.979 -> of a new folder called components I'm
523.5 -> going to create a new file called CLD
526.339 -> image.tsx inside I'm going to start it
529.08 -> off by adding that use client directive
531.24 -> or I'm going to create a new constant
532.68 -> called CLD image and I'm going to just
535.14 -> scaffold out a new component
538.14 -> where I'm going to ultimately export
540.72 -> that default CLD image but inside is
544.26 -> where I'm going to return that CLD image
546.48 -> component so at the top I'm going to cut
549.18 -> out that import statement I'm going to
551.94 -> import this as next CLD image just so
555.18 -> that I can have it nice for organization
557.519 -> and not actually have a conflicting name
560.22 -> there but I'm going to ultimately return
561.54 -> that component I'm going to take the
563.459 -> props from CLD image I'm going to spread
566.399 -> those out onto that next CLD image
569.82 -> component now if you notice we're going
571.56 -> to get a typescript issue here and
573.42 -> there's probably better ways to solve
574.98 -> this but again I'm just going to escape
576.42 -> into using any but then we can see that
578.94 -> we're going to be able to pass in any
580.62 -> prop that we want into the actual CLD
583.68 -> image component now to use this I'm
585.66 -> going to import CLD image from my
588.959 -> components directory where I'm going to
590.58 -> grab CLD image and I'm going to make
592.74 -> sure that I have that capital I now I'm
595.019 -> realizing the reason why I'm getting the
596.459 -> squiggly line is because I have to
597.779 -> actually access this from the root so
599.94 -> it's actually app slash component CLD
602.1 -> image but as we can see we're already
603.72 -> referencing that CLD image so we don't
605.459 -> actually have to make any changes but
607.2 -> once the page reloads we can start to
608.7 -> see our images trickle in and they might
610.86 -> be a little slow to load for the first
612.12 -> time because we're loading new URLs and
614.279 -> if you're loading huge huge huge images
616.019 -> like I am of course they need to process
618.12 -> fast a little bit slower the first time
619.98 -> but as soon as I reload the page we can
621.6 -> see they start to trickle in fast even
623.459 -> though these are huge images now if I
625.62 -> look back inside of the image and I
627.54 -> start to inspect it we can still see
629.58 -> that I'm loading these images at huge
631.92 -> sizes that are completely unnecessary
634.2 -> for my project such as we can see this
636.66 -> one image here it's rendered size is 158
639.959 -> by 158 yeah I'm loading it at 383 840
644.64 -> pixels by 3848 pixels anyways it's super
648.42 -> huge way more than I need so what can we
650.7 -> do to actually serve it at the size we
652.2 -> need now there's a few ways I can do
654 -> this such as adding the sizes attribute
655.92 -> where if I add since it's four column
658.019 -> let's add 25 viewport width which isn't
660.899 -> going to be exact but it works for our
662.339 -> purpose once the page reloads and we
664.26 -> look at that intrinsic size we can see
666.12 -> that it's at a lot smaller of a size and
669.18 -> we can see that the source set has all
671.04 -> those different sizes depending on the
673.26 -> size of the viewport now one thing we
675.36 -> didn't look at is the performance of all
677.04 -> this so let's revert this back to image
679.019 -> and I'm going to also comment out the
680.519 -> sizes where if I'm loading all these
682.56 -> images as is I'm currently loading 27.6
685.98 -> megabytes of images that's an incredibly
688.92 -> huge amount and completely unnecessary
690.839 -> now starting to reverse back through
693 -> that reversing I guess let's now switch
695.76 -> that back to the CLD image component yet
697.98 -> not add that size is prop yet now once
700.56 -> we refresh the page again we're going to
702.12 -> start to see a few things including the
703.8 -> type is going to be avif for all those
705.839 -> images if you're inside of a monochrome
708.18 -> browser where it's going to
709.56 -> automatically optimize the images and
711.839 -> automatically deliver the most modern
713.16 -> format for that browser so by just
716.1 -> simply changing it to CLD image alone
717.959 -> we're now cutting that down to 15.5
720.54 -> megabytes and we're still serving them
722.76 -> at those huge resolution sizes but now
726.06 -> again let's enable that sizes attribute
728.339 -> which is going to give us the responsive
729.66 -> sizes we're going to completely cut that
731.579 -> down even more where now we're going to
733.74 -> be serving it in that Abiff format where
735.42 -> it makes sense but now we've taken that
737.279 -> down all the way to 389 kilobytes that's
740.519 -> a huge transformation and so much less
743.1 -> that we're needing our our users to
745.62 -> actually download when they're visiting
747.54 -> our site but I think my excitement here
750 -> got ahead of myself so let's actually
751.44 -> get back to the use case here where we
753.42 -> want to make a search on these actual
755.04 -> images right now the way that cloud
756.54 -> running search is going to work is it's
757.86 -> going to take a few things into
758.94 -> consideration where it's definitely
760.68 -> possible that when we're searching on
762.3 -> these these image files are going to
763.8 -> just have an ID or Garbage as the actual
766.62 -> image name but one thing that I have on
768.54 -> all my images is I auto tag them using
770.82 -> the Google auto tagging add-on so that I
773.339 -> have everything that's included inside
774.839 -> of that using AI now if you already have
776.94 -> your images inside of cloudner you can
778.56 -> always check out this Auto Tags feature
780.54 -> which you can use the image analysis
782.399 -> using that Google auto tagging add-on
784.74 -> and apply those for each of every every
787.139 -> one of your images but you can also do
788.94 -> this programmatically using the SDK but
791.459 -> now let's actually dive into building
792.72 -> that search where we're going to use
794.04 -> next.js server actions which is just to
796.38 -> be clear an alpha feature that's built
798.48 -> on top of react actions but we can see
800.7 -> here in this example we're going to
802.38 -> really just Define a function we're
803.82 -> going to make sure that we mark it as
805.74 -> we're using the server but then we can
808.019 -> use the server methods like the node STK
812.16 -> in order to make different requests or
814.32 -> perform different actions now ultimately
816.18 -> the way that works is we're going to
817.44 -> pass in that function as the action onto
820.5 -> a form element which is going to allow
822.48 -> us to actually invoke that action now
824.459 -> inside of my project the way that I'm
825.839 -> going to handle this is I have a form
827.579 -> where I have my search query input and
830.04 -> I'm going to add an action to that form
831.779 -> and let's call it search where then I'm
834.06 -> going to create my new async
837.06 -> function called search and inside is
840.12 -> where I'm going to perform that search
841.56 -> action now I'm not going to actually
843.48 -> make that search request inside of
845.579 -> search instead what I'm going to do is
847.44 -> I'm going to grab the form data from
849.42 -> this form I'm going to grab that query
851.88 -> I'm going to just append it as a
853.8 -> redirect to the page so that it's going
856.26 -> to revalidate that page it's going to
858.54 -> then use that to hit the expression that
861.54 -> I'm going to configure here and make the
863.339 -> search that way that way it's a little
864.959 -> bit more linear for how I'm going to
866.639 -> actually make this search request so
868.62 -> let's first set this up using use server
871.32 -> but then the way that we're going to get
873.18 -> our form data is the first parameter is
875.94 -> going to be this data object which is
878.22 -> ultimately going to be typed as form
879.779 -> data so what we can do is we can get the
881.82 -> query by running data.get where we get
884.639 -> it where we're going to grab that query
886.199 -> key from the form data now ultimately we
889.199 -> want to pass that into the URL so that
891.12 -> we can revalidate the page to trigger
893.16 -> that search request to do that we're
895.199 -> going to import the redirect method from
898.8 -> next navigation and once we have this
901.62 -> query data we're going to run that
903.36 -> redirect and we're going to just simply
904.92 -> stay on the page that we're on now
906.18 -> unless of course you want to redirect
907.74 -> this to a search page for instance and
909.72 -> I'm going to add a query parameter where
912.6 -> I'm going to pass in that data query
914.579 -> value now one final thing that we need
916.74 -> to do in order to take advantage of
918.3 -> these server actions is actually opt
920.339 -> into the server actions as I mentioned
922.74 -> this is still an alpha feature so we
924.899 -> want to make sure that we add this
926.16 -> experimental feature to our next config
927.899 -> so inside of our next config we're going
930.06 -> to set up the experimental property
933.18 -> we're inside we're going to say server
935.04 -> actions we're going to set that to true
936.959 -> but now when we try to load this page
939.18 -> and let's search for something like
940.92 -> rocket we can see that the page
943.26 -> refreshed now we currently show the same
945.42 -> results because we haven't tweaked that
946.86 -> search request at all but we now see
948.54 -> that query parameter in the URL so let's
950.519 -> actually start to build this expression
951.899 -> so I'm going to say let expression is
954.36 -> equal to and we're going to start off
956.1 -> with this folder of my gallery
959.88 -> so let's set that as a string I'm going
961.56 -> to then pass in that expression as the
963.48 -> value but we ultimately want to say if
965.639 -> we have a query which we're going to set
967.68 -> up in a second we want to append to this
970.56 -> expression we're going to say expression
972.72 -> is equal to expression and we're going
975.36 -> to say and we're going to say our search
978.36 -> of query now the nice thing with using
981 -> server components we can pretty easily
982.74 -> get that query parameter so the first
985.199 -> argument of our home props is going to
988.26 -> give us the search params which we need
991.139 -> to make sure we type this and let's call
993.06 -> this an object and search params I'm
996 -> just going to call this any for now but
997.259 -> inside the next.js documentation you can
999.3 -> find a better way to actually type this
1000.92 -> out but now we can say inside of search
1002.899 -> params which is just going to be an
1004.639 -> object we can say that this query
1006.8 -> constant query is equal to search
1009.1 -> params.query I'm going to move that
1010.94 -> above just for organizational sake but
1013.1 -> just to reiterate what's happening we're
1014.6 -> grabbing this query from our search
1016.339 -> params which we appended using that
1018.68 -> server action now once we have that
1020.779 -> we're going to append it dynamically to
1023.12 -> our expression our search expression
1025.04 -> which we're going to pass into that
1026.6 -> culinary search request and we can
1028.339 -> already see once the page updates that
1029.959 -> it has that rocket now but let's go back
1032 -> to the home page and if we make a new
1033.559 -> search such as what if I search for a
1035.36 -> guitar we're going to be able to see
1037.04 -> that we get that image with a guitar or
1039.26 -> if we search for something else like
1040.64 -> rocket again we get the rocket if we
1042.62 -> search for cat because I guess it didn't
1045.079 -> detect that image that's not going to
1046.339 -> show up but if I search for space we see
1048.5 -> all the images that kind of look like
1049.7 -> space even though this base jellyfish is
1051.919 -> that a space jellyfish it might be but
1053.78 -> we get the images that kind of deal with
1055.4 -> space now another method that we can use
1057.38 -> for these server actions is the clear
1059.059 -> where if we have a query and only if we
1061.4 -> have a query we want to be able to clear
1063.38 -> that query so first of all we're going
1065.36 -> to take that query and we're going to
1066.74 -> scroll down and if we have it we're or
1070.16 -> rather if we yeah if we have a query
1073.28 -> that's the only time we're going to
1075.26 -> actually show that clear button but on
1077.72 -> this form where we hold that clear we're
1080.419 -> going to add another action of clear
1082.4 -> where I'm going to just duplicate this
1084.5 -> search one I'm going to call it clear
1085.94 -> and but this time we're not going to use
1088.1 -> that form data we're just going to
1089.419 -> Simply send it back to that root page so
1092.179 -> now once I hit clear we can see it goes
1094.58 -> back to the initial page now this is a
1097.28 -> simple use case of server actions this
1099.08 -> might not even be the best use case for
1100.88 -> using this kind of functionality but
1102.74 -> it's a simple way to start to illustrate
1104.419 -> what's actually happening in that
1106.22 -> process of using the server actions and
1108.62 -> again I do want to make sure that it's
1109.94 -> clear that these are an alpha feature so
1112.34 -> it's super prone to breakage but it's
1114.14 -> fun to start to explore what different
1115.46 -> apis that we're going to have available
1116.9 -> and ultimately the fact that now that we
1119.419 -> have these server components it's much
1121.4 -> easier to be able to use different sdks
1123.62 -> like the node SDK for clownery in order
1126.26 -> to easily grab our assets manage our
1128.419 -> assets or do whatever we want to do
1129.799 -> inside of that same react environment
1132.38 -> that we know and love there's so much
1134.12 -> functionality to explore in the new app
1135.679 -> router that I feel like I'm really only
1136.94 -> scratching the surface what what's your
1138.98 -> favorite part of the new app router
1140.299 -> features is it server actions is it
1141.799 -> something else let me know in the
1142.82 -> comments if you want to explore other
1144.559 -> new app router features check out my
1146.36 -> video where I show you how to create a
1147.98 -> site map an RSS feed and even a static
1150.44 -> Json file using the static route
1152.179 -> handlers and that first class sitemap
1153.919 -> support and if you like this video make
1155.84 -> sure you hit thumbs up subscribe and
1157.52 -> click that little notification Bell for
1158.96 -> future web dev content thanks for
1160.7 -> watching

Source: https://www.youtube.com/watch?v=kK-XtyDuUD4