> ## Content Index
> Fetch the complete content index at: https://madewithlove.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# Crossing boundaries: Handling data mutations in client components
- URL: https://madewithlove.com/blog/crossing-boundaries-handling-data-mutations-in-client-components/
- Published: 2024-09-30T14:44:20.000Z
- Updated: 2025-02-25T08:14:30.000Z
- Description: Discover how to efficiently mutate data in client components using server actions in Next.js. Learn to handle form submissions with React’s evolving hooks like useActionState, eliminate the need for API endpoints, and streamline client-server interactions for faster, more responsive applications.
- Author: Geoffrey Dhuyvetters
- Tags: Engineering, Code quality, Tutorials, JavaScript, React, Frontend

In a previous article we explored [how to fetch and resolve data within a client component](https://madewithlove.com/blog/crossing-boundaries-passing-server-data-to-client-components-in-react/). Now, we’ll take it a step further and focus on how to **mutate data directly** from a client component, using server actions in **Next.js**.

## What are server actions?

At its core, a **server action** is simply an asynchronous function that runs on the server. It has direct access to secrets, database operations and services. You can find a [deeper breakdown here](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations).

`// ./actions/createComment.ts`

```javascript
"use server";

export async function createComment(formData) {
    // validate data (Zod)
    // process data (ORM, service)
    // return data or error message
}
```

## Using the server action in a client component

A **server action** can be invoked in a client component either via the **action attribut**e on a form or **directly** within a handler method.

In this example, we have a component that includes a form that submits its data to a `createComment` action. 

`// ./components/Comments.tsx`

```javascript
"use client";

import { useActionState } from 'react'; 
import { createComment } from './actions/createComment';

export default function Comments() {
  
  // only works with React 19
  const [createdComment, createCommentAction, isCreatingComment] =
    useActionState(createComment, null);
  
  return (
    <div>
      <form action={createCommentAction}>
        <div>
          <label htmlFor="message">Message:</label>
          <textarea
            id="message"
            name="message"
            required
          />
        </div>
        <button type="submit" disabled={isCreatingComment}>
          {isCreatingComment ? 'Submitting...' : 'Submit'}
        </button>
      </form>
      
      {createdComment && <p>{createdComment.message}</p>}
    </div>
  );
       
}
```

The action’s return state and lifecycle are managed using `useActionState`, a hook further refined in the upcoming **React 19** release. Previously known as `useFormState`, this hook lacked the `isPending` return parameter. The parameter has since been added for better state tracking during form submissions.

When using this hook, we need to make sure the server action accepts `previousState` as its first argument. In this example, we’re setting it to null in the client component (2nd argument on useActionState).

```javascript
'use server';

// previousState is new here
export async function createComment(previousState, formData) {
    // validate data (Zod)
    // process data (ORM, service)
    // return data or error message
}
```

We can integrate this with the previous approach to create a client component that both **reads and writes** data to the server-side database, all without the need for additional API endpoints.

`// ./components/Comments.tsx`

```javascript
"use client";

import { use, useActionState } from 'react'; 
import { createComment } from './actions/createComment'; 

export default function Comments({ getComments }) {
  
  const comments = use(getComments);

  const [createdComment, createCommentAction, isCreatingComment] = 
    useActionState(createComment, null);
  
  return (
    <div>
      <h2>Existing Comments</h2>
      <ul>
        {comments.map((comment) => (
          <li key={comment.id}>{comment.message}</li>
        ))}
        {createdComment && <li>{createdComment.message}</li>}
      </ul>
      <h2>Add a new comment</h2>
      <form action={async (formData) => {
          await createCommentAction(formData);
          router.refresh();
        }}>
        <div>
          <label htmlFor="message">Message:</label>
          <textarea
            id="message"
            name="message"
            required
          />
        </div>
        <button type="submit" disabled={isCreatingComment}>
          {isCreatingComment ? 'Submitting...' : 'Submit'}
        </button>
      </form>
    </div>
  );

}

```

By leveraging **server actions** and **React’s evolving features**, such as `useActionState` and the `use()`hook, we can efficiently manage both data fetching and mutations directly within client components. This approach **simplifies** the architecture by eliminating the need for custom API endpoints while maintaining a clean and intuitive workflow. As React continues to **evolve**, these tools provide a powerful way to handle complex interactions, improve performance, and streamline server-client data management, all while keeping the developer experience front and centre.

Overall, **crossing boundaries** between client and server is becoming more seamless, enabling developers to build dynamic applications that offer fast, responsive user experiences.

**Also read:**

- [Passing server data to client components in React](https://madewithlove.com/blog/crossing-boundaries-passing-server-data-to-client-components-in-react/)
- [Instant feedback on useOptimistic hook in React](https://madewithlove.com/blog/instant-feedback-using-the-useoptimistic-hook-in-react/)