Did you know that Next.js Server Actions can return JSX markup instead of raw JSON data?
While it's not explicitly mentioned in the docs, I was pleasantly surprised that it works.
Example
I have a page that renders users list with server action:
export default function Page() {
async function loadUsersAction() {
"use server";
return [
{ name: "John", age: 30 },
{ name: "Jane", age: 25 },
{ name: "Doe", age: 40 },
];
}
return <UsersList loadUsersAction={loadUsersAction} />;
}
UsersList component loads users by button click:
export default function UsersList({ loadUsersAction }) {
const [users, setUsers] = useState();
const onClick = async () => {
const data = await loadUsersAction();
setUsers(data);
};
return (
<>
<button onClick={onClick}>Load users</button>
<ul>
{users?.map((user) => (
<li key={user.name}>
{user.name} - {user.age}
</li>
))}
</ul>
</>
);
}
Demo:
Now I change server action to return JSX with rendered users:
async function loadUsersAction() {
"use server";
const users = [
{ name: "John", age: 30 },
{ name: "Jane", age: 25 },
{ name: "Doe", age: 40 },
];
return (
<ul>
{users?.map((user) => (
<li key={user.name}>
{user.name} - {user.age}
</li>
))}
</ul>
);
}
And in UsersList component just render server action response:
export default function UsersList({ loadUsersAction }) {
const [users, setUsers] = useState();
const onClick = async () => {
const data = await loadUsersAction();
setUsers(data);
};
return (
<>
<button onClick={onClick}>Load users</button>
{users}
</>
);
}
In browser everything works in the same way!
Note on errors handling
What if server action throws an error? When it returns a JSON data, we can catch that error inside action and return it in own format like:
{ error: "my error" }
When returning JSX, we can let error throw and catch it with the nearest error boundary on the client. Per React docs, server action calls outside the <form> component should be wrapped into Transition for proper errors handling.
The final code of UsersList component:
export default function UsersList({ loadUsersAction }) {
const [isPending, startTransition] = useTransition();
const [users, setUsers] = useState();
const onClick = () => {
startTransition(async () => {
const data = await loadUsersAction();
setUsers(data);
});
};
return (
<>
<button onClick={onClick}>Load users</button>
{isPending ? <div>Loading users...</div> : users}
</>
);
}
Additionally, I utilize isPending flag to show message while loading users.
Hope it would be helpful.
Thanks for reading and happy coding! ❤️

