Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

next-safe-action

Package Overview
Dependencies
Maintainers
1
Versions
245
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

next-safe-action

A typesafe server actions implementation for Next.js with RSC.

  • 2.5.2
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
32K
decreased by-13.56%
Maintainers
1
Weekly downloads
 
Created
Source

next-safe-action

next-safe-action is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features to let you define typesafe actions on the server and call them from Client Components.

Features

  • ✅ Pretty simple
  • ✅ End to end type safety
  • ✅ Input validation
  • ✅ Direct or hook usage from client
  • ✅ Optimistic updates
  • ✅ Authenticated actions

Requirements

Next.js >= 13.3.0 and >= TypeScript 5.0.

Installation

npm i next-safe-action zod

Code example ⬇️

Check out this example repository to see a basic implementation of this library and to experiment a bit with it.


Project configuration

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 validator 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 {
        error: {
          reason: "user_suspended",
        },
      };
    }

    if (username === "user" && password === "password") {
      return {
        success: true,
      };
    }

    return {
      error: {
        reason: "incorrect_credentials",
      },
    };
  }
);

action returns a new function (in this case loginUser). To make it actually work, we must pass the action to a Client Component as a prop, otherwise Server Component functions (e.g. cookies() or headers()) wouldn't work in the server action body (defined above).

// 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} />
  );
}

There are two ways to call safe actions from the client:

1. The direct way

"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" });

        // Res keys.
        const { data, validationError, serverError } = res;
      }}>
      Log in
    </button>
  );
}

On the client you get back a typesafe response 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 validator) 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 response. By default, the server error will be logged via console.error, but this is configurable.

2. The hook way

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`) passed to `useAction` hook.
  const { execute, isExecuting, res } = useAction(loginUser);

  return (
    <>
      <button
        onClick={() => {
          // Typesafe action called from client.
          execute({ username: "user", password: "password" });
        }}>
        Log in
      </button>
      <p>Is executing: {JSON.stringify(isExecuting)}</p>
      <p>Res: {JSON.stringify(res)}</p>
    </>
  );
}

The useAction hook returns an object with three 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 action the non-hooky way.
  • isExecuting: a boolean that is true while the execute function is mutating data.
  • res: when execute finished mutating data, the response object. Otherwise it is null. 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.

Image example:

Hook typesafe response

Optimistic update ✨ (experimental)

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.

Note: this React hook is not stable, 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 inputValidator = z.object({
  incrementBy: z.number(),
});

export const addLikes = action(
  { input: inputValidator },
  async ({ incrementBy }) => {
    // Add delay to simulate db call.
    await new Promise((res) => setTimeout(res, 2000));

    const newLikes = 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 {
      newLikes,
    };
  }
);

Then, you can pass state as prop from a Server Component to a Client Component (in this case likesCount), 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 comes from server
  addLikes: typeof addLikes;
};

export default function AddLikes({ likesCount, addLikes }: Props) {
  // Safe action (`addLikes`) and current server state passed to
  // `useOptimisticAction` hook.
  const { execute, isExecuting, res, optimisticState } = useOptimisticAction(
    addLikes,
    { likesCount }
  );

  return (
    <>
      <button
        onClick={() => {
          const randomIncrement = Math.round(Math.random() * 100);

          // Action call. Here we pass action input and expected (optimistic)
          // server state.
          execute(
            { incrementBy: 42 },
            { likesCount: likesCount + randomIncrement }
          );
        }}>
        Add likes
      </button>
      <p>Optimistic state: {JSON.stringify(optimisticState)}</p> {/* [1] */}
      <p>Is executing: {JSON.stringify(isExecuting)}</p>
      <p>Res: {JSON.stringify(res)}</p>
    </>
  );
}

As you can see, useOptimisticAction has the same three keys as the normal useAction hook (execute, isExecuting, res).

[1]: It returns one additional key though: optimisticState is an object with a type of the second argument passed to useOptimisticAction hook. This object will update immediately when you execute the action. Real data will come back once action has finished executing.

Authenticated action

The library also supports creating protected actions, that will return a serverError back if user is not authenticated. You need to make some changes to the above code in order to use them.

First, when creating the safe action client, you must provide an async function called getAuthData as an option. You can return anything you want from here. If you find out that the user is not authenticated, you can safely throw an error in this function. It will be caught, and the client will receive a serverError response.

// src/lib/safe-action.ts

import { createSafeActionClient } from "next-safe-action";

export const action = createSafeActionClient({
  // Here you can use functions such as `cookies()` or `headers()`
  // from next/headers, or utilities like `getServerSession()` from NextAuth.
  getAuthData: async () => {
    const session = true;

    if (!session) {
      throw new Error("user is not authenticated!");
    }

    return {
      userId: "coolest_user_id",
    };
  },
});

Then, you can provide a withAuth: true option to the safe action you're creating:

"use server"; // don't forget to add this

import { z } from "zod";
import { action } from "~/lib/safe-action";

...

// [1] For protected actions, you need to provide `withAuth: true` here.
// [2] Then, you'll have access to the auth object, in this case it's just
// `{ userId }`, which comes from the return type of the `getAuthData` function
// declared in the previous step.
export const editUser = action({ input, withAuth: true }, // [1]
  async (parsedInput, { userId }) => { // [2]
    console.log(userId); // will output: "coolest_user_id",
    ...
  }
);

If you set withAuth to true in the safe action you're creating, but you forgot to define a getAuthData function when creating the client (above step), an error will be thrown when calling the action from client, that results in a serverError response for the client.

Custom server error logging

As you just saw, you can provide a getAuthData function to createSafeActionClient function.

You can also provide a custom logger function for server errors. By default, they'll be logged via console.error (on the server, obviously), but this is configurable:

// src/lib/safe-action.ts

import { createSafeActionClient } from "next-safe-action";

export const action = createSafeActionClient({
  // You can also provide an empty function here (if you don't want server error
  // logging), or a Promise. Return type is `void`.
  serverErrorLogFunction: (e) => {
    console.error("CUSTOM ERROR LOG FUNCTION:", e);
  },
});

Alternatives

License

This project is licensed under the MIT License.

Keywords

FAQs

Package last updated on 09 May 2023

Did you know?

Socket

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.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc