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.
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!
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