Security News
Research
Supply Chain Attack on Rspack npm Packages Injects Cryptojacking Malware
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
next-safe-action
Advanced tools
A typesafe server actions implementation for Next.js with RSC using Zod.
next-safe-action
is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features, using Zod, to let you define typesafe actions on the server and call them from Client Components.
This is the documentation for version 4 of the library, the current one. If you want to check out the 2.x.x documentation, you can find it here. If you want to check out the 3.x.x documentation, you can find it here.
Next.js >= 13.4.2 and >= TypeScript 5.0.
npm i next-safe-action zod
In next.config.js
(since Next.js 13.4.0):
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true, // add this
},
};
module.exports = nextConfig;
Then, you need to create the safe action client:
// src/lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";
export const action = createSafeActionClient();
Now create a file for an action:
// src/app/login-action.ts
"use server"; // don't forget to add this
import { z } from "zod";
import { action } from "@/lib/safe-action";
// This is used to validate input from client.
const input = z.object({
username: z.string().min(3).max(10),
password: z.string().min(8).max(100),
});
// This is how a safe action is created.
// Since we provided a Zod input schema to the function, we're sure
// that data that comes in is type safe and validated.
// The second argument of this function is an async function that receives
// parsed input, and defines what happens on the server when the action is
// called from the client.
// In short, this is your backend code. It never runs on the client.
export const loginUser = action(input, async ({ username, password }) => {
if (username === "johndoe") {
return {
failure: {
reason: "user_suspended",
},
};
}
if (username === "user" && password === "password") {
return {
success: true,
};
}
return {
failure: {
reason: "incorrect_credentials",
},
};
}
);
action
returns a new function (in this case loginUser
). At this time, to make it actually work, we must pass the action to a Client Component as a prop, otherwise calling actions from hooks wouldn't work properly.
// src/app/page.tsx
import Login from "./login";
import { loginUser } from "./login-action";
export default function Home() {
return (
{/* here we pass the safe action to the Client Component */}
<Login loginUser={loginUser} />
);
}
Note If you use
redirect()
andnotFound()
functions in the Server Action's backend code, you must use theuseAction
hook too. Other ways are not currently supported by Next.js. Withredirect()
, you also get aUND_ERR_REQ_CONTENT_LENGTH_MISMATCH
error in the server console, just ignore it for now, since the action is performed anyway.
There are two ways to call safe actions from the client:
"use client"; // this is a Client Component
import type { loginUser } from "./login-action";
type Props = {
loginUser: typeof loginUser; // infer typings with `typeof`
}
export default function Login({ loginUser }: Props) {
return (
<button
onClick={async () => {
// Typesafe action called from client.
const res = await loginUser({ username: "user", password: "password" });
// Result keys.
const { data, validationError, serverError } = res;
}}>
Log in
</button>
);
}
On the client you get back a typesafe result object, with three optional keys:
data
: if action runs without issues, you get what you returned in the server action body.
validationError
: if an invalid input object (parsed by Zod via input schema) is passed from the client when calling the action, invalid fields will populate this key, in the form of:
{
"validationError": {
"fieldName": ["issue"],
}
}
serverError
: if an unexpected error occurs in the server action body, it will be caught, and the client will only get back a serverError
result. By default, the server error will be logged via console.error
, but this is configurable.Another way to mutate data from client is by using the useAction
hook. This is useful when you need global access to the action state in the Client Component.
useAction
uses React's useTransition
hook behind the scenes to manage the mutation.
Here's how it works:
"use client"; // this is a Client Component
import { useAction } from "next-safe-action/hook";
import { loginUser } from "./login-action";
type Props = {
loginUser: typeof loginUser;
};
export default function Login({ loginUser }: Props) {
// Safe action (`loginUser`) and optional `onSuccess`, `onError` and `onExecute` callbacks
// passed to `useAction` hook.
const {
execute,
result,
status,
reset,
} = useAction(loginUser, {
onSuccess: (data, input, reset) => {
// Data from server action.
const { failure, success } = data;
// Reset result object.
reset();
// Data used to call `execute`.
const { username, password } = input;
},
onError: (error, input, reset) => {
// One of these errors.
const { fetchError, serverError, validationError } = error;
// Reset result object.
reset();
// Data used to call `execute`.
const { username, password } = input;
},
onExecute: (input) => {
// Action input.
const { username, password } = input;
},
}
);
return (
<>
<button
onClick={() => {
// Typesafe action called from client.
execute({ username: "user", password: "password" });
}}>
Log in
</button>
<button
onClick={() => {
// Reset result object programmatically.
reset();
}}>
Reset
</button>
<p>Is executing: {JSON.stringify(status === "executing")}</p>
<p>Res: {JSON.stringify(result)}</p>
</>
);
}
The useAction
has one required argument (the action) and one optional argument (an object with onExecute
, onSuccess
, and onError
callbacks).
onExecute(input)
, onSuccess(data, input, reset)
and onError(error, input, reset)
are executed, respectively, when the action is executing, when the execution is successful, or when it fails. The original payload of the action is available as the second argument of the callback (input
): this is the same data that was passed to the execute
function. You can also reset the result object inside these callbacks with reset()
(third argument of the callback).
It returns an object with four keys:
execute
: a caller for the safe action you provided as argument to the hook. Here you pass your typesafe input
, the same way you do when using safe actions the non-hooky way.result
: when execute
finished mutating data, the result object. It has the same three optional keys as the one above (data
, validationError
, serverError
), plus one: fetchError
. This additional optional key is populated when communication with the server fails for some reason.status
: a string representing the current status of the action. It can be idle
, executing
, hasErrored
, or hasSucceded
.reset
function, to programatically reset the result object.If you need optimistic UI in your Client Component, the lib also exports a hook called useOptimisticAction
, that under the hood uses React's experimental_useOptimistic
hook.
Warning This feature is experimental, use it at your own risk.
Here's how it works. First, define your server action as usual:
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { action } from "@/lib/safe-action";
import { incrementLikes } from "./db";
const input = z.object({
incrementBy: z.number(),
});
export const addLikes = action(input, async ({ incrementBy }) => {
// Add delay to simulate db call.
await new Promise((res) => setTimeout(res, 2000));
const likesCount = incrementLikes(incrementBy);
// This Next.js function revalidates the provided path.
// More info here: https://nextjs.org/docs/app/api-reference/functions/revalidatePath
revalidatePath("/optimistic-hook");
return {
likesCount,
};
}
);
Note If you use this hook, you need to return an object from your action's backend code. Otherwise optimistic state update won't work properly.
Then, you need to pass the initial fetched data as prop (in this case likesCount
) from Server Component to Client Component, and use the hook in it.
"use client"; // this is a Client Component
import { useOptimisticAction } from "next-safe-action/hook";
import { addLikes } from "./addlikes-action";
type Props = {
likesCount: number; // this is fetched initially from server
addLikes: typeof addLikes;
};
export default function AddLikes({ likesCount, addLikes }: Props) {
// Safe action (`addLikes`), initial data, and optional
// `onSuccess` and `onError` callbacks passed to `useOptimisticAction` hook.
const {
execute,
result,
status,
reset,
optimisticData,
} = useOptimisticAction(
addLikes,
{ likesCount }, // [1]
({ likesCount }, { incrementBy }) => ({ // [2]
likesCount: likesCount + incrementBy,
}),
{
onSuccess: (data, input, reset) => {},
onError: (error, input, reset) => {},
onExecute: (input) => {},
}
);
return (
<>
<button
onClick={() => {
const incrementBy = Math.round(Math.random() * 100);
// Action call. Here we pass action input and expected (optimistic) data.
execute({ incrementBy });
}}>
Add likes
</button>
<p>Optimistic data: {JSON.stringify(optimisticData)}</p> {/* [3] */}
<p>Is executing: {JSON.stringify(status === "executing")}</p>
<p>Res: {JSON.stringify(result)}</p>
</>
);
}
As you can see, useOptimisticAction
requires a safe action just like useAction
, but it also requires:
reducer
function that defines the optimistic behavior when the action is executed. [2]It returns the same four keys as the regular useAction
hook, plus one additional key [3]: optimisticData
has the same type of the action's return object. This object will update immediately when you execute
the action. Real data will come back once action has finished executing.
You can provide a middleware function when initializing a new action client. It will be called before the action is executed, but after input validation from the client. You can optionally return anything you want from this function: the returned value will be passed as context
(second argument) to the server code function, when creating new Server Actions. You can safely throw an error in this function's body; if that happens, the client will receive a serverError
result.
// src/lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";
// This is the base safe action client.
export const action = createSafeActionClient();
// This is a safe action client with an auth context.
export const authAction = createSafeActionClient({
// Here you can use functions such as `cookies()` or `headers()`
// from next/headers, or utilities like `getServerSession()` from NextAuth.
middleware: () => {
const session = true;
if (!session) {
throw new Error("user is not authenticated!");
}
return {
userId: "coolest_user_id",
};
},
});
Then, you can use the previously defined client and access context
:
"use server"; // don't forget to add this
import { z } from "zod";
import { authAction } from "@/lib/safe-action";
...
// [1]: Here you have access to the context, in this case it's just `{ userId }`,
// which comes from the return type of the `middleware` function declared in the previous step.
export const editUser = authAction(input, async (parsedInput, { userId /* [1] */ }) => {
console.log(userId); // will output: "coolest_user_id",
...
}
);
As you just saw, you can provide a middleware
function to createSafeActionClient
function.
You can also provide:
e
as argument. By default, they'll be logged via console.error
(on the server, obviously), but this is configurable.handleReturnedServerError
, that has the error object e
as argument, and returns a result object with a serverError
key. When this option is provided, the safe action client lets you handle returned server errors in a custom way. So, if an error occurs on the server, instead of returning back a default message to the client, the custom logic of this function will be used to build the result object. Though, the original server error will still be logged and/or passed to the handleServerErrorLog
function, if provided.// src/lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";
export const action = createSafeActionClient({
// You can also provide an empty Promise here (if you don't want to log
// server errors at all). Return type is `void`.
handleServerErrorLog: (e) => {
console.error("CUSTOM ERROR LOG FUNCTION:", e);
},
// Default is undefined. If this Promise is not provided, the client will return
// a default server error result when an error occurs on the server.
handleReturnedServerError: (e) => {
// Your custom error handling logic here.
// ...
return {
serverError: "Something went wrong",
}
}
});
This project is licensed under the MIT License.
FAQs
Type safe and validated Server Actions in your Next.js project.
The npm package next-safe-action receives a total of 29,647 weekly downloads. As such, next-safe-action popularity was classified as popular.
We found that next-safe-action demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.
Security News
Sonar’s acquisition of Tidelift highlights a growing industry shift toward sustainable open source funding, addressing maintainer burnout and critical software dependencies.