[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-nextjs-weekly-server-actions-mutations-security":44,"search-suggestions":490,"related-nextjs-weekly-server-actions-mutations-security":531,"code:tsx:true:xd49z":562,"code:tsx:true:130q9le":563,"code:tsx:true:1ic27c6":564,"code:ts:true:3qxkw6":565,"code:tsx:true:d7nrys":566,"code:tsx:true:epm6uy":567,"code:tsx:true:2yy7bh":568,"code:tsx:true:ttzer1":569,"code:tsx:true:ea5nv0":570,"comments-01a06d6c-41f6-77bd-95e3-cdd69a0f72eb":571},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":51,"likeCount":19,"commentCount":19,"author":53,"vertical":58,"topic":59,"tags":62,"_count":73,"playground":75,"body":77,"bodyMd":465,"seo":466,"translationGroupId":468,"series":469,"podcastUrl":48,"verticalId":5,"thread":478,"assessments":480,"translations":486,"quiz":488},"01a06d6c-41f6-77bd-95e3-cdd69a0f72eb","nextjs-weekly-server-actions-mutations-security","Next.js Server Actions: Mutations & Security (Cheat Sheet)",null,"Next.js Server Actions look like plain functions but compile to public POST endpoints. Learn the mutation flow, built-in CSRF checks, and the auth you owe.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-server-actions-mutations-security.png",15,"2026-09-08T10:57:00.975Z",{"id":54,"name":55,"username":56,"avatarUrl":48,"headline":57},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":60,"name":61},"nextjs","Nextjs",[63,64,67,70],{"slug":60,"name":61,"color":48},{"slug":65,"name":66,"color":48},"react","React",{"slug":68,"name":69,"color":48},"webdev","Webdev",{"slug":71,"name":72,"color":48},"tutorial","Tutorial",{"assessments":74},1,{"slug":46,"title":76},"Next.js Server Actions — request lifecycle playground",{"blocks":78,"version":74},[79,83,86,89,94,97,106,109,112,115,131,135,138,144,148,151,154,158,161,164,170,173,176,179,182,186,189,192,197,200,203,206,210,213,216,219,222,226,229,232,235,240,244,247,250,253,257,260,263,266,269,276,279,282,286,289,292,300,303,311,314,317,320,324,327,330,333,336,339,342,345,348,404,407,414,417,420,423,426,429,432,435,438,441,444,447,450,453,456,459],{"id":80,"html":81,"type":82},"b1","\u003Cp>You write a \u003Ccode>deleteComment(commentId)\u003C\u002Fcode> Server Action, wire it to a trash-can button, and it just works — no \u003Ccode>fetch\u003C\u002Fcode>, no \u003Ccode>\u002Fapi\u003C\u002Fcode> route, no \u003Ccode>JSON.stringify\u003C\u002Fcode>. It feels like the network disappeared, like you called a local function from a click handler. Ship it, move on.\u003C\u002Fp>","paragraph",{"id":84,"html":85,"type":82},"b2","\u003Cp>A week later someone opens their browser&#39;s dev tools, finds a \u003Ccode>POST\u003C\u002Fcode> request to your app with a long encrypted ID in the body, and replays it with a \u003Ccode>commentId\u003C\u002Fcode> that isn&#39;t theirs. It works. There was never a network boundary to disappear — you just couldn&#39;t see it. Every Server Action you write compiles into a real HTTP endpoint, and Next.js never assumed you&#39;d add the authorization check yourself.\u003C\u002Fp>",{"id":87,"html":88,"type":82},"b3","\u003Cp>This article is written against \u003Cstrong>Next.js 16.3\u003C\u002Fstrong> (verified against the framework&#39;s own release notes and npm&#39;s \u003Ccode>latest\u003C\u002Fcode> dist-tag in September 2026). Everything here — the \u003Ccode>&#39;use server&#39;\u003C\u002Fcode> directive, \u003Ccode>useActionState\u003C\u002Fcode>, \u003Ccode>useOptimistic\u003C\u002Fcode>, and the built-in CSRF protections — is current App Router behavior, not a Pages Router pattern in disguise.\u003C\u002Fp>",{"id":90,"html":91,"text":92,"type":93,"level":31},"b4","What you&#39;ll learn","What you'll learn","heading",{"id":95,"html":96,"type":82},"b5","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":98,"type":99,"items":100,"ordered":18},"b6","list",[101,102,103,104,105],"Explain what \u003Ccode>&#39;use server&#39;\u003C\u002Fcode> actually compiles into, and why a Server Action is a same-origin RPC endpoint rather than a function call","Build a full mutation flow: a progressively-enhanced form, pending and result state with \u003Ccode>useActionState\u003C\u002Fcode>, and an instant UI with \u003Ccode>useOptimistic\u003C\u002Fcode>","Invalidate the right data after a mutation with \u003Ccode>revalidatePath\u003C\u002Fcode> and \u003Ccode>revalidateTag\u003C\u002Fcode>, and avoid the \u003Ccode>redirect()\u003C\u002Fcode> inside \u003Ccode>try\u002Fcatch\u003C\u002Fcode> trap","Describe exactly what Next.js secures for you (CSRF origin checks, encrypted action IDs, encrypted closures) and what it deliberately leaves to you (authentication, authorization, input validation)","Decide when a Server Action is the right tool and when a Route Handler still is",{"id":107,"html":108,"text":108,"type":93,"level":31},"b7","Who this is for",{"id":110,"html":111,"type":82},"b8","\u003Cp>You&#39;ve built at least a small App Router project — a \u003Ccode>page.tsx\u003C\u002Fcode>, maybe a form that posts to an API route. You don&#39;t need prior experience with Server Actions; we build the model from nothing.\u003C\u002Fp>",{"id":113,"html":114,"text":114,"type":93,"level":31},"b9","Table of contents",{"id":116,"type":99,"items":117,"ordered":18},"b10",[118,119,120,121,122,123,124,125,126,127,128,129,130],"\u003Ca href=\"#the-problem-a-function-call-that-isnt-one\">The problem: a function call that isn&#39;t one\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-an-rpc-endpoint-wearing-a-functions-clothes\">The mental model: an RPC endpoint wearing a function&#39;s clothes\u003C\u002Fa>","\u003Ca href=\"#stage-1-defining-a-server-action\">Stage 1: defining a Server Action\u003C\u002Fa>","\u003Ca href=\"#stage-2-wiring-it-to-a-form\">Stage 2: wiring it to a form\u003C\u002Fa>","\u003Ca href=\"#stage-3-pending-and-result-state-with-useactionstate\">Stage 3: pending and result state with useActionState\u003C\u002Fa>","\u003Ca href=\"#stage-4-invalidating-data-after-a-mutation\">Stage 4: invalidating data after a mutation\u003C\u002Fa>","\u003Ca href=\"#stage-5-instant-ui-with-useoptimistic\">Stage 5: instant UI with useOptimistic\u003C\u002Fa>","\u003Ca href=\"#stage-6-what-nextjs-secures-for-you\">Stage 6: what Next.js secures for you\u003C\u002Fa>","\u003Ca href=\"#stage-7-what-you-still-owe-it\">Stage 7: what you still owe it\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>",{"id":132,"html":133,"text":134,"type":93,"level":31},"b11","The problem: a function call that isn&#39;t one","The problem: a function call that isn't one",{"id":136,"html":137,"type":82},"b12","\u003Cp>Here&#39;s the naïve version of that \u003Ccode>deleteComment\u003C\u002Fcode> action — the one that looks completely reasonable in a code review:\u003C\u002Fp>",{"id":139,"code":140,"type":141,"language":142,"highlight":143},"b13","\u002F\u002F app\u002Factions.ts\n'use server';\n\nimport { db } from '@\u002Flib\u002Fdb';\nimport { revalidatePath } from 'next\u002Fcache';\n\nexport async function deleteComment(commentId: string) {\n  await db.comment.delete({ where: { id: commentId } });\n  revalidatePath('\u002Fposts');\n}","code","tsx",[],{"id":145,"code":146,"type":141,"language":142,"highlight":147},"b14","\u002F\u002F A button inside a Client Component\n\u003Cbutton onClick={() => deleteComment(comment.id)}>Delete\u003C\u002Fbutton>",[],{"id":149,"html":150,"type":82},"b15","\u003Cp>Nothing here checks \u003Cem>who\u003C\u002Fem> is asking. The function trusts its caller the way a same-process function normally can — because on the page, it looks like one. But \u003Ccode>&#39;use server&#39;\u003C\u002Fcode> doesn&#39;t keep this code on the server in some abstract sense; it publishes it as a callable endpoint the client can reach. Open the Network tab after clicking Delete and you&#39;ll see a \u003Ccode>POST\u003C\u002Fcode> to your app&#39;s own origin, carrying an encrypted reference to this exact function and its argument. Any client that can construct that same request — not just your button — can call it, with any \u003Ccode>commentId\u003C\u002Fcode> it likes.\u003C\u002Fp>",{"id":152,"html":153,"type":82},"b16","\u003Cp>The bug isn&#39;t that Server Actions are insecure. It&#39;s that the syntax hides the network call so well that it&#39;s easy to forget one exists, and skip the check you&#39;d never skip in a hand-written API route.\u003C\u002Fp>",{"id":155,"html":156,"text":157,"type":93,"level":31},"b17","The mental model: an RPC endpoint wearing a function&#39;s clothes","The mental model: an RPC endpoint wearing a function's clothes",{"id":159,"html":160,"type":82},"b18","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a Server Action is not code that &quot;runs on the server instead of the client.&quot; It&#39;s an RPC (remote procedure call) — a function whose \u003Cem>body\u003C\u002Fem> runs on the server, but whose \u003Cem>invocation\u003C\u002Fem> is a real HTTP request from whatever calls it, same as \u003Ccode>fetch(&quot;\u002Fapi\u002Fcomments\u002F123&quot;, { method: &quot;DELETE&quot; })\u003C\u002Fcode> would be.\u003C\u002Fp>",{"id":162,"html":163,"type":82},"b19","\u003Cp>When you mark a function with \u003Ccode>&#39;use server&#39;\u003C\u002Fcode>, the Next.js compiler:\u003C\u002Fp>",{"id":165,"type":99,"items":166,"ordered":17},"b20",[167,168,169],"Leaves the function&#39;s body on the server, and strips it entirely out of the client JavaScript bundle.","Replaces every reference to it in client code with an encrypted, opaque ID.","Registers a server-side handler that, given that ID and a serialized argument list, finds the matching function and runs it.",{"id":171,"html":172,"type":82},"b21","\u003Cp>Calling \u003Ccode>deleteComment(comment.id)\u003C\u002Fcode> from a click handler, under the hood, sends a \u003Ccode>POST\u003C\u002Fcode> request carrying that ID and the arguments, and awaits the response. The syntax reads like a function call because React and Next.js serialize the request and deserialize the response for you — but the trust boundary is exactly where it would be for a REST endpoint. Nothing about \u003Cem>who is asking\u003C\u002Fem> crosses that boundary automatically. That&#39;s the one fact this whole article hangs off.\u003C\u002Fp>",{"id":174,"html":175,"text":175,"type":93,"level":31},"b22","Stage 1: defining a Server Action",{"id":177,"html":178,"type":82},"b23","\u003Cp>There are two ways to mark a function as a Server Action, and they mean different things.\u003C\u002Fp>",{"id":180,"html":181,"type":82},"b24","\u003Cp>\u003Cstrong>Inline, inside a Server Component\u003C\u002Fstrong>, \u003Ccode>&#39;use server&#39;\u003C\u002Fcode> goes at the top of the function body:\u003C\u002Fp>",{"id":183,"code":184,"type":141,"language":142,"highlight":185},"b25","\u002F\u002F app\u002Fposts\u002F[id]\u002Fpage.tsx — a Server Component\nexport default function PostPage({ params }: { params: { id: string } }) {\n  async function likePost() {\n    'use server';\n    await db.post.update({ where: { id: params.id }, data: { likes: { increment: 1 } } });\n  }\n\n  return \u003Cform action={likePost}>\u003Cbutton>Like\u003C\u002Fbutton>\u003C\u002Fform>;\n}",[],{"id":187,"html":188,"type":82},"b26","\u003Cp>This action closes over \u003Ccode>params.id\u003C\u002Fcode> from its surrounding scope — a real convenience, and one that matters later in the security section.\u003C\u002Fp>",{"id":190,"html":191,"type":82},"b27","\u003Cp>\u003Cstrong>At the top of a separate file\u003C\u002Fstrong>, \u003Ccode>&#39;use server&#39;\u003C\u002Fcode> on line one marks \u003Cem>every exported function in that file\u003C\u002Fem> as a Server Action:\u003C\u002Fp>",{"id":193,"code":194,"type":141,"language":195,"highlight":196},"b28","\u002F\u002F app\u002Factions.ts\n'use server';\n\nexport async function deleteComment(commentId: string) { \u002F* … *\u002F }\nexport async function likePost(postId: string) { \u002F* … *\u002F }","ts",[],{"id":198,"html":199,"type":82},"b29","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> a Client Component can never define an inline Server Action — it can only \u003Cem>import\u003C\u002Fem> one from a \u003Ccode>&#39;use server&#39;\u003C\u002Fcode> file. If a component needs to call a mutation from an \u003Ccode>onClick\u003C\u002Fcode>, that mutation has to live in its own server-only module.\u003C\u002Fp>",{"id":201,"html":202,"text":202,"type":93,"level":31},"b30","Stage 2: wiring it to a form",{"id":204,"html":205,"type":82},"b31","\u003Cp>The idiomatic entry point is a \u003Ccode>&lt;form&gt;\u003C\u002Fcode>&#39;s \u003Ccode>action\u003C\u002Fcode> prop, not a click handler:\u003C\u002Fp>",{"id":207,"code":208,"type":141,"language":142,"highlight":209},"b32","import { deleteComment } from '@\u002Fapp\u002Factions';\n\nexport function CommentRow({ comment }: { comment: Comment }) {\n  return (\n    \u003Cform action={deleteComment.bind(null, comment.id)}>\n      \u003Cbutton type=\"submit\">Delete\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}",[],{"id":211,"html":212,"type":82},"b33","\u003Cp>\u003Ccode>.bind(null, comment.id)\u003C\u002Fcode> pre-supplies the argument so the form doesn&#39;t need a hidden input for it — the bound value travels inside the encrypted action payload, not as plain form data.\u003C\u002Fp>",{"id":214,"html":215,"type":82},"b34","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> because this is a real \u003Ccode>&lt;form&gt;\u003C\u002Fcode>, it works before React hydrates and even with JavaScript disabled — the browser submits it as a normal POST and Next.js handles the round trip. That&#39;s progressive enhancement you get for free, and it&#39;s a strong reason to prefer \u003Ccode>action={}\u003C\u002Fcode> over an \u003Ccode>onClick\u003C\u002Fcode> that calls the function directly.\u003C\u002Fp>",{"id":217,"html":218,"text":218,"type":93,"level":31},"b35","Stage 3: pending and result state with useActionState",{"id":220,"html":221,"type":82},"b36","\u003Cp>A raw form submission doesn&#39;t give you a pending spinner or an error message. \u003Ccode>useActionState\u003C\u002Fcode> (a React 19 hook; it replaced the older \u003Ccode>useFormState\u003C\u002Fcode>) wraps an action and gives you both:\u003C\u002Fp>",{"id":223,"code":224,"type":141,"language":142,"highlight":225},"b37","'use client';\nimport { useActionState } from 'react';\nimport { createComment } from '@\u002Fapp\u002Factions';\n\nconst initialState = { error: null as string | null };\n\nexport function CommentForm({ postId }: { postId: string }) {\n  const [state, formAction, isPending] = useActionState(\n    async (prevState: typeof initialState, formData: FormData) => {\n      const text = formData.get('text');\n      if (typeof text !== 'string' || text.trim().length === 0) {\n        return { error: 'Comment cannot be empty.' };\n      }\n      await createComment(postId, text);\n      return { error: null };\n    },\n    initialState,\n  );\n\n  return (\n    \u003Cform action={formAction}>\n      \u003Ctextarea name=\"text\" disabled={isPending} \u002F>\n      \u003Cbutton disabled={isPending}>{isPending ? 'Posting…' : 'Post comment'}\u003C\u002Fbutton>\n      {state.error && \u003Cp role=\"alert\">{state.error}\u003C\u002Fp>}\n    \u003C\u002Fform>\n  );\n}",[],{"id":227,"html":228,"type":82},"b38","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> the function you pass to \u003Ccode>useActionState\u003C\u002Fcode> receives the \u003Cem>previous state\u003C\u002Fem> as its first argument and the submitted \u003Ccode>FormData\u003C\u002Fcode> as its second, and whatever it returns becomes the new state on the next render. That&#39;s how a form gets validation feedback without a separate \u003Ccode>useState\u003C\u002Fcode> and a manual \u003Ccode>fetch\u003C\u002Fcode>.\u003C\u002Fp>",{"id":230,"html":231,"text":231,"type":93,"level":31},"b39","Stage 4: invalidating data after a mutation",{"id":233,"html":234,"type":82},"b40","\u003Cp>A mutation that doesn&#39;t invalidate anything leaves stale data on screen. Two functions from \u003Ccode>next\u002Fcache\u003C\u002Fcode> handle this:\u003C\u002Fp>",{"id":236,"type":99,"items":237,"ordered":18},"b41",[238,239],"\u003Ccode>revalidatePath(&#39;\u002Fposts&#39;)\u003C\u002Fcode> — throws away the cached render for that path (and re-renders it on next visit).","\u003Ccode>revalidateTag(&#39;comments&#39;)\u003C\u002Fcode> — throws away every cached entry tagged \u003Ccode>&#39;comments&#39;\u003C\u002Fcode>, wherever it lives, which pairs directly with the \u003Ccode>cacheTag(&#39;comments&#39;)\u003C\u002Fcode> call inside the cached function that produced it. (We covered \u003Ccode>cacheTag\u003C\u002Fcode> and the caching layers themselves in \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob\">Cache Components Explained\u003C\u002Fa> — this article assumes you have somewhere to invalidate \u003Cem>into\u003C\u002Fem>, not how that cache is built.)",{"id":241,"code":242,"type":141,"language":142,"highlight":243},"b42","'use server';\nimport { revalidateTag } from 'next\u002Fcache';\n\nexport async function createComment(postId: string, text: string) {\n  await db.comment.create({ data: { postId, text } });\n  revalidateTag('comments');\n}",[],{"id":245,"html":246,"type":82},"b43","\u003Cp>If the mutation should also navigate — say, after creating a post — call \u003Ccode>redirect()\u003C\u002Fcode> from \u003Ccode>next\u002Fnavigation\u003C\u002Fcode>. It belongs at the end of the action, never inside a \u003Ccode>try\u003C\u002Fcode> block (see \u003Ca href=\"#edge-cases-and-gotchas\">Edge cases\u003C\u002Fa>).\u003C\u002Fp>",{"id":248,"html":249,"text":249,"type":93,"level":31},"b44","Stage 5: instant UI with useOptimistic",{"id":251,"html":252,"type":82},"b45","\u003Cp>Waiting for a round trip before showing a &quot;liked&quot; heart feels slow. \u003Ccode>useOptimistic\u003C\u002Fcode> lets you render the \u003Cem>assumed\u003C\u002Fem> result immediately, then reconcile once the action resolves:\u003C\u002Fp>",{"id":254,"code":255,"type":141,"language":142,"highlight":256},"b46","'use client';\nimport { useOptimistic } from 'react';\nimport { likePost } from '@\u002Fapp\u002Factions';\n\nexport function LikeButton({ postId, likes }: { postId: string; likes: number }) {\n  const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (state) => state + 1);\n\n  return (\n    \u003Cform\n      action={async () => {\n        addOptimisticLike(undefined);\n        await likePost(postId);\n      }}\n    >\n      \u003Cbutton>❤️ {optimisticLikes}\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}",[],{"id":258,"html":259,"type":82},"b47","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> the optimistic value rolls back automatically only if the action \u003Cem>throws\u003C\u002Fem>. If your action instead catches its own error and returns a value, the optimistic state sticks around until the real props change — so a Server Action backing a \u003Ccode>useOptimistic\u003C\u002Fcode> update should let real failures propagate, not swallow them into a returned \u003Ccode>{ error }\u003C\u002Fcode> object the way Stage 3&#39;s form does.\u003C\u002Fp>",{"id":261,"html":262,"text":262,"type":93,"level":31},"b48","Stage 6: what Next.js secures for you",{"id":264,"html":265,"type":82},"b49","\u003Cp>This is the part worth being precise about, because getting it wrong in either direction is expensive — either you re-invent protections that already exist, or you assume protections that don&#39;t.\u003C\u002Fp>",{"id":267,"html":268,"type":82},"b50","\u003Cp>Next.js secures the \u003Cstrong>transport\u003C\u002Fstrong>:\u003C\u002Fp>",{"id":270,"type":99,"items":271,"ordered":18},"b51",[272,273,274,275],"\u003Cstrong>CSRF protection is automatic.\u003C\u002Fstrong> A Server Action request only succeeds if its \u003Ccode>Origin\u003C\u002Fcode> header matches the app&#39;s own \u003Ccode>Host\u003C\u002Fcode> (or \u003Ccode>X-Forwarded-Host\u003C\u002Fcode> behind a proxy). A cross-site form or script trying to trigger your action from another origin gets rejected before your code runs. If you sit behind a reverse proxy or CDN on a different domain, add it to \u003Ccode>experimental.serverActions.allowedOrigins\u003C\u002Fcode> in \u003Ccode>next.config.ts\u003C\u002Fcode> — otherwise your own legitimate traffic gets blocked.","\u003Cstrong>Action IDs are encrypted and non-deterministic\u003C\u002Fstrong>, recalculated between builds, so they can&#39;t be guessed or reused across deployments.","\u003Cstrong>Only referenced functions ship at all.\u003C\u002Fstrong> An exported Server Action your client code never calls is stripped from the client bundle entirely — it has no public endpoint.","\u003Cstrong>Closed-over values are encrypted.\u003C\u002Fstrong> In Stage 1&#39;s inline example, \u003Ccode>params.id\u003C\u002Fcode> is captured from the surrounding scope; Next.js encrypts that captured value before it round-trips to the client and back, so it isn&#39;t readable or tamperable in the browser. Self-hosting on multiple instances needs a stable \u003Ccode>NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\u003C\u002Fcode> shared across them, or instances can&#39;t decrypt each other&#39;s action payloads.",{"id":277,"html":278,"text":278,"type":93,"level":31},"b52","Stage 7: what you still owe it",{"id":280,"html":281,"type":82},"b53","\u003Cp>None of the above answers \u003Cem>who is allowed to call this\u003C\u002Fem>. That&#39;s Stage 7, and it&#39;s on you, exactly as it would be inside a Route Handler:\u003C\u002Fp>",{"id":283,"code":284,"type":141,"language":142,"highlight":285},"b54","'use server';\nimport { auth } from '@\u002Flib\u002Fauth';\n\nexport async function deleteComment(commentId: string) {\n  const session = await auth();\n  if (!session) throw new Error('Not authenticated.');\n\n  const comment = await db.comment.findUnique({ where: { id: commentId } });\n  if (!comment || comment.authorId !== session.userId) {\n    throw new Error('Not authorized.');\n  }\n\n  await db.comment.delete({ where: { id: commentId } });\n  revalidatePath('\u002Fposts');\n}",[],{"id":287,"html":288,"type":82},"b55","\u003Cp>Treat every Server Action as if it were a \u003Ccode>POST\u003C\u002Fcode> handler a stranger could call directly with a tool like \u003Ccode>curl\u003C\u002Fcode> — because, protected transport aside, that&#39;s exactly what it is. Check the session, check ownership of whatever&#39;s being mutated, and validate the input shape (a schema library like Zod on the \u003Ccode>FormData\u003C\u002Fcode> fields is the idiomatic App Router pattern) before touching the database.\u003C\u002Fp>",{"id":290,"html":291,"text":291,"type":93,"level":31},"b56","Edge cases and gotchas",{"id":293,"type":99,"items":294,"ordered":18},"b57",[295,296,297,298,299],"\u003Cstrong>\u003Ccode>redirect()\u003C\u002Fcode> inside a \u003Ccode>try\u002Fcatch\u003C\u002Fcode> gets swallowed.\u003C\u002Fstrong> \u003Ccode>redirect()\u003C\u002Fcode> works by throwing a special internal signal that Next.js catches higher up the tree. If you call it inside a \u003Ccode>try\u003C\u002Fcode> block, your own \u003Ccode>catch\u003C\u002Fcode> intercepts that signal first and treats it like a normal error. Call \u003Ccode>redirect()\u003C\u002Fcode> after the \u003Ccode>try\u002Fcatch\u003C\u002Fcode> finishes, not inside it.","\u003Cstrong>A Client Component cannot define an inline action.\u003C\u002Fstrong> Only a Server Component function body can hold \u003Ccode>&#39;use server&#39;\u003C\u002Fcode> inline; a Client Component must import the action from a server-only file, as in Stage 2.","\u003Cstrong>Optimistic state doesn&#39;t self-correct on a caught error.\u003C\u002Fstrong> As noted in Stage 5, \u003Ccode>useOptimistic\u003C\u002Fcode> only rolls back when the wrapping action throws — a caught-and-returned error leaves the optimistic UI stuck until real props update.","\u003Cstrong>Server Actions aren&#39;t cached like \u003Ccode>fetch\u003C\u002Fcode> or \u003Ccode>use cache\u003C\u002Fcode> data.\u003C\u002Fstrong> They&#39;re mutations, not reads; caching applies to what you read afterward, invalidated via \u003Ccode>revalidatePath\u003C\u002Fcode>\u002F\u003Ccode>revalidateTag\u003C\u002Fcode>, not to the action call itself.","\u003Cstrong>Rate limiting is not built in.\u003C\u002Fstrong> The CSRF origin check stops cross-site abuse; it does nothing to stop a signed-in user from calling your action a thousand times a second. Add your own limiter (per-user, per-IP, or both) for anything sensitive.",{"id":301,"html":302,"text":302,"type":93,"level":31},"b58","Best practices",{"id":304,"type":99,"items":305,"ordered":18},"b59",[306,307,308,309,310],"\u003Cstrong>Reach for a Server Action\u003C\u002Fstrong> for form-driven mutations inside your own app — creating, updating, deleting data the user is looking at right now. Progressive enhancement and the built-in CSRF handling make it the right default there.","\u003Cstrong>Reach for a Route Handler instead\u003C\u002Fstrong> when the caller isn&#39;t a form in your app: a webhook from a third party, a public API consumed by non-browser clients, or anything that needs a stable, documented URL and method rather than an internal action reference.","\u003Cstrong>Validate input with a schema\u003C\u002Fstrong>, not ad-hoc \u003Ccode>if\u003C\u002Fcode> checks — \u003Ccode>FormData\u003C\u002Fcode> gives you strings and files, never trust the shape.","\u003Cstrong>Check auth and ownership first, mutate second.\u003C\u002Fstrong> Fail fast, before touching the database.","\u003Cstrong>Pair every \u003Ccode>cacheTag\u003C\u002Fcode> with the \u003Ccode>revalidateTag\u003C\u002Fcode> call that invalidates it\u003C\u002Fstrong>, and keep that pairing close together in the codebase so it&#39;s obvious which mutation clears which cache.",{"id":312,"html":313,"text":313,"type":93,"level":31},"b60","FAQ",{"id":315,"html":316,"text":316,"type":93,"level":43},"b61","Do Server Actions replace API Routes entirely?",{"id":318,"html":319,"type":82},"b62","\u003Cp>No. They cover form-driven mutations from your own app&#39;s UI well; a public API, a webhook receiver, or a non-browser client still wants a Route Handler with a stable URL.\u003C\u002Fp>",{"id":321,"html":322,"text":323,"type":93,"level":43},"b63","Is \u003Ccode>useActionState\u003C\u002Fcode> the same as \u003Ccode>useFormState\u003C\u002Fcode>?","Is useActionState the same as useFormState?",{"id":325,"html":326,"type":82},"b64","\u003Cp>\u003Ccode>useActionState\u003C\u002Fcode> is \u003Ccode>useFormState\u003C\u002Fcode>&#39;s React 19 successor — the same shape (previous state in, new state out, plus a \u003Ccode>pending\u003C\u002Fcode> flag), under a name that reflects it isn&#39;t limited to forms. \u003Ccode>useFormState\u003C\u002Fcode> still works in the interim but is deprecated in favor of it.\u003C\u002Fp>",{"id":328,"html":329,"text":329,"type":93,"level":43},"b65","Are Server Actions secure by default?",{"id":331,"html":332,"type":82},"b66","\u003Cp>The transport is: CSRF origin checks, encrypted action IDs, and encrypted closures all happen automatically. Authorization is not — every action still needs its own auth and ownership checks, the same as any endpoint you&#39;d hand-write.\u003C\u002Fp>",{"id":334,"html":335,"text":335,"type":93,"level":43},"b67","Can a Server Action be called from outside my app?",{"id":337,"html":338,"type":82},"b68","\u003Cp>Only if your code lets it. The CSRF origin check blocks requests whose \u003Ccode>Origin\u003C\u002Fcode> doesn&#39;t match your app&#39;s \u003Ccode>Host\u003C\u002Fcode> (or an explicitly configured allowed origin), so a script on another site can&#39;t trigger it. A request crafted directly against your own origin — from \u003Ccode>curl\u003C\u002Fcode>, from your own signed-in browser session — still reaches the function, which is exactly why Stage 7&#39;s checks matter.\u003C\u002Fp>",{"id":340,"html":341,"text":341,"type":93,"level":43},"b69","Why did my optimistic update stay stuck after an error?",{"id":343,"html":344,"type":82},"b70","\u003Cp>Almost always because the action caught its own error and returned a value instead of throwing. \u003Ccode>useOptimistic\u003C\u002Fcode> reverts on a thrown error, not on a returned one — see Stage 5.\u003C\u002Fp>",{"id":346,"html":347,"text":347,"type":93,"level":31},"b71","Cheat sheet",{"id":349,"head":350,"rows":354,"type":403},"b72",[351,352,353],"Task","Code","Notes",[355,359,363,367,371,375,379,383,387,391,395,399],[356,357,358],"Define inline (Server Component only)","\u003Ccode>async function f() { &#39;use server&#39;; … }\u003C\u002Fcode>","Can close over component scope; values are encrypted in transit.",[360,361,362],"Define in a shared file","\u003Ccode>&#39;use server&#39;\u003C\u002Fcode> at top of the file","Every export in that file becomes a Server Action.",[364,365,366],"Call from a form","\u003Ccode>&lt;form action={myAction}&gt;\u003C\u002Fcode>","Works before hydration; prefer over manual \u003Ccode>onClick\u003C\u002Fcode> + call.",[368,369,370],"Pre-bind an argument","\u003Ccode>myAction.bind(null, id)\u003C\u002Fcode>","Bound value ships inside the encrypted payload, not a hidden input.",[372,373,374],"Pending + result state","\u003Ccode>useActionState(fn, initialState)\u003C\u002Fcode> → \u003Ccode>[state, formAction, isPending]\u003C\u002Fcode>","React 19; successor to \u003Ccode>useFormState\u003C\u002Fcode>.",[376,377,378],"Pending flag only","\u003Ccode>useFormStatus()\u003C\u002Fcode> inside a form&#39;s child","Reads the nearest parent \u003Ccode>&lt;form&gt;\u003C\u002Fcode>&#39;s submission state.",[380,381,382],"Instant UI","\u003Ccode>useOptimistic(value, reducer)\u003C\u002Fcode>","Rolls back only if the action \u003Cstrong>throws\u003C\u002Fstrong>, not on a returned error.",[384,385,386],"Invalidate a route&#39;s cache","\u003Ccode>revalidatePath(&#39;\u002Fposts&#39;)\u003C\u002Fcode>","From \u003Ccode>next\u002Fcache\u003C\u002Fcode>, called inside the action.",[388,389,390],"Invalidate by tag","\u003Ccode>revalidateTag(&#39;comments&#39;)\u003C\u002Fcode>","Pairs with \u003Ccode>cacheTag(&#39;comments&#39;)\u003C\u002Fcode> on the read side.",[392,393,394],"Navigate after mutating","\u003Ccode>redirect(&#39;\u002Fposts&#39;)\u003C\u002Fcode>","From \u003Ccode>next\u002Fnavigation\u003C\u002Fcode>; call \u003Cstrong>outside\u003C\u002Fstrong> any \u003Ccode>try\u002Fcatch\u003C\u002Fcode>.",[396,397,398],"Cross-origin protection","Automatic (Origin vs Host)","Extra trusted origins via \u003Ccode>experimental.serverActions.allowedOrigins\u003C\u002Fcode>.",[400,401,402],"Auth check","Your own code, every action","Never assumed by the framework — treat it like an API route.","table",{"id":405,"html":406,"text":406,"type":93,"level":31},"b73","Key takeaways",{"id":408,"type":99,"items":409,"ordered":18},"b74",[410,411,412,413],"A Server Action is an RPC call to a real, encrypted-ID-backed HTTP endpoint — not a function call that happens to run elsewhere.","Next.js secures the transport (CSRF origin checks, encrypted IDs, encrypted closures); it never secures \u003Cem>who is allowed to call this\u003C\u002Fem> — that&#39;s your job, on every action, every time.","\u003Ccode>useActionState\u003C\u002Fcode> gives you pending\u002Fresult state, \u003Ccode>useOptimistic\u003C\u002Fcode> gives you instant UI, and \u003Ccode>revalidatePath\u003C\u002Fcode>\u002F\u003Ccode>revalidateTag\u003C\u002Fcode> clear the cache the mutation just invalidated.","\u003Ccode>redirect()\u003C\u002Fcode> throws internally — keep it out of \u003Ccode>try\u002Fcatch\u003C\u002Fcode>, or your own \u003Ccode>catch\u003C\u002Fcode> will swallow the navigation.",{"id":415,"html":416,"type":82},"b75","\u003Cp>That \u003Ccode>deleteComment\u003C\u002Fcode> bug from the top of this article has a one-line fix: a session and ownership check before the \u003Ccode>db.comment.delete\u003C\u002Fcode> call. The syntax will never remind you it&#39;s missing — the request in your Network tab is the only thing that will.\u003C\u002Fp>",{"id":418,"html":419,"type":82},"b76","\u003Cp>What&#39;s one Server Action in your own codebase you&#39;d want to re-check for an authorization gap after reading this? Drop it in the comments.\u003C\u002Fp>",{"id":421,"html":422,"type":82},"b77","\u003C!-- playground:start -->",{"id":424,"html":425,"text":425,"type":93,"level":31},"b78","🎮 Try it yourself",{"id":427,"html":428,"type":82},"b79","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":430,"html":431,"type":82},"b80","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":433,"html":434,"type":82},"b81","\u003C!-- playground:end -->",{"id":436,"html":437,"type":82},"b82","\u003C!-- quiz:start -->",{"id":439,"html":440,"text":440,"type":93,"level":31},"b83","🧠 Test yourself",{"id":442,"html":443,"type":82},"b84","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":445,"html":446,"type":82},"b85","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":448,"html":449,"type":82},"b86","\u003C!-- quiz:end -->",{"id":451,"type":452},"b87","divider",{"id":454,"html":455,"type":82},"b88","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":457,"html":458,"type":82},"b89","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":460,"type":99,"items":461,"ordered":18},"b90",[462,463,464],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","You write a `deleteComment(commentId)` Server Action, wire it to a trash-can button, and it just works — no `fetch`, no `\u002Fapi` route, no `JSON.stringify`. It feels like the network disappeared, like you called a local function from a click handler. Ship it, move on.\n\nA week later someone opens their browser's dev tools, finds a `POST` request to your app with a long encrypted ID in the body, and replays it with a `commentId` that isn't theirs. It works. There was never a network boundary to disappear — you just couldn't see it. Every Server Action you write compiles into a real HTTP endpoint, and Next.js never assumed you'd add the authorization check yourself.\n\nThis article is written against **Next.js 16.3** (verified against the framework's own release notes and npm's `latest` dist-tag in September 2026). Everything here — the `'use server'` directive, `useActionState`, `useOptimistic`, and the built-in CSRF protections — is current App Router behavior, not a Pages Router pattern in disguise.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain what `'use server'` actually compiles into, and why a Server Action is a same-origin RPC endpoint rather than a function call\n- Build a full mutation flow: a progressively-enhanced form, pending and result state with `useActionState`, and an instant UI with `useOptimistic`\n- Invalidate the right data after a mutation with `revalidatePath` and `revalidateTag`, and avoid the `redirect()` inside `try\u002Fcatch` trap\n- Describe exactly what Next.js secures for you (CSRF origin checks, encrypted action IDs, encrypted closures) and what it deliberately leaves to you (authentication, authorization, input validation)\n- Decide when a Server Action is the right tool and when a Route Handler still is\n\n## Who this is for\n\nYou've built at least a small App Router project — a `page.tsx`, maybe a form that posts to an API route. You don't need prior experience with Server Actions; we build the model from nothing.\n\n## Table of contents\n\n- [The problem: a function call that isn't one](#the-problem-a-function-call-that-isnt-one)\n- [The mental model: an RPC endpoint wearing a function's clothes](#the-mental-model-an-rpc-endpoint-wearing-a-functions-clothes)\n- [Stage 1: defining a Server Action](#stage-1-defining-a-server-action)\n- [Stage 2: wiring it to a form](#stage-2-wiring-it-to-a-form)\n- [Stage 3: pending and result state with useActionState](#stage-3-pending-and-result-state-with-useactionstate)\n- [Stage 4: invalidating data after a mutation](#stage-4-invalidating-data-after-a-mutation)\n- [Stage 5: instant UI with useOptimistic](#stage-5-instant-ui-with-useoptimistic)\n- [Stage 6: what Next.js secures for you](#stage-6-what-nextjs-secures-for-you)\n- [Stage 7: what you still owe it](#stage-7-what-you-still-owe-it)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n\n## The problem: a function call that isn't one\n\nHere's the naïve version of that `deleteComment` action — the one that looks completely reasonable in a code review:\n\n```tsx\n\u002F\u002F app\u002Factions.ts\n'use server';\n\nimport { db } from '@\u002Flib\u002Fdb';\nimport { revalidatePath } from 'next\u002Fcache';\n\nexport async function deleteComment(commentId: string) {\n  await db.comment.delete({ where: { id: commentId } });\n  revalidatePath('\u002Fposts');\n}\n```\n\n```tsx\n\u002F\u002F A button inside a Client Component\n\u003Cbutton onClick={() => deleteComment(comment.id)}>Delete\u003C\u002Fbutton>\n```\n\nNothing here checks *who* is asking. The function trusts its caller the way a same-process function normally can — because on the page, it looks like one. But `'use server'` doesn't keep this code on the server in some abstract sense; it publishes it as a callable endpoint the client can reach. Open the Network tab after clicking Delete and you'll see a `POST` to your app's own origin, carrying an encrypted reference to this exact function and its argument. Any client that can construct that same request — not just your button — can call it, with any `commentId` it likes.\n\nThe bug isn't that Server Actions are insecure. It's that the syntax hides the network call so well that it's easy to forget one exists, and skip the check you'd never skip in a hand-written API route.\n\n## The mental model: an RPC endpoint wearing a function's clothes\n\n**The mental model:** a Server Action is not code that \"runs on the server instead of the client.\" It's an RPC (remote procedure call) — a function whose *body* runs on the server, but whose *invocation* is a real HTTP request from whatever calls it, same as `fetch(\"\u002Fapi\u002Fcomments\u002F123\", { method: \"DELETE\" })` would be.\n\nWhen you mark a function with `'use server'`, the Next.js compiler:\n\n1. Leaves the function's body on the server, and strips it entirely out of the client JavaScript bundle.\n2. Replaces every reference to it in client code with an encrypted, opaque ID.\n3. Registers a server-side handler that, given that ID and a serialized argument list, finds the matching function and runs it.\n\nCalling `deleteComment(comment.id)` from a click handler, under the hood, sends a `POST` request carrying that ID and the arguments, and awaits the response. The syntax reads like a function call because React and Next.js serialize the request and deserialize the response for you — but the trust boundary is exactly where it would be for a REST endpoint. Nothing about *who is asking* crosses that boundary automatically. That's the one fact this whole article hangs off.\n\n## Stage 1: defining a Server Action\n\nThere are two ways to mark a function as a Server Action, and they mean different things.\n\n**Inline, inside a Server Component**, `'use server'` goes at the top of the function body:\n\n```tsx\n\u002F\u002F app\u002Fposts\u002F[id]\u002Fpage.tsx — a Server Component\nexport default function PostPage({ params }: { params: { id: string } }) {\n  async function likePost() {\n    'use server';\n    await db.post.update({ where: { id: params.id }, data: { likes: { increment: 1 } } });\n  }\n\n  return \u003Cform action={likePost}>\u003Cbutton>Like\u003C\u002Fbutton>\u003C\u002Fform>;\n}\n```\n\nThis action closes over `params.id` from its surrounding scope — a real convenience, and one that matters later in the security section.\n\n**At the top of a separate file**, `'use server'` on line one marks *every exported function in that file* as a Server Action:\n\n```ts\n\u002F\u002F app\u002Factions.ts\n'use server';\n\nexport async function deleteComment(commentId: string) { \u002F* … *\u002F }\nexport async function likePost(postId: string) { \u002F* … *\u002F }\n```\n\n**Key concept:** a Client Component can never define an inline Server Action — it can only *import* one from a `'use server'` file. If a component needs to call a mutation from an `onClick`, that mutation has to live in its own server-only module.\n\n## Stage 2: wiring it to a form\n\nThe idiomatic entry point is a `\u003Cform>`'s `action` prop, not a click handler:\n\n```tsx\nimport { deleteComment } from '@\u002Fapp\u002Factions';\n\nexport function CommentRow({ comment }: { comment: Comment }) {\n  return (\n    \u003Cform action={deleteComment.bind(null, comment.id)}>\n      \u003Cbutton type=\"submit\">Delete\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}\n```\n\n`.bind(null, comment.id)` pre-supplies the argument so the form doesn't need a hidden input for it — the bound value travels inside the encrypted action payload, not as plain form data.\n\n**Key concept:** because this is a real `\u003Cform>`, it works before React hydrates and even with JavaScript disabled — the browser submits it as a normal POST and Next.js handles the round trip. That's progressive enhancement you get for free, and it's a strong reason to prefer `action={}` over an `onClick` that calls the function directly.\n\n## Stage 3: pending and result state with useActionState\n\nA raw form submission doesn't give you a pending spinner or an error message. `useActionState` (a React 19 hook; it replaced the older `useFormState`) wraps an action and gives you both:\n\n```tsx\n'use client';\nimport { useActionState } from 'react';\nimport { createComment } from '@\u002Fapp\u002Factions';\n\nconst initialState = { error: null as string | null };\n\nexport function CommentForm({ postId }: { postId: string }) {\n  const [state, formAction, isPending] = useActionState(\n    async (prevState: typeof initialState, formData: FormData) => {\n      const text = formData.get('text');\n      if (typeof text !== 'string' || text.trim().length === 0) {\n        return { error: 'Comment cannot be empty.' };\n      }\n      await createComment(postId, text);\n      return { error: null };\n    },\n    initialState,\n  );\n\n  return (\n    \u003Cform action={formAction}>\n      \u003Ctextarea name=\"text\" disabled={isPending} \u002F>\n      \u003Cbutton disabled={isPending}>{isPending ? 'Posting…' : 'Post comment'}\u003C\u002Fbutton>\n      {state.error && \u003Cp role=\"alert\">{state.error}\u003C\u002Fp>}\n    \u003C\u002Fform>\n  );\n}\n```\n\n**Key concept:** the function you pass to `useActionState` receives the *previous state* as its first argument and the submitted `FormData` as its second, and whatever it returns becomes the new state on the next render. That's how a form gets validation feedback without a separate `useState` and a manual `fetch`.\n\n## Stage 4: invalidating data after a mutation\n\nA mutation that doesn't invalidate anything leaves stale data on screen. Two functions from `next\u002Fcache` handle this:\n\n- `revalidatePath('\u002Fposts')` — throws away the cached render for that path (and re-renders it on next visit).\n- `revalidateTag('comments')` — throws away every cached entry tagged `'comments'`, wherever it lives, which pairs directly with the `cacheTag('comments')` call inside the cached function that produced it. (We covered `cacheTag` and the caching layers themselves in [Cache Components Explained](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob) — this article assumes you have somewhere to invalidate *into*, not how that cache is built.)\n\n```tsx\n'use server';\nimport { revalidateTag } from 'next\u002Fcache';\n\nexport async function createComment(postId: string, text: string) {\n  await db.comment.create({ data: { postId, text } });\n  revalidateTag('comments');\n}\n```\n\nIf the mutation should also navigate — say, after creating a post — call `redirect()` from `next\u002Fnavigation`. It belongs at the end of the action, never inside a `try` block (see [Edge cases](#edge-cases-and-gotchas)).\n\n## Stage 5: instant UI with useOptimistic\n\nWaiting for a round trip before showing a \"liked\" heart feels slow. `useOptimistic` lets you render the *assumed* result immediately, then reconcile once the action resolves:\n\n```tsx\n'use client';\nimport { useOptimistic } from 'react';\nimport { likePost } from '@\u002Fapp\u002Factions';\n\nexport function LikeButton({ postId, likes }: { postId: string; likes: number }) {\n  const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (state) => state + 1);\n\n  return (\n    \u003Cform\n      action={async () => {\n        addOptimisticLike(undefined);\n        await likePost(postId);\n      }}\n    >\n      \u003Cbutton>❤️ {optimisticLikes}\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}\n```\n\n**Key concept:** the optimistic value rolls back automatically only if the action *throws*. If your action instead catches its own error and returns a value, the optimistic state sticks around until the real props change — so a Server Action backing a `useOptimistic` update should let real failures propagate, not swallow them into a returned `{ error }` object the way Stage 3's form does.\n\n## Stage 6: what Next.js secures for you\n\nThis is the part worth being precise about, because getting it wrong in either direction is expensive — either you re-invent protections that already exist, or you assume protections that don't.\n\nNext.js secures the **transport**:\n\n- **CSRF protection is automatic.** A Server Action request only succeeds if its `Origin` header matches the app's own `Host` (or `X-Forwarded-Host` behind a proxy). A cross-site form or script trying to trigger your action from another origin gets rejected before your code runs. If you sit behind a reverse proxy or CDN on a different domain, add it to `experimental.serverActions.allowedOrigins` in `next.config.ts` — otherwise your own legitimate traffic gets blocked.\n- **Action IDs are encrypted and non-deterministic**, recalculated between builds, so they can't be guessed or reused across deployments.\n- **Only referenced functions ship at all.** An exported Server Action your client code never calls is stripped from the client bundle entirely — it has no public endpoint.\n- **Closed-over values are encrypted.** In Stage 1's inline example, `params.id` is captured from the surrounding scope; Next.js encrypts that captured value before it round-trips to the client and back, so it isn't readable or tamperable in the browser. Self-hosting on multiple instances needs a stable `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` shared across them, or instances can't decrypt each other's action payloads.\n\n## Stage 7: what you still owe it\n\nNone of the above answers *who is allowed to call this*. That's Stage 7, and it's on you, exactly as it would be inside a Route Handler:\n\n```tsx\n'use server';\nimport { auth } from '@\u002Flib\u002Fauth';\n\nexport async function deleteComment(commentId: string) {\n  const session = await auth();\n  if (!session) throw new Error('Not authenticated.');\n\n  const comment = await db.comment.findUnique({ where: { id: commentId } });\n  if (!comment || comment.authorId !== session.userId) {\n    throw new Error('Not authorized.');\n  }\n\n  await db.comment.delete({ where: { id: commentId } });\n  revalidatePath('\u002Fposts');\n}\n```\n\nTreat every Server Action as if it were a `POST` handler a stranger could call directly with a tool like `curl` — because, protected transport aside, that's exactly what it is. Check the session, check ownership of whatever's being mutated, and validate the input shape (a schema library like Zod on the `FormData` fields is the idiomatic App Router pattern) before touching the database.\n\n## Edge cases and gotchas\n\n- **`redirect()` inside a `try\u002Fcatch` gets swallowed.** `redirect()` works by throwing a special internal signal that Next.js catches higher up the tree. If you call it inside a `try` block, your own `catch` intercepts that signal first and treats it like a normal error. Call `redirect()` after the `try\u002Fcatch` finishes, not inside it.\n- **A Client Component cannot define an inline action.** Only a Server Component function body can hold `'use server'` inline; a Client Component must import the action from a server-only file, as in Stage 2.\n- **Optimistic state doesn't self-correct on a caught error.** As noted in Stage 5, `useOptimistic` only rolls back when the wrapping action throws — a caught-and-returned error leaves the optimistic UI stuck until real props update.\n- **Server Actions aren't cached like `fetch` or `use cache` data.** They're mutations, not reads; caching applies to what you read afterward, invalidated via `revalidatePath`\u002F`revalidateTag`, not to the action call itself.\n- **Rate limiting is not built in.** The CSRF origin check stops cross-site abuse; it does nothing to stop a signed-in user from calling your action a thousand times a second. Add your own limiter (per-user, per-IP, or both) for anything sensitive.\n\n## Best practices\n\n- **Reach for a Server Action** for form-driven mutations inside your own app — creating, updating, deleting data the user is looking at right now. Progressive enhancement and the built-in CSRF handling make it the right default there.\n- **Reach for a Route Handler instead** when the caller isn't a form in your app: a webhook from a third party, a public API consumed by non-browser clients, or anything that needs a stable, documented URL and method rather than an internal action reference.\n- **Validate input with a schema**, not ad-hoc `if` checks — `FormData` gives you strings and files, never trust the shape.\n- **Check auth and ownership first, mutate second.** Fail fast, before touching the database.\n- **Pair every `cacheTag` with the `revalidateTag` call that invalidates it**, and keep that pairing close together in the codebase so it's obvious which mutation clears which cache.\n\n## FAQ\n\n### Do Server Actions replace API Routes entirely?\n\nNo. They cover form-driven mutations from your own app's UI well; a public API, a webhook receiver, or a non-browser client still wants a Route Handler with a stable URL.\n\n### Is `useActionState` the same as `useFormState`?\n\n`useActionState` is `useFormState`'s React 19 successor — the same shape (previous state in, new state out, plus a `pending` flag), under a name that reflects it isn't limited to forms. `useFormState` still works in the interim but is deprecated in favor of it.\n\n### Are Server Actions secure by default?\n\nThe transport is: CSRF origin checks, encrypted action IDs, and encrypted closures all happen automatically. Authorization is not — every action still needs its own auth and ownership checks, the same as any endpoint you'd hand-write.\n\n### Can a Server Action be called from outside my app?\n\nOnly if your code lets it. The CSRF origin check blocks requests whose `Origin` doesn't match your app's `Host` (or an explicitly configured allowed origin), so a script on another site can't trigger it. A request crafted directly against your own origin — from `curl`, from your own signed-in browser session — still reaches the function, which is exactly why Stage 7's checks matter.\n\n### Why did my optimistic update stay stuck after an error?\n\nAlmost always because the action caught its own error and returned a value instead of throwing. `useOptimistic` reverts on a thrown error, not on a returned one — see Stage 5.\n\n## Cheat sheet\n\n| Task | Code | Notes |\n| --- | --- | --- |\n| Define inline (Server Component only) | `async function f() { 'use server'; … }` | Can close over component scope; values are encrypted in transit. |\n| Define in a shared file | `'use server'` at top of the file | Every export in that file becomes a Server Action. |\n| Call from a form | `\u003Cform action={myAction}>` | Works before hydration; prefer over manual `onClick` + call. |\n| Pre-bind an argument | `myAction.bind(null, id)` | Bound value ships inside the encrypted payload, not a hidden input. |\n| Pending + result state | `useActionState(fn, initialState)` → `[state, formAction, isPending]` | React 19; successor to `useFormState`. |\n| Pending flag only | `useFormStatus()` inside a form's child | Reads the nearest parent `\u003Cform>`'s submission state. |\n| Instant UI | `useOptimistic(value, reducer)` | Rolls back only if the action **throws**, not on a returned error. |\n| Invalidate a route's cache | `revalidatePath('\u002Fposts')` | From `next\u002Fcache`, called inside the action. |\n| Invalidate by tag | `revalidateTag('comments')` | Pairs with `cacheTag('comments')` on the read side. |\n| Navigate after mutating | `redirect('\u002Fposts')` | From `next\u002Fnavigation`; call **outside** any `try\u002Fcatch`. |\n| Cross-origin protection | Automatic (Origin vs Host) | Extra trusted origins via `experimental.serverActions.allowedOrigins`. |\n| Auth check | Your own code, every action | Never assumed by the framework — treat it like an API route. |\n\n## Key takeaways\n\n- A Server Action is an RPC call to a real, encrypted-ID-backed HTTP endpoint — not a function call that happens to run elsewhere.\n- Next.js secures the transport (CSRF origin checks, encrypted IDs, encrypted closures); it never secures *who is allowed to call this* — that's your job, on every action, every time.\n- `useActionState` gives you pending\u002Fresult state, `useOptimistic` gives you instant UI, and `revalidatePath`\u002F`revalidateTag` clear the cache the mutation just invalidated.\n- `redirect()` throws internally — keep it out of `try\u002Fcatch`, or your own `catch` will swallow the navigation.\n\nThat `deleteComment` bug from the top of this article has a one-line fix: a session and ownership check before the `db.comment.delete` call. The syntax will never remind you it's missing — the request in your Network tab is the only thing that will.\n\nWhat's one Server Action in your own codebase you'd want to re-check for an authorization gap after reading this? Drop it in the comments.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":47,"canonical":467,"description":49},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security","01a06d6c-41f6-77bd-95e3-d0efc674a9d2",{"name":470,"part":31,"total":31,"items":471},"Next.js Deep Dive",[472,477],{"slug":473,"title":474,"publishedAt":475,"readingMinutes":476},"nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","2026-09-01T11:24:15.087Z",14,{"slug":46,"title":47,"publishedAt":52,"readingMinutes":51},{"id":479,"locked":18},"01a06d6c-4232-7153-9cfe-eff23f5279ce",[481],{"id":482,"slug":46,"title":483,"_count":484},"01a06d6c-425d-77fd-a66c-b7a479c8e296","Next.js Server Actions",{"questions":485},8,[487],{"locale":13,"slug":46},{"id":482,"slug":46,"title":483,"_count":489,"questionCount":485},{"questions":485},[491,493,497,501,505,507,511,515,517,521,524,528],{"slug":68,"name":69,"articles":492},86,{"slug":494,"name":495,"articles":496},"javascript","Javascript",70,{"slug":498,"name":499,"articles":500},"frontend","Frontend",69,{"slug":502,"name":503,"articles":504},"css","Css",31,{"slug":71,"name":72,"articles":506},19,{"slug":508,"name":509,"articles":510},"performance","Performance",12,{"slug":512,"name":513,"articles":514},"typescript","Typescript",11,{"slug":65,"name":66,"articles":516},10,{"slug":518,"name":519,"articles":520},"browser","Browser",6,{"slug":522,"name":523,"articles":520},"grammar","Grammar",{"slug":525,"name":526,"articles":527},"html","Html",5,{"slug":529,"name":530,"articles":527},"node","Node",{"items":532,"meta":560},[533,545],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":534,"likeCount":19,"commentCount":19,"author":535,"vertical":536,"topic":537,"tags":538,"_count":543,"playground":544,"hasQuiz":17,"hasPlayground":17},16,{"id":54,"name":55,"username":56,"avatarUrl":48,"headline":57},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":60,"name":61},[539,540,541,542],{"slug":60,"name":61,"color":48},{"slug":65,"name":66,"color":48},{"slug":68,"name":69,"color":48},{"slug":71,"name":72,"color":48},{"assessments":74},{"slug":46},{"id":546,"slug":473,"title":474,"subtitle":48,"excerpt":547,"coverUrl":548,"locale":13,"readingMinutes":476,"publishedAt":475,"viewCount":549,"likeCount":19,"commentCount":19,"author":550,"vertical":551,"topic":552,"tags":553,"_count":558,"playground":559,"hasQuiz":17,"hasPlayground":17},"01a0499d-b520-778f-8286-8221ec03b8b5","How Next.js Cache Components decide what's static, what's cached, and what streams — the use cache directive, cacheLife, and Suspense explained.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-cache-components-explained.png",178,{"id":54,"name":55,"username":56,"avatarUrl":48,"headline":57},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":60,"name":61},[554,555,556,557],{"slug":60,"name":61,"color":48},{"slug":65,"name":66,"color":48},{"slug":68,"name":69,"color":48},{"slug":71,"name":72,"color":48},{"assessments":74},{"slug":473},{"page":74,"perPage":561,"total":31,"totalPages":74},7,"\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Factions.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'use server'\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> db \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'@\u002Flib\u002Fdb'\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> revalidatePath \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'next\u002Fcache'\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">deleteComment\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>commentId\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>comment\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">delete\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> where\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> commentId \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-func\">revalidatePath\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'\u002Fposts'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F A button inside a Client Component\n\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onClick\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">deleteComment\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>comment\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>Delete\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fposts\u002F[id]\u002Fpage.tsx — a Server Component\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PostPage\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> params \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> params\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">likePost\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-str\">'use server'\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>post\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">update\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> where\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> params\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> data\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> likes\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> increment\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">form\u003C\u002Fspan> \u003Cspan class=\"shj-class\">action\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>likePost\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>Like\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">form\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Factions.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'use server'\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">deleteComment\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>commentId\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F* … *\u002F\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">likePost\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>postId\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F* … *\u002F\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> deleteComment \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'@\u002Fapp\u002Factions'\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">CommentRow\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> comment \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> comment\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Comment\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">form\u003C\u002Fspan> \u003Cspan class=\"shj-class\">action\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>deleteComment\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">bind\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">null\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> comment\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan> \u003Cspan class=\"shj-class\">type\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"submit\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>Delete\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">form\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003Cdiv>22\u003C\u002Fdiv>\u003Cdiv>23\u003C\u002Fdiv>\u003Cdiv>24\u003C\u002Fdiv>\u003Cdiv>25\u003C\u002Fdiv>\u003Cdiv>26\u003C\u002Fdiv>\u003Cdiv>27\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-str\">'use client'\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> useActionState \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'react'\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> createComment \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'@\u002Fapp\u002Factions'\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> initialState \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> error\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">null\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">as\u003C\u002Fspan> string \u003Cspan class=\"shj-oper\">|\u003C\u002Fspan> \u003Cspan class=\"shj-num\">null\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">CommentForm\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> postId \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> postId\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> formAction\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> isPending\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useActionState\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>prevState\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">typeof\u003C\u002Fspan> initialState\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> formData\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormData\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> text \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> formData\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'text'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n      \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">typeof\u003C\u002Fspan> text \u003Cspan class=\"shj-oper\">!==\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'string'\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">||\u003C\u002Fspan> text\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">trim\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>length \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n        \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> error\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'Comment cannot be empty.'\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n      \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">createComment\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>postId\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> text\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n      \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> error\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">null\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    initialState\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">form\u003C\u002Fspan> \u003Cspan class=\"shj-class\">action\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>formAction\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">textarea\u003C\u002Fspan> \u003Cspan class=\"shj-class\">name\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"text\"\u003C\u002Fspan> \u003Cspan class=\"shj-class\">disabled\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>isPending\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan> \u003Cspan class=\"shj-class\">disabled\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>isPending\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>isPending \u003Cspan class=\"shj-oper\">?\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'Posting…'\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'Post comment'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>error \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">p\u003C\u002Fspan> \u003Cspan class=\"shj-class\">role\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"alert\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>state\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>error\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">p\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">form\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-str\">'use server'\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> revalidateTag \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'next\u002Fcache'\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">createComment\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>postId\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> text\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>comment\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">create\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> data\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> postId\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> text \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-func\">revalidateTag\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'comments'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-str\">'use client'\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> useOptimistic \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'react'\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> likePost \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'@\u002Fapp\u002Factions'\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">LikeButton\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> postId\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> likes \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> postId\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>; likes\u003Cspan class=\"shj-type\">: number\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>optimisticLikes\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> addOptimisticLike\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useOptimistic\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>likes\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>state\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> state \u003Cspan class=\"shj-oper\">+\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">form\u003C\u002Fspan>\n      \u003Cspan class=\"shj-class\">action\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n        \u003Cspan class=\"shj-func\">addOptimisticLike\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">undefined\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n        \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">likePost\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>postId\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n      \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n    \u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>❤️ \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>optimisticLikes\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">form\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-str\">'use server'\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> auth \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'@\u002Flib\u002Fauth'\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">deleteComment\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>commentId\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> session \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">auth\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>session\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">throw\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Error\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'Not authenticated.'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> comment \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>comment\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">findUnique\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> where\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> commentId \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>comment \u003Cspan class=\"shj-oper\">||\u003C\u002Fspan> comment\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>authorId \u003Cspan class=\"shj-oper\">!==\u003C\u002Fspan> session\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>userId\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">throw\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Error\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'Not authorized.'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>comment\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">delete\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> where\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> commentId \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-func\">revalidatePath\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'\u002Fposts'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":572},[]]