Code Monkey home page Code Monkey logo

next-safe-action's Introduction

next-safe-action is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features, using validation libraries of your choice, to let you define type safe Server Actions and execute them inside React Components.

How does it work?

Like magic. ๐Ÿช„

demo.mp4

Features

  • โœ… Pretty simple
  • โœ… End-to-end type safety
  • โœ… Powerful middleware system
  • โœ… Input validation using multiple validation libraries
  • โœ… Advanced server error handling
  • โœ… Optimistic updates

Documentation

Explore the documentation for the next version of the library on the next-safe-action website. โœจ

Installation

npm i next-safe-action@next

Playground

You can find a basic working implementation of the library here.

Contributing

If you want to contribute to next-safe-action, please check out the contributing guide.

If you found bugs or just want to ask a question, feel free to open an issue or a discussion by following the issue templates.

Contributors

Made with contrib.rocks.

License

next-safe-action is released under the BSD-3-Clause License.

next-safe-action's People

Contributors

alerosa avatar burry avatar jssusenka avatar kassiogluten avatar meienberger avatar theboxer avatar theedoran avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

next-safe-action's Issues

v4 Action statuses change to a single "status" property

Hey, first of all, huge thanks for this pckg! Good job!

Problem

Quick question/suggestion. What's the background of this change?

image

Single status string property is a step back in terms of DX imho. JSX with string conditions (like status === 'executing') looks messier and more code to write. Noticed, I started manually creating isExecuting consts (and some for other statuses) all over the project.

Old api vs New api

const { status } = useAction(action);
const isExecuting = status === 'executing';
const { status, isExecuting } = useAction(action);

Extra

Tanstack Query Api could be great inspiration (for naming things as well) - I assume community got really used to it in past years.

image

`useOptimisticAction` overwrites optimistic state with previous state

It seems that useOptimisticAction is prematurely overwriting the "optimistic state", i.e. the state passed in as the second arg to the returned execute function, once the corresponding API request has resolved. This results in an "old state -> updated (optimistic) state -> old state -> updated state" behavior:

Ex.
ex.mov

Note in the example above, the optimistic state gets set correctly upon click, but then once the request resolves the state reverts back to the original state, and finally upon refresh of the route (via the action running revalidatePath('<path>')), the updated state comes back in via props.

I believe this may be due to the res state here being updated, which causes a rerender & in turn for the hook to be reran, setting optState back to deaultOptState, which of course is still the (old) state we originally rendered the component with. Locally, if I spread res.data into the default state as well, it works as expected, since the updated state gets set as the optimistic state. You can see me doing that here.

Is that the issue or am I just actually using the hook incorrectly? ๐Ÿค”

You can see exactly how I'm using it here, which is where the example noted above also comes from.

feat: initialize an action client with an optional `serverCode` wrapper

Will take a shot at this if I find the time, but I'd love to be able to pass in some function that wraps the server action code when initializing a new server action client. The primary use-case for this is instrumentation and logging the start/end of an action execution, for example Sentry's new server action utility. Currently I'm getting around this problem by using middleware to create a wrapping function in context, but first-class support for this feature would be ideal.

[BUG] TypeSchema breaks edge deployment and Turbopack dev server

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Describe the bug

When deploying on Vercel with the Edge Runtime I get the following error:

Error: The Edge Function "[locale]" is referencing unsupported modules:
        - index.js: @deepkit/type, @effect/data/Either, @effect/schema/Schema, @effect/schema/TreeFormatter, fp-ts/Either, ow, valibot, yup

Reproduction steps

  1. Update to v6
  2. Create a layout/page
  3. add export const runtime = 'edge'at the top
  4. Use a safe server action created with this package in that page
  5. Deploy on Vercel

Expected behavior

It should work on the edge runtime as it did in v5

Reproduction example

https://github.com/AlexisWalravens/next-safe-action-edge-broken

Operating System

macOS 14.1.2

Library version

6.0.0

Additional context

Thanks for this package !

Caching responses from action?

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

latest

Ask a question

Is it possible to cache the responses from a next safe action, probably using the middleware, depending on the arguments passed to it?

Additional context

No response

Usage with Form Actions

Hi, thank you for the library.

Is there a way I can use actions directly on <form action={...} and <button formAction={...}?
Those actions seem to follow async (prevState: any, formData: FormAction) => signature.

What I'm currently doing is to use other function to pass and transform the data

like this:

export const login = action(loginFormSchema, async ({ username, password }) => {
  const authRequest = auth.handleRequest("GET", context);
  const key = await auth.useKey("username", username, password);
  const session = await auth.createSession({
    userId: key.userId,
    attributes: {},
  });
  authRequest.setSession(session);
  return;
});

