Full Stack Development

Server Actions



by Tomas Trescak t.trescak@westernsydney.edu.au

Next.js

/

Server Actions

  • Slow updates
  • Fast mutations
  • Misconceptions

Next.js

/

Server Actions

    'use server';

export async function addPost(formData: FormData): Promise<void> {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
  const tag = formData.get('tag') as string;
  // Simulate saving to a database
  console.log(`Post added: ${title}, ${content}, Tag: ${tag}`);
}
  
/ app/actions.ts
  • Definition
  • Server Actions = Function Calls = No API!

Next.js

/

Simple Example

    'use server';
import { revalidatePath } from 'next/cache';

export async function addPost(formData: FormData): Promise<{ 
    success: boolean; error?: 
    string 
  }> {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
  const tag = formData.get('tag') as string;

  if (!title || title.length < 3) {
    return { success: false, error: 'Title must be at least 3 characters' };
  }

  const newPost = { id: Date.now(), title, content, tag, date: new Date() };
  console.log('Post added:', newPost);
  revalidatePath('/posts'); // Refresh user view
  return { success: true };
}

// app/admin/add-post/page.tsx
'use client';
import { useActionState } from 'react';
import { addPost } from '@/app/actions';

export default function AddPostPage() {
  const [state, formAction] = useActionState(addPost, { 
    success: false, 
    error: null 
  });

  return (
    <form action={formAction}>
      <input name="title" placeholder="Post Title" />
      <textarea name="content" placeholder="Write your post..." />
      <input name="tag" placeholder="Tag (e.g., tech)" />
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state.success && <p style={{ color: 'green' }}>Post added!</p>}
      <button type="submit">Add Post</button>
    </form>
  );
}
  

Next.js

/

Client Example

    // app/actions.ts
'use server';

export async function saveDraft(content: string): Promise<{ success: boolean }> {
  console.log('Draft saved:', content);
  return { success: true };
}

// app/admin/edit-post/page.tsx
'use client';
import { saveDraft } from '@/app/actions';

export default function EditPost() {
  async function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
    await saveDraft(e.target.value);
  }

  return (
    <form>
      <textarea name="content" placeholder="Draft your post..." onChange={handleChange} />
      <button type="submit">Publish</button>
    </form>
  );
}
  

Next.js

/

Server Actions

  • Type Safe
  • Not for APIs

Next.js

/

Optimistic Updates

    / app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';

export async function addPostOptimistic(formData: FormData): Promise<{ 
  id: number; 
  title: string; 
  tag: string 
}> {
  const title = formData.get('title') as string;
  const tag = formData.get('tag') as string;
  const newPost = { id: Date.now(), title, tag };
  console.log('Post added:', newPost);
  revalidatePath('/posts');
  return newPost;
}

// app/posts/page.tsx
'use client';
import { useOptimistic } from 'react';
import { addPostOptimistic } from '@/app/actions';

type Post = { id: number; title: string; tag: string };

export default function PostsPage({ initialPosts }: { initialPosts: Post[] }) {
  const [optimisticPosts, addOptimisticPost] = useOptimistic<Post[], Post>(
    initialPosts,
    (state, newPost) => [...state, newPost]
  );

  async function handleAddPost(formData: FormData) {
    const newPost = {
      id: Date.now(),
      title: formData.get('title') as string,
      tag: formData.get('tag') as string,
    };
    addOptimisticPost(newPost); // Show instantly
    await addPostOptimistic(formData); // Confirm with server
  }

  return (
    <div>
      <ul>
        {optimisticPosts.map(post => (
          <li key={post.id}>{post.title} - {post.tag}</li>
        ))}
      </ul>
      <form action={handleAddPost}>
        <input name="title" placeholder="Post Title" />
        <input name="tag" placeholder="Tag (e.g., tech)" />
        <button type="submit">Add Post</button>
      </form>
    </div>
  );
}
  

Next.js

/

Conclusion

  • Server Actions rock!
  • Works with the cache
  • Don’t skip validation