Fix: Action Called from Server Instead of Client in Astro

Error message:
Actions should be called from client-side code.
Actions 2025-01-25

What Causes This Error?

This error occurs when you call an action directly from server-side code (like the frontmatter of an Astro component). Actions are designed to be called from the client, with special methods for server-side form handling.

The Problem

---
// ❌ Calling action directly in frontmatter (server-side)
import { actions } from 'astro:actions';

const result = await actions.subscribe({ email: 'test@example.com' });
---

The Fix

Call from Client-Side

---
export const prerender = false;
---

<form id="subscribe-form">
  <input type="email" name="email" />
  <button type="submit">Subscribe</button>
</form>

<script>
  // ✅ Call from client-side JavaScript
  import { actions } from 'astro:actions';

  document.getElementById('subscribe-form')
    ?.addEventListener('submit', async (e) => {
      e.preventDefault();
      const formData = new FormData(e.target);

      const result = await actions.subscribe({
        email: formData.get('email'),
      });
    });
</script>

Common Scenarios

Form Submissions (No JavaScript)

---
// For server-side form handling, use getActionResult
import { actions } from 'astro:actions';
export const prerender = false;

// ✅ Get result from form submission
const result = Astro.getActionResult(actions.subscribe);
---

<!-- Use form action attribute -->
<form method="POST" action={actions.subscribe}>
  <input type="email" name="email" required />
  <button type="submit">Subscribe</button>
</form>

{result?.error && <p class="error">{result.error.message}</p>}
{result?.data && <p class="success">Subscribed!</p>}

Client Component Calling Actions

// src/components/SubscribeForm.tsx (React)
import { actions } from 'astro:actions';

export default function SubscribeForm() {
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    // ✅ Client-side call
    const result = await actions.subscribe({
      email: formData.get('email') as string,
    });

    if (result.data) {
      alert('Subscribed!');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
    </form>
  );
}

Direct Server-Side Logic

---
// If you need server-side logic, use the handler directly
export const prerender = false;

// ❌ Don't call action
// const result = await actions.subscribe({ email });

// ✅ Use the same logic in an API endpoint or directly
async function subscribeUser(email: string) {
  // Same logic as your action handler
  await db.insert({ email });
  return { success: true };
}

// Use in form handling
if (Astro.request.method === 'POST') {
  const formData = await Astro.request.formData();
  const email = formData.get('email') as string;
  const result = await subscribeUser(email);
}
---

API Endpoint Alternative

// src/pages/api/subscribe.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  const data = await request.json();

  // ✅ Server-side logic in API endpoint
  await db.insert({ email: data.email });

  return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' },
  });
};

Using getActionResult

---
import { actions } from 'astro:actions';
export const prerender = false;

// ✅ getActionResult for form submissions
const subscribeResult = Astro.getActionResult(actions.subscribe);
const isSubscribed = subscribeResult?.data?.success;
---

<form method="POST" action={actions.subscribe}>
  {!isSubscribed ? (
    <>
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
    </>
  ) : (
    <p>Thanks for subscribing!</p>
  )}
</form>

Progressive Enhancement

---
import { actions } from 'astro:actions';
export const prerender = false;

const result = Astro.getActionResult(actions.subscribe);
---

<!-- Works without JavaScript (form submission) -->
<form id="subscribe-form" method="POST" action={actions.subscribe}>
  <input type="email" name="email" required />
  <button type="submit">Subscribe</button>
</form>

{result?.data && <p>Subscribed!</p>}

<!-- Enhanced with JavaScript -->
<script>
  import { actions } from 'astro:actions';

  document.getElementById('subscribe-form')
    ?.addEventListener('submit', async (e) => {
      e.preventDefault();
      const formData = new FormData(e.target);

      // JavaScript takes over for better UX
      const result = await actions.subscribe({
        email: formData.get('email'),
      });

      if (result.data) {
        // Show success message without page reload
      }
    });
</script>

When to Use What

Client-side (actions.myAction):
- Interactive forms
- Real-time updates
- Better UX (no page reload)
- React/Vue/Svelte components

Server-side (getActionResult):
- Progressive enhancement
- Works without JavaScript
- Form submissions
- SEO-friendly forms

Quick Checklist

  • Don’t call actions in Astro frontmatter
  • Use <script> tag for client-side calls
  • Use getActionResult for server-side form handling
  • Use form action attribute for progressive enhancement
  • Set prerender = false for action pages