export async function loginFormAction(prevState: any, formData: FormData) {
  "use server";
  const username = formData.get("username") as string;
  const password = formData.get("password") as string;
  const result = await login({ username, password });
  return result;
}

I was trying to create a wrapper function.

export function toFormAction<
  Schema extends z.ZodTypeAny,
  Data extends any,
  PrevState = any,
>(
  action: SafeAction<Schema, Data>,
): (
  prevState: PrevState,
  formData: FormData,
) => ReturnType<SafeAction<Schema, Data>> {
  return (prevState: PrevState, formData: FormData) => {
    const data = Object.fromEntries(formData.entries());
    return action(data as Data);
  };
}

export const actionFormTheForm = toFormAction(normalAction);

Is there an easier built-in approach to these?

Accept FormData as input

Most of Next's docs are passing server actions to the action parameter of form:

import { cookies } from 'next/headers';
 
export default function AddToCart({ productId }) {
  async function addItem(data) {
    'use server';
 
    const cartId = cookies().get('cartId')?.value;
    await saveToDb({ cartId, data });
  }
 
  return (
    <form action={addItem}>
      <button type="submit">Add to Cart</button>
    </form>
  );
}

However, next-safe-action's ClientAction doesn't take FormData as an input. This means that we have to do a lot of casting to make actions work:

    <form
      action={async (formData) => {
        "use server";

        await loginUser({
          username: formData.get("username") as string,
          password: formData.get("password") as string,
        });
      }}
    >

Maybe we could make it accept FormData as an input and let Zod do the work?

    <form
      action={async (formData) => {
        "use server";

        await loginUser(formData);
      }}
    >

[ASK] Error: Cannot access 'action' before initialization

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

No response

Ask a question

Hello,

First of all, thank you for this repo.

However, I have a problem that I have been trying to solve for several days.

Indeed, on my feature.action file, I use an action(), and I have a synchronization error that pushes the error "Error: Cannot access 'action' before initialization".

Note: when I reload the page, no error is detected. However, as soon as I navigate through my app, this error is systematically detected. I don't know why...

Capture dโ€™eฬcran 2024-03-08 aฬ€ 09 40 56

The code :

File : safe-action.ts

import { getServerSession } from "next-auth";
import { createSafeActionClient, DEFAULT_SERVER_ERROR } from "next-safe-action";
import { authOptions } from "./next-auth/auth";

export class ActionError extends Error {}

const handleReturnedServerError = async (e: Error) => {
  // If the error is an instance of `ActionError`, unmask the message.
  if (e instanceof ActionError) {
    console.error("Action error:", e.message);
    return e.message;
  }

  // Otherwise return default error message.
  return DEFAULT_SERVER_ERROR;
};

export const action = createSafeActionClient({
  // Can also be an async function.
  handleReturnedServerError,
});

File ("use server") : features.action.ts

"use server";
import {
  HandleResponseProps,
  handleRes,
} from "@/src/lib/error-handling/handleResponse";
import { prisma } from "@/src/lib/prisma";
import { ActionError, action } from "@/src/lib/safe-actions";
import { iFeature } from "@/src/types/iFeatures";
import { updateFeatureSchema } from "@/src/types/schemas/dbSchema";
import { z } from "zod";


export const getFeature = action(z.object({
  id: z.string().cuid(),
}), async ({
  id,
}): Promise<HandleResponseProps<iFeature>> => {
  try {
    const feature = await prisma.feature.findUnique({
      where: { id },
      include,
    });
    if (!feature) throw new ActionError("No feature found");
    return handleRes<iFeature>({
      success: feature,
      statusCode: 200,
    });
  } catch (ActionError) {
    return handleRes<iFeature>({ error: ActionError, statusCode: 500 });
  }
});

Here, the console error :

