v0.21
August 20, 2024

Server actions are here!

Enjoy full support for React 19’s server actions API.

by Sophia Andren
technical director of candycode

While Waku already had limited support for server actions, today’s v0.21 release brings full support for the React server actions API. This includes inline server actions created in server components, server actions imported from separate files, and the invocation of server actions via <form> element action props.

Server actions allow you to define and securely execute server-side logic directly from your React components without the need for manually setting up API endpoints, sending POST requests to them with fetch, or managing pending states and errors.

Continue to learn more about the new API.

Server actions API

Defining and protecting actions

The 'use server' directive marks an async function as a server action. Waku automatically creates a reference to the action that can be passed as props or imported into client components, which can then call the referenced function.

When the directive is placed at the top of a function body, it will mark that specific function as an action. Alternatively, the directive can be placed at the top of a file, which will mark all exported functions as actions at once.

Be careful not to add the directive where inappropriate and inadvertently create unwanted endpoints. Endpoints created by server actions are not secured unless you add your own authentication and authorization logic inside the function body.

Note

The 'use server' directive has no relation to the'use client' directive. It does not mark a component as a server component and should not be placed at the top of server components!

Making and consuming actions

When creating an inline server action within a server component, it can be passed as props to a client component.

// ./src/pages/contact.tsx

import db from 'some-db';

export default async function ContactPage() {
  const sendMessage = async (message: string) => {
    'use server';
    await db.messages.create(message);
  };

  return <ContactForm sendMessage={sendMessage} />;
}
// ./src/components/contact-form.tsx
'use client';

import { useState } from 'react';

export const ContactForm = ({ sendMessage }) => {
  const [message, setMessage] = useState('');

  return (
    <>
      <textarea
        value={message}
        onChange={(event) => setMessage(event.target.value)}
        rows={4}
      />
      <button onClick={() => sendMessage(message)}>Send message</button>
    </>
  );
};

When creating server actions in a separate file, they can be imported directly into client components.

Note

When using a top-level 'use server' directive, note that all exported functions will be made into API endpoints. So be careful only to export functions intended for this purpose. Add server-side logic to validate proper authentication and authorization if appropriate.

// ./src/actions/send-message.ts
'use server';

import db from 'some-db';

export async function sendMessage(message: string) {
  await db.messages.create(message);
}
// ./src/components/contact-button.tsx
'use client';

import { sendMessage } from '../actions/send-message';

export const ContactButton = () => {
  const message = `Hello world!`;

  return <button onClick={() => sendMessage(message)}>Send message</button>;
};

Invoking actions

Actions can be invoked via event handlers such as onClick or onSubmit, as in the examples above, or in a useEffect hook, based on whichever conditions you choose.

They can also be invoked via an action prop on native <form> elements. In this case the server action will automatically receive a parameter of FormData with all of the form field values, including hidden ones.

// ./src/actions/send-message.ts
'use server';

import db from 'some-db';

export async function sendMessage(formData: FormData) {
  const message = formData.get('message');

  await db.messages.create(message);
}
// ./src/components/create-todo-button.tsx
'use client';

import { sendMessage } from '../actions/send-message';

export const ContactForm = () => {
  return (
    <form action={sendMessage}>
      <textarea name="message" rows={4} />
      <input type="hidden" name="secret-message" value="This too!" />
      <button type="submit">Send message</button>
    </form>
  );
};

If you must pass additional arguments to a form action beyond its native form fields, you can use the bind method to create an extended server action with the extra arguments.

// ./src/components/create-todo-button.tsx
'use client';

import { sendMessage } from '../actions/send-message';

export const ContactForm = ({ author = 'guest' }) => {
  const sendMessageWithAuthor = sendMessage.bind(null, author);

  return (
    <form action={sendMessageWithAuthor}>
      <textarea name="message" rows={4} />
      <button type="submit">Send message</button>
    </form>
  );
};

Enhancing actions

Server actions integrate with many other React APIs such as the useTransition hook for handling pending states, the useActionState hook for accessing returned values, and the useOptimistic hook for performing optimistic UI updates.

See the talk What’s new in React 19? to learn more.

Summer of Waku

We’re excited to see what you’ll build with Waku now that it fully supports server actions. Enjoy accelerated development of many common use cases such as form submissions, database operations, and much more!

As always your feedback in our GitHub discussions is much appreciated and you’re welcome to join us in our Discord server to share what you’re building.

Please stay tuned for our upcoming v0.22 release featuring API routes, which support even more complex or resource intensive server-side functionalities.

designed bycandycode alternative graphic design web development agency San Diego