ReferenceError: Cannot access 'action' before initialization
    at Module.action (./src/lib/safe-actions.ts:4:53)
    at eval (./src/helpers/db/features.action.ts:47:74)
    at (action-browser)/./src/helpers/db/features.action.ts (/Users/hubertgiorgi/Sites/starter/.next/server/app/[locale]/(index)/page.js:1460:1)
    at __webpack_require__ (/Users/hubertgiorgi/Sites/starter/.next/server/webpack-runtime.js:33:43)
    at eval (./src/helpers/db/plans.action.ts:22:74)
    at (action-browser)/./src/helpers/db/plans.action.ts (/Users/hubertgiorgi/Sites/starter/.next/server/app/[locale]/(index)/page.js:1493:1)
    at __webpack_require__ (/Users/hubertgiorgi/Sites/starter/.next/server/webpack-runtime.js:33:43)
    at eval (./app/[locale]/admin/classes/stripeManager.ts:5:86)
    at (action-browser)/./app/[locale]/admin/classes/stripeManager.ts (/Users/hubertgiorgi/Sites/starter/.next/server/app/[locale]/(index)/page.js:1249:1)
    at __webpack_require__ (/Users/hubertgiorgi/Sites/starter/.next/server/webpack-runtime.js:33:43)
    at eval (./src/helpers/functions/stripeCustomerIdManager.ts:12:97)
    at (action-browser)/./src/helpers/functions/stripeCustomerIdManager.ts (/Users/hubertgiorgi/Sites/starter/.next/server/app/[locale]/(index)/page.js:1658:1)
    at __webpack_require__ (/Users/hubertgiorgi/Sites/starter/.next/server/webpack-runtime.js:33:43)
    at eval (./src/lib/next-auth/auth.ts:8:104)
    at (action-browser)/./src/lib/next-auth/auth.ts (/Users/hubertgiorgi/Sites/starter/.next/server/app/[locale]/(index)/page.js:1757:1)
    at __webpack_require__ (/Users/hubertgiorgi/Sites/starter/.next/server/webpack-runtime.js:33:43)
    at eval (./src/lib/safe-actions.ts:12:73)
    at (action-browser)/./src/lib/safe-actions.ts (/Users/hubertgiorgi/Sites/starter/.next/server/app/[locale]/(index)/page.js:1801:1)
    at __webpack_require__ (/Users/hubertgiorgi/Sites/starter/.next/server/webpack-runtime.js:33:43)
    at eval (./src/helpers/db/featuresCategories.action.ts:17:79)
    at (action-browser)/./src/helpers/db/featuresCategories.action.ts (/Users/hubertgiorgi/Sites/starter/.next/server/app/[locale]/(index)/page.js:1482:1)
    at Function.__webpack_require__ (/Users/hubertgiorgi/Sites/starter/.next/server/webpack-runtime.js:33:43)

Do you have any idea where the problem might be coming from? It also seems that the problem is only in this file, as I use other actions in other files, and this error is never detected.

Thanks.

Hubr'

Additional context

Another thing : I call getFeature from a client component un async (click button)

No response

[FEATURE] [v7] Support binding additional arguments

Problem

If you have a CRUD like model, you might want to provide two arguments instead of one (schema, and id of item being updated/removed). Personally, at the moment I'm doing something like this:

// /app/items/[id]/actions.ts

export const updateItem = action(z.tuple([schema, idSchema]), async (args, { updatedById }) => {
  const [data, id] = args
 
  return ItemModel.update({
    where: { id },
    data: {
      ...data,
      updatedById
    }
  })
})

Today I found out that you can bind additional arguments to actions like this:

Next.js solution

https://nextjs.org/docs/app/api-reference/functions/server-actions#binding-arguments

You can bind arguments to a Server Action using the bind method. This allows you to create a new Server Action with some arguments already bound. This is beneficial when you want to pass extra arguments to a Server Action.

// app/client-component.jsx
'use client'
 
import { updateUser } from './actions'
 
export function UserProfile({ userId }) {
  const updateUserWithId = updateUser.bind(null, userId)
 
  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Update User Name</button>
    </form>
  )
}

And then, the updateUser Server Action will always receive the userId argument, in addition to the form data:

// app/actions.js
'use server'
 
export async function updateUser(userId, formData) {
  // ...
}

Good to know: .bind of a Server Action works in both Server and Client Components. It also supports Progressive Enhancement.

next-safe-action

I wanted to ask others, what is their preferred way to do this.

Import of "next/dist/client/components/not-found" fails in "next-safe-action/dist/index.mjs" with ES Modules

Problem

I'm currently working on a project with Nextjs 14.0.4, Next Safe Action 5.2.1, and Vitest 1.0.4.
The problem I am facing is that when I run Vitest on a file that imports Next Safe Action, the tests fail with the following error:

Screenshot 2023-12-17 at 18 41 04

My understanding is that Vitest 1.0.4 is now running on Vite 5, which uses ES Modules by default.
This also means that, and please correct me if Iโ€™m wrong, ES Modules need to import CommonJs modules by adding the file extension. Since the Next Safe Action library is exported in the index.mjs and hook.mjs files as ES Modules, they should import the dependencies with the file extensions.

What I tried to solve the problem

I did the simplest thing I could try, I went to /node_modules/next-safe-action/dist/index.mjs and added the file extension to the imports, as:

// src/server.ts
import { isNotFoundError } from "next/dist/client/components/not-found.js";
import { isRedirectError } from "next/dist/client/components/redirect.js";

This solved the problem, although is not a permanent solution.

Suggestion

We add the file extensions to the imports in src/server.ts and src/hook.ts

Pass parameter to middleware serverside? [ASK]

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

No response

Ask a question

Is there a way to pass a variable to the middleware server side?

I want to setup some middleware that checks if a user has permission to perform an action. To do this I would like to be able to pass a value to the middleware (the permission the user requires), that the middleware can then check before performing the action.

I already have a function that takes the permission required as an input, and returns true or false depending on the user, but I see no way to pass this string to the middleware anywhere.

I could do it in the form, but this would leave it open to manipulation.

Currently I've just copy/pasted the action client for each permission, passing a different string for each.

Additional context

No response

Is it possible to get correct validation errors type back from an action defining schema with `zod-form-data`?

Currently we get Partial<Record<"_root", string[]>> out of an action defining schema with zod-form-data instead of the actual fields that were specified, which is cumbersome to use.

Example:

zfd.formData({
    id: zfd.text(z.string().uuid()),
    name: zfd.text(z.string().min(8).max(100)),
    is_public: zfd.checkbox(),
})

Is there a way to get the correct type out of an action with zfd schema?

Cheers

Enable sponsors

This is a valuable project, and you seem like a dedicated maintainer!

i there a way to use the action wrapper without input validation

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

No response

Ask a question

Hello, i wanted to ask if there is a way to use the createSafeActionClient wrapper without input validation and still getting all the status right ?

Additional context

No response

onSuccess Hook of useAction is not called if action returns undefined

// client component
const { execute } = useAction(deleteAssignmentAction, {
  onSuccess: () => {
    console.log('onSuccess was called')
  },
})

With this server action:

export const deleteAssignmentAction = teacherAction(idSchema, async (id, { user }) => {
  // delete assignment ...
  revalidatePath('/assignments')
})

The onSuccess is not called.
But in this case it is:

export const deleteAssignmentAction = teacherAction(idSchema, async (id, { user }) => {
  // delete assignment ...
  revalidatePath('/assignments')
  return true
})

Destructure res object in useAction hook

First, I love this, thank you for sharing.

I've tried both Zact and this and I love that this builds in extensive error reporting, authentication and logging. It makes for a really nice dev experience and brings working with actions close to working with something like React Query. Great stuff.

I have a minor request.

At present the useAction (& useOptimisticAction) hooks return execute, isExecuting, & a res object which has data, serverError, validationError and fetchError.

Because the res object is not always returned, it means you cant directly destructure from res without checking res exists first. No bad thing but I prefer to check the elements under res for existence directly as it results in cleaner code.

Right now, my setup has to look like this:

const { execute, isExecuting, res } = useAction(addTask);
const data = res?.data;
const validationError = res?.validationError;
const serverError = res?.serverError;
const fetchError = res?.fetchError as Error;

Whereas I'd greatly prefer if I could do this:

const { execute, isExecuting, data, validationError, serverError, fetchError  } = useAction(addTask);

I've built myself a custom hook to be able to do this, but would be cleaner if it was the libraries default behaviour:

// useActionPlus.ts custom hook
import { useAction } from 'next-safe-action/hook';
import type { z } from 'zod';

type ClientAction<IV extends z.ZodTypeAny, AO> = (
  input: z.input<IV>
) => Promise<{
  data?: AO;
  serverError?: true;
  validationError?: Partial<Record<keyof z.input<IV>, string[]>>;
}>;

export const useActionPlus = <const IV extends z.ZodTypeAny, const AO>(
  clientAction: ClientAction<IV, AO>
) => {
  const { execute, isExecuting, res } = useAction(clientAction);
  const data = res?.data;
  const serverError = res?.serverError;
  const validationError = res?.validationError;
  const fetchError: unknown = res?.fetchError;

  return {
    execute,
    isExecuting,
    data,
    serverError,
    validationError,
    fetchError,
  };
};

Appreciate your thoughts, perhaps there's an easy solution I'm not thinking of that requires no code change?

Server action always return { serverError: true }

Hi, i'm making some api call in my serverAction, but i cannot get the error.
At the moment i'm trying somehting like this

export const createUser = action(createUserSchema, async (data) => {
    try {
        const res = await service({
            url: "/users/",
            method: "post",
            data,
        })
        return res.data
    } catch (error: any) {
        throw new Error(error?.response?.data?.message || "GenericError")
    }
})

In my client however i always get the { serverError: true }

Maybe something like this ?

export const action = createSafeActionClient({
    serverCustomErrorReturn: (e) => {
        return { myErrorKey:e}
    }
})

[BUG] - NextJs passing additional arguments to server form action .bind(null, userId) is not working

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Describe the bug

I want to pass additional arguments as the NextJs doc said
, mainly for edition while a I need an id along with the form values.

// form.tsx
'use client'
 
import { updateUser } from './actions'
 
export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)
 
  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Update User Name</button>
    </form>
  )
}
// server actions
'use server'
 
import { SafeAction } from 'next-safe-action';

export const updateUser = async action(UserSchema, (userId, formData) => {
  // ...
})

Reproduction steps

use the exemple in the Next doc

Expected behavior

The update should be working and the data saved

Reproduction example

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#binding-arguments

Operating System

Windows 10

Library version

6.0.2

Additional context

No response

Feature Request: better pattern for using actions with intellisense

I really enjoy this pattern from create-t3-app with trpc:

import { api } from "~/trpc/server";
const hello = await api.post.hello.query({ text: "from tRPC" }); //example from create-t3-app boilerplate 

It would be nice to have an api/actions object with all actions in it for easy access with intellisense. Something like this:

import { nsa } from "~/nsa/actions";
const foo = await nsa.post.createPost({...})

https://create.t3.gg/

A "use server" file can only export async functions issue

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

6.1.0

Ask a question

After exporting my safe action objects, I get the following error, which I did not get when doing the same operation in older versions
A "use server" file can only export async functions, found object.

createSafeActionClient

image

My Custom Safe Action

safe-action

Additional context

No response

Consistent `types.ts` Filename

I have been reusing the types in types.ts in my project and noticed that each minor version the types.ts file has its name change with a hash:

Is there a specific design reason behind these name changes? If not, would it be possible to maintain a consistent filename or export the types?

[ASK] Optimistic update error handling

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

No response

Ask a question

How can I rollback to the previous state if I optimistically update data but the actual API call fails? In React Query, there's an onError and in there I can simply rollback to the previous state. See https://create.t3.gg/en/usage/trpc#optimistic-updates

Additional context

No response

Is this a turborepo issue?

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

6.1.0

Ask a question

I've been using next-safe-action since v3 and think this is a great tool. Please keep up the great work!

I recently migrated to a turborepo mono repo and am using pnpm as the package manager. In VSCode when looking at the safe-action file or the safe actions themselves code and when I try to build I get this error.

vbpay:build: Type error: The inferred type of 'action' cannot be named without a reference to '../../node_modules/next-safe-action/dist/index-7AaVMS6R.mjs'. This is likely not portable. A type annotation is necessary.
nextjs:build:
nextjs:build: 2 | import { createSafeActionClient } from "next-safe-action";
nextjs:build: 3 |
nextjs:build: > 4 | export const action = createSafeActionClient({
nextjs:build: | ^
nextjs:build: 5 | async middleware() {
nextjs:build: 6 | const { userId } = auth();
nextjs:build: 7 | if (userId === null) {
nextjs:build: โ€‰ELIFECYCLEโ€‰ Command failed with exit code 1.

When I open ../../node_modules/next-safe-action/dist/index-7AaVMS6R.mjs I saw it had an error bc it was importing @decs/typeschema which wasn't installed so i installed that but still getting the same error.

The funny thing is everything works perfectly fine when I run in dev mode. Of course, if I have a type mismatch between my action schema and type im passing in its brutal because of these errors the typing doesn't work for me to know if its a real error or not.

Additional context

I thought maybe it was pnpm issue but I get the same error when using npm as the package manager for the turbo repo mono repo.

[BUG] Package is incompatible with Vitest

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Describe the bug

Importing anything from next-safe-action/hooks causes Vitest tests to crash due to how React is being imported in that file.
I don't fully understand how the module resolution works here so I cannot tell you exactly why this happens, but I do know that making the following change in the hooks.mjs file fixes the problem ๐Ÿ‘‡

diff --git a/dist/hooks.mjs b/dist/hooks.mjs
index 636b65d50449fa41e913bb5bb632370f8f4f9df9..7f48d3c48278d76c36a73ae6bf99bed51ba5679b 100644
--- a/dist/hooks.mjs
+++ b/dist/hooks.mjs
@@ -3,7 +3,8 @@
 // src/hooks.ts
 import { isNotFoundError } from "next/dist/client/components/not-found.js";
 import { isRedirectError } from "next/dist/client/components/redirect.js";
-import { useCallback, useEffect, useOptimistic, useRef, useState, useTransition } from "react";
+import pkg from 'react';
+const { useCallback, useEffect, useOptimistic, useRef, useState, useTransition } = pkg;

That's a patch I've applied temporarily to fix the issue in my project.

Reproduction steps

  1. Open https://stackblitz.com/edit/vitest-dev-vitest-6lgc1a?file=test%2Fbasic.test.tsx
  2. See test fail
  3. Comment out the 'next-safe-action/hooks' import and the useAction invocation further down
  4. See test pass

Expected behavior

The test should pass without failing due to how React is being imported.

Reproduction example

https://stackblitz.com/edit/vitest-dev-vitest-6lgc1a?file=test%2Fbasic.test.tsx

Operating System

macOS

Library version

6.0.1

Additional context

No response

[FEATURE] Return async function from hooks

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Suggest an idea

I would like to have an async function returned from hooks. This function would be executed from event handlers to set internal state with more granularity.

Additional context

No response

Dist files contain incorrect types

The version on npm contains the following.
Notice the <const Schema extends z.ZodTypeAny .etc.
For some reason they contain const which Typescript treats as a type.

/**
 * Initialize a new action client.
 * @param createOpts Options for creating a new action client.
 * @returns {Function} A function that creates a new action, to be used in server files.
 *
 * {@link https://github.com/TheEdoRan/next-safe-action/tree/main/packages/next-safe-action#project-configuration See an example}
 */
declare const createSafeActionClient: <Context>(createOpts?: {
    handleServerErrorLog?: ((e: Error) => MaybePromise<void>) | undefined;
    handleReturnedServerError?: ((e: Error) => MaybePromise<{
        serverError: string;
    }>) | undefined;
    middleware?: (() => MaybePromise<Context>) | undefined;
} | undefined) => <const Schema extends z.ZodTypeAny, const Data>(schema: Schema, serverCode: ServerCode<Schema, Data, Context>) => SafeAction<Schema, Data>;

[BREAKING CHANGE] Switch to context based client

The current way to instantiate a new client, that supports authenticated actions is:

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

export const action = createSafeActionClient({
  getAuthData: async () => {
    // return auth object
  },
});

Then, a new action that needs auth info is defined like this:

"use server";

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

export const exampleAction = action({ input, withAuth: true },
  async (parsedInput, { userId }) => {
    console.log(userId);
    ...
  }
);

This implementation is fine for authentication/authorization purposes, but it's extremely specific too.
There's a better, easier, and generic way to implement auth actions and anything else a user needs: context.

โš ๏ธ BREAKING CHANGE: Context based client

Using context, you can define multiple clients for different purposes. Here are two examples with a base client, and an auth one:

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

export const action = createSafeActionClient();
export const authAction = createSafeActionClient({
  buildContext: async () => {
    // return your context object, for example:
    return {
      userId: "123";
    }
  },
});

Then, a new action that needs auth info would be defined like this:

"use server";

import { z } from "zod";
import { authAction } from "@/lib/safe-action"; // import `authAction` client

// We don't need to pass an object with `input` and `withAuth` keys as first argument
// to the client here, because we know by the client name (`authAction`), that
// the second argument of function that defines server action's code will be { `userId` }.
export const exampleAction = authAction(input, async (parsedInput, { userId }) => {
    console.log(userId);
    ...
  }
);

With context based clients, the second argument of server action function (in this case { userId }), will always be the context object. If you don't define a buildContext function while instantiating a safe action client, then context would be an empty object.

The idea is to include these changes in version 3.0.0 of the library.

Import action in Client Component instead of passing it as prop from Server Component

As mentioned by @Apestein in another issue (#16), it's possible to directly import actions in Client Component, instead of passing them as props from Server Components.

This seems to work fine in most cases, however, I tried to do it in useOptimisticAction demo from example app, and in this specific case it does not work.

Here's the weird part. Server revalidation works as expected:

  • if I define likes, getLikes and incrementLikes in addlikes-action.ts file (so, no db.ts file);
  • if I use Prisma with a real database, even if I define the SELECT query (getLikes) in a different file.

I'll provide reproduction examples with working and non-working implementations soon.

[FEATURE] Support throwing server errors in addition to returning them

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

6.1.0

Ask a question

First of all, thanks for the package. It is awesome!

React Query requires a new Error trowing to fire onError callback and properly handle errors. NSA returns an object a data object with serverError entry inside with type string | undefined. Is there a way to throw a new Error instead of handling errors inside the onSuccess callback?

Thanks

const { mutate, error } = useMutation({
    mutationFn: (sum: string) => postPaymentAction({ sum }),
    onSuccess(data) {
      // data here
    },
    onError(error) {
      // NEVER WILL BE FIRED

      toast.error(error.message);
    },
  });

Additional context

No response

[ASK] How do I avoid FetchError: "TypeError: Cannot redefine property: $$id"

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

6.0.2

Ask a question

I assume I have made a silly error, but I keep getting a "Fetch Error" when running execute from useAction hook.

Everything works fine if I run the db query insertGroup(values) instead of execute, but the minute I try and use useAction I get "TypeError: Cannot redefine property: $$id"

I'm following developedbyed s tutorial, if that helps: https://www.youtube.com/watch?v=UKupfEuUc1M&t=71s

// safe-server-actions-client.ts

import { createSafeActionClient } from "next-safe-action"
export const serverActionClient = createSafeActionClient()
export default serverActionClient
/// create-group-action.ts
"use server"

import { insertGroupSchema } from "@/server/data/schema"
import { revalidatePath } from "next/cache"
import { insertGroup } from "../queries/groups"
import { serverActionClient } from "./utils/safe-sever-actions-client"

type ActionSuccess = { success: true; status: "success" }
type ActionError = { success: false; status: "error"; errorMessage: string }
type ActionResponse = ActionSuccess | ActionError

export const createGroupAction = serverActionClient(insertGroupSchema, async (formData): Promise<ActionResponse> => {
  if (!formData) return { success: false, status: "error", errorMessage: "No form data" }

  await insertGroup(formData)
  revalidatePath("/")
  revalidatePath("/groups")
  return { success: true, status: "success" }
})

export default createGroupAction
// 
"use client"

import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { groupsTable, insertGroupSchema } from "@/server/data/schema"
import { DevTool } from "@hookform/devtools"
import { zodResolver } from "@hookform/resolvers/zod"
import { Table } from "drizzle-orm"
import { ControllerRenderProps, useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "../ui/button"
import { Checkbox } from "../ui/checkbox"
import { toast } from "sonner"
import createGroupAction from "@/server/actions/create-group-action"
import { useAction } from "next-safe-action/hooks"
import { insertGroup } from "@/server/queries/groups"

import { FormMap } from "@/app/form-hooks"
import { Input } from "../ui/input"
import { Textarea } from "../ui/textarea"

export const newGroupFormMap: FormMap = {
  fields: [
    {
      name: "name",
      label: "Group Name",
      description: "This is the name of the group.",
      Component: Input,
    },
    {
      name: "description",
      label: "Group Description",
      description: "This is the description of the group.",
      Component: Textarea,
      componentProps: { placeholder: "GroupDescription" },
    },
    {
      name: "logoUrl",
      label: "Group Logo URL",
      description: "This is the URL of the group logo, including the prefix (for example https://)",
      Component: Input,
      componentProps: { placeholder: "https://example.com/logo.png" },
    },
    // {
    //   name: "active",
    //   label: "Group Active",
    //   description: "This is whether the group is active.",
    //   Component: Checkbox,
    //   componentProps: {},
    // },
  ],
} as const

const defaultValidationSchema = insertGroupSchema.pick({
  name: true,
  description: true,
  active: true,
})

type NewGroupFormProps = {
  tableSchema?: Table
  validationSchema?: any
  formMap?: typeof newGroupFormMap
}
export default function NewGroupForm({
  tableSchema = groupsTable,
  validationSchema = defaultValidationSchema,
  formMap = newGroupFormMap,
}: NewGroupFormProps) {
  const { execute, result, status } = useAction(createGroupAction, {
    onSuccess: (_data, { name }) => {
      toast.success(`Success: Group ${name} created`)
    },
    onError: (error, input) => {
      if (error.fetchError) toast.error(`Fetch Error: ${error.fetchError}. Failed to create ${input.name}`)
      if (error.serverError) {
        console.error(`${error.serverError}`)
        toast.error(`Server Error: Failed to create ${input.name}`)
      }
      if (error.validationErrors) toast.error(`Validation errors: ${error.validationErrors}`)
    },
  })
  const form = useForm<z.input<typeof validationSchema>>({
    resolver: zodResolver(validationSchema),
  })

  async function handleValid(values: z.output<typeof validationSchema>) {
    execute(values)
  }

  const handleInvalid = () => {
    // TODO: handle form submission error
    toast.error("Group creation failed")
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(handleValid, handleInvalid)} className="flex flex-col gap-4">
        {formMap.fields.map((formField) => {
          const { Component, componentProps, name, label, description } = formField

          const getProps = (field: ControllerRenderProps) => {
            let props = { ...componentProps }

            //	add additional props that require `field` value for Checkbox
            if (Component === Checkbox)
              props = {
                ...componentProps,
                checked: field.value,
                onCheckedChange: field.onChange,
              }

            return { ...field, ...props }
          }

          return (
            <FormField
              key={name}
              control={form.control}
              name={name}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>{label}</FormLabel>
                  <FormControl>
                    <Component {...getProps(field)} />
                  </FormControl>
                  <FormDescription>{description}</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
          )
        })}
        <Button aria-disabled={status === "executing"} type="submit">
          Submit
        </Button>
      </form>
      {/* <DevTool control={form.control} /> */}
    </Form>
  )
}

Additional context

No response

`useAction` triggering stale callback functions unexpectedly due to outdated references

Refs in the following lines of code always reference the callback functions initially passed instead of those passed in the most recent call because they never get updated.

const onExecuteRef = React.useRef(cb?.onExecute);
const onSuccessRef = React.useRef(cb?.onSuccess);
const onErrorRef = React.useRef(cb?.onError);
const onSettledRef = React.useRef(cb?.onSettled);

It this the expected behavior?

res `undefined` case not handled

Purpose

It could happen that the return value of clientCaller is undefined (for example when the next server is not running anymore) this results in an app crash whereas the hook should just enter onError

Screenshot 2023-10-12 at 22 09 27

An easy fix would be to access the .data property safely in getActionStatus

const hasSucceded = typeof res?.data !== "undefined";
const hasErrored =
    typeof res === "undefined" ||
    typeof res.validationError !== "undefined" ||
    typeof res.serverError !== "undefined" ||
    typeof res.fetchError !== "undefined";

Suggestion: Make error handling more flexible

Hello @TheEdoRan,

First of all thanks for building this library!

I saw you've included a new flag called "unmaskServerError" in the latest update, and I think having this option is a great idea. However, I've noticed that setting it to true exposes all types of errors to the client, which could pose a security risk.

I'd like to propose an improvement: offering users of this library the ability to specify which errors should be sent back to the client. This can be achieved by allowing users to pass a function as the unmaskServerError parameter, instead of just a boolean value.

Here's how this change can be implemented:

// Modify the definition of the `unmaskServerError` parameter like this:
// unmaskServerError?: boolean | ((e: unknown) => { serverError: string });

// ...

if (createOpts?.unmaskServerError) {
    // If 'unmaskServerError' is a function, let the user handle the error
    if (typeof createOpts.unmaskServerError === "function") {
        return createOpts.unmaskServerError(e);
    }

    if (isError(e)) {
        return { serverError: (e as Error).message };
    }

    if (typeof e === "string") {
        return { serverError: e };
    }

    // If the error type cannot be logged, issue a warning to the user.
    console.warn("Could not log server error:", e);
}

You can use this feature when creating a client like this:

import { DbError } from "./errors";

export const action = createSafeActionClient({
  unmaskServerError: (e: unknown) => {
    if (e instanceof DbError) {
      return { serverError: e.message };
    }

    return {
      serverError: "Oops!",
    };
  },
});

Here's an example of an action that throws an error:

export const deleteUser = action(input, async ({ userId }: any) => {
 try {
    await db.deleteUserById(userId);
 } catch {
    throw new DbError("Couldn't delete user")
 }

  return {
    deleted: true,
  };
});

Currently, I'm using this modification in my own fork of the library. I'd appreciate hearing your thoughts on whether it would make sense to incorporate this enhancement into the official library

Related: #15

useOptimisticAction only works if action returns an object

Just wanna say thanks and I love this lib. It's a game changer. There is just a small bug with useOptimisticAction hook. It only works if I pass an action that returns a object. If I pass an action that returns a boolean for example, the optimisticData object will always be empty {}.

const {
    execute,
    optimisticData,
    res,
    isExecuting
  } = useOptimisticAction(myAction, { isSaved: true }) //this works but if I pass an action that returns a boolean it will not
                                                           

[FEATURE] [v7] Support schema nested objects validation

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Suggest an idea

I wanted to implement a form where a user enters the customer info and the address info at the same time. I've made a custom zod validation like this:
z.object({address: addressSchema, customer: customerSchema })
I did it this way since I can automatically get the schema from drizzle-orm based on the db.
But the problem i faced when implementing this is that the zod validation breaks since flatten() can't have nested objects. Wouldn't it be better to implement the zod validation errors using the format() function instead so we can have nested objects?
The way I need to implement it currently is using two different actions so each form has it's own validation but I lose the ability to add the customer and address in a db transaction since they will be sepparate.

Additional context

No response

[FEATURE] Allow for actions with no validators (no parameters)

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Suggest an idea

I'd like to be able to use next-safe-action without needing to pass any schema as an input to the first parameter. This could be accomplished by send undefined as the first parameter.

image

Additional context

If maintainers of this repo think this would be a good idea, I would like to try to work on this one.

Thank you!

[FEATURE] Add isSuccess, isLoading etc to useAction return object

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Suggest an idea

While status already exists, I find myself needing to creating the same additional "boiler-plate" code around the status string value every time I use useAction; specifically creating boolean variables such as isLoading for button state.

While not a high priority, it would be nice if the response object was a more similar to projects like tanstack/react-query that include both the status string and boolean variables like:

  • isLoading
  • isSuccess
  • isError

I imagine any accepted change would need to be applied to useOptimisticAction too.

Additional context

No response

[FEATURE] dynamic field validation

Are you using the latest version of this library?

  • I verified that the issue exists in the latest next-safe-action release

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Suggest an idea

Suppose you are validating a user registration form and the email address has already been registered.

You might want the form's email input field to enter the error state and to display an error message underneath: "this email address has already been registered".

As far as I can tell, there is presently no way to return for validationErrors to reflect dynamic information (e.g. pulling from the database to find a duplicate).

The only thing that you can do is throw a general Error and handle that on the client as a serverError case.

Additional context

No response

Safe Actions on Edge

Is there a way to run safe actions on edge? I know with API routes and pages its:

export const runtime = "edge"

But I am not sure how to go about this with the library. Thanks for the help!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.