diff --git a/website/docs/contributing.md b/website/docs/contributing.md index eedff701..ac6349f1 100644 --- a/website/docs/contributing.md +++ b/website/docs/contributing.md @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 11 description: Learn how to contribute to next-safe-action via GitHub. --- diff --git a/website/docs/getting-started.md b/website/docs/getting-started.md index 5346c06c..b048fd98 100644 --- a/website/docs/getting-started.md +++ b/website/docs/getting-started.md @@ -7,22 +7,32 @@ description: Getting started with next-safe-action version 5. :::note This is the documentation for the current version of the library (5.x.x). If you are looking for version 3.x.x or 2.x.x docs, please check out [README_v3](https://github.com/TheEdoRan/next-safe-action/blob/main/packages/next-safe-action/README_v3.md) or [README_v2](https://github.com/TheEdoRan/next-safe-action/blob/main/packages/next-safe-action/README_v2.md) from the GitHub repository. - -If you're still using Next.js 13, please install version 4 of the library with `npm i next-safe-action@v4`. ::: :::info Requirements - v4: Next.js >= 13.4.2, v5: Next.js >= 14.0.0 - TypeScript >= 5.0.0 -- Zod >= 3.0.0 +- pre-v6: Zod >= 3.0.0, from v6: a validation library supported by [TypeSchema](https://typeschema.com/#coverage) ::: -**next-safe-action** provides a typesafe Server Actions implementation for Next.js 13 App Router, using Zod. +**next-safe-action** provides a typesafe Server Actions implementation for Next.js App Router. + +## Validation libraries support + +We will use Zod as our validation library in this documentation, but since version 6 of next-safe-action, you can use your validation library of choice, or even multiple and custom ones at the same time, thanks to the **TypeSchema** library. You can find supported libraries [here](https://typeschema.com/#coverage). ## Installation +For Next.js >= 14, use the following command: + +```bash npm2yarn +npm i next-safe-action +``` + +For Next.js 13, use the following command: + ```bash npm2yarn -npm i next-safe-action zod +npm i next-safe-action@v4 zod ``` ## Usage @@ -45,7 +55,7 @@ This is a basic client, without any options. If you want to explore the full set ### 2. Define a new action -This is how a safe action is created. Providing a Zod input schema to the function, we're sure that data that comes in is type safe and validated. +This is how a safe action is created. Providing a validation 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 the parsed input, and defines what happens on the server when the action is called from client. In short, this is your server code. It never runs on the client: ```typescript title="src/app/login-action.ts" diff --git a/website/docs/introduction.md b/website/docs/introduction.md index 048fb4c9..79cfa43b 100644 --- a/website/docs/introduction.md +++ b/website/docs/introduction.md @@ -1,11 +1,11 @@ --- sidebar_position: 1 -description: 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 Server Actions and execute them inside Client Components. +description: 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 typesafe Server Actions and execute them inside Client Components. --- # Introduction -**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** Server Actions and execute them inside Client Components. +**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 **typesafe** Server Actions and execute them inside Client Components. ## How does it work? @@ -20,6 +20,6 @@ Your browser does not support the video tag. - ✅ Pretty simple - ✅ End-to-end type safety - ✅ Context based clients (with middlewares) -- ✅ Input validation using Zod +- ✅ Input validation using multiple validation libraries - ✅ Advanced server error handling - ✅ Optimistic updates diff --git a/website/docs/migration-from-v5-to-v6.md b/website/docs/migration-from-v5-to-v6.md new file mode 100644 index 00000000..93eed540 --- /dev/null +++ b/website/docs/migration-from-v5-to-v6.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 10 +description: Learn how to migrate from next-safe-action version 5 to version 6. +--- + +# Migration from v5 to v6 + +WIP \ No newline at end of file diff --git a/website/docs/safe-action-client/custom-server-error-handling.md b/website/docs/safe-action-client/custom-server-error-handling.md index 1d2319cb..023a7564 100644 --- a/website/docs/safe-action-client/custom-server-error-handling.md +++ b/website/docs/safe-action-client/custom-server-error-handling.md @@ -15,9 +15,7 @@ Here's a simple example, changing the message for every error thrown on the serv export const action = createSafeActionClient({ // Can also be an async function. handleReturnedServerError(e) { - return { - serverError: "Oh no, something went wrong!", - }; + return "Oh no, something went wrong!"; }, }); ``` @@ -27,22 +25,21 @@ export const action = createSafeActionClient({ A more useful one would be to customize the message based on the error type. We can, for instance, create a custom error class and check the error type inside this function: ```typescript title=src/lib/safe-action.ts +import { DEFAULT_SERVER_ERROR } from "next-safe-action"; + class MyCustomError extends Error {} export const action = createSafeActionClient({ + // Can also be an async function. handleReturnedServerError(e) { // In this case, we can use the 'MyCustomError` class to unmask errors // and return them with their actual messages to the client. if (e instanceof MyCustomError) { - return { - serverError: e.message, - }; + return e.message; } - // Every other error will be masked with this message. - return { - serverError: "Oh no, something went wrong!", - }; + // Every other error that occurs will be masked with the default message. + return DEFAULT_SERVER_ERROR; }, }); ``` diff --git a/website/docs/types.md b/website/docs/types.md index edcba614..8cd3d5e1 100644 --- a/website/docs/types.md +++ b/website/docs/types.md @@ -5,29 +5,45 @@ description: List of exported types. # Types +## / + +### `SafeClientOpts` + +Type of options when creating a new safe action client. + +```typescript +export type SafeClientOpts = { + handleServerErrorLog?: (e: Error) => MaybePromise; + handleReturnedServerError?: (e: Error) => MaybePromise; + middleware?: (parsedInput: unknown) => MaybePromise; +}; +``` + ### `SafeAction` Type of the function called from Client Components with typesafe input data. ```typescript -type SafeAction = (input: z.input) => Promise<{ +type SafeAction = (input: InferIn) => Promise<{ data?: Data; serverError?: string; - validationError?: Partial | "_root", string[]>>; + validationErrors?: Partial | "_root", string[]>>; }>; ``` -### `ServerCode` +### `ServerCodeFn` Type of the function that executes server code when defining a new safe action. ```typescript -type ServerCode = ( - parsedInput: z.infer, +type ServerCodeFn = ( + parsedInput: Infer, ctx: Context ) => Promise; ``` +## /hooks + ### `HookResult` Type of `result` object returned by `useAction` and `useOptimisticAction` hooks. @@ -35,9 +51,7 @@ Type of `result` object returned by `useAction` and `useOptimisticAction` hooks. If a server-client communication error occurs, `fetchError` will be set to the error message. ```typescript -type HookResult = Awaited< - ReturnType> -> & { +type HookResult = Awaited>> & { fetchError?: string; }; ``` @@ -47,17 +61,17 @@ type HookResult = Awaited< Type of hooks callbacks. These are executed when action is in a specific state. ```typescript -type HookCallbacks = { - onExecute?: (input: z.input) => MaybePromise; - onSuccess?: (data: Data, input: z.input, reset: () => void) => MaybePromise; +type HookCallbacks = { + onExecute?: (input: InferIn) => MaybePromise; + onSuccess?: (data: Data, input: InferIn, reset: () => void) => MaybePromise; onError?: ( - error: Omit, "data">, - input: z.input, + error: Omit, "data">, + input: InferIn, reset: () => void ) => MaybePromise; onSettled?: ( - result: HookResult, - input: z.input, + result: HookResult, + input: InferIn, reset: () => void ) => MaybePromise; }; @@ -69,4 +83,10 @@ Type of the action status returned by `useAction` and `useOptimisticAction` hook ```typescript type HookActionStatus = "idle" | "executing" | "hasSucceeded" | "hasErrored"; -``` \ No newline at end of file +``` + +--- + +## TypeSchema library + +`Infer`, `InferIn`, `Schema` types come from [TypeSchema](https://typeschema.com/#types) library. \ No newline at end of file diff --git a/website/docs/usage-from-client/action-result-object.md b/website/docs/usage-from-client/action-result-object.md index 8ded5a43..0b53c466 100644 --- a/website/docs/usage-from-client/action-result-object.md +++ b/website/docs/usage-from-client/action-result-object.md @@ -11,5 +11,5 @@ Here's how action result object is structured (all keys are optional): | Name | When | Value | |--------------------|--------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `data?` | Execution is successful. | What you returned in action's server code. | -| `validationError?` | Data doesn't pass Zod schema validation. | A partial `Record` of input schema keys as key or `_root`, and `string[]` as value. `_root` is a reserved key, used for Zod global validation. Example: `{ _root: ["A global error"], email: ["Email is required."] }`. | +| `validationErrors?` | Input data doesn't pass schema validation. | A partial `Record` of input schema keys as key or `_root`, and `string[]` as value. `_root` is a reserved key, used for global validation issues. Example: `{ _root: ["A global error"], email: ["Email is required."] }`. | | `serverError?` | An error occurs during action's server code execution. | A `string` that by default is "Something went wrong while executing the operation" for every server error that occurs, but this is [configurable](/docs/safe-action-client/custom-server-error-handling#handlereturnedservererror) when instantiating a new client. | \ No newline at end of file diff --git a/website/docs/usage-from-client/hooks/callbacks.md b/website/docs/usage-from-client/hooks/callbacks.md index f1acb724..7df4b814 100644 --- a/website/docs/usage-from-client/hooks/callbacks.md +++ b/website/docs/usage-from-client/hooks/callbacks.md @@ -20,7 +20,7 @@ Here is the full list of callbacks, with their behavior explained. All of them a | Name | [`HookActionStatus`](/docs/types#hookactionstatus) state | Arguments | |--------------|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| `onExecute?` | `"executing"` | `input: z.input` | -| `onSuccess?` | `"hasSucceeded"` | `data: Data`,
`input: z.input`,
`reset: () => void` | -| `onError?` | `"hasErrored"` | `error: Omit, "data">`,
`input: z.input`,
`reset: () => void` | -| `onSettled?` | `"hasSucceeded"` or `"hasErrored"` (after `onSuccess` and/or `onError`) | `result: HookResult`,
`input: z.input`,
`reset: () => void` | \ No newline at end of file +| `onExecute?` | `"executing"` | `input: InferIn` | +| `onSuccess?` | `"hasSucceeded"` | `data: Data`,
`input: InferIn`,
`reset: () => void` | +| `onError?` | `"hasErrored"` | `error: Omit, "data">`,
`input: InferIn`,
`reset: () => void` | +| `onSettled?` | `"hasSucceeded"` or `"hasErrored"` (after `onSuccess` and/or `onError`) | `result: HookResult`,
`input: InferIn`,
`reset: () => void` | \ No newline at end of file diff --git a/website/docs/usage-from-client/hooks/useaction.md b/website/docs/usage-from-client/hooks/useaction.md index 77ae4ae1..2bd1da0e 100644 --- a/website/docs/usage-from-client/hooks/useaction.md +++ b/website/docs/usage-from-client/hooks/useaction.md @@ -29,6 +29,7 @@ export const greetUser = action(schema, async ({ name }) => { 2. In your Client Component, you can use it like this: ```tsx title=src/app/greet.tsx +import { useAction } from "next-safe-action/hooks"; import { greetUser } from "@/app/greet-action"; export default function Greet() { @@ -67,7 +68,7 @@ As you can see, here we display a greet message after the action is performed, i | Name | Type | Purpose | |-----------|----------------------------------------------|---------------------------------------------------------------------------------------------------| -| `execute` | `(input: z.input) => void` | An action caller with no return. The input is the same as the safe action you passed to the hook. | +| `execute` | `(input: InferIn) => void` | An action caller with no return. The input is the same as the safe action you passed to the hook. | | `result` | [`HookResult`](/docs/types#hookresult) | When the action gets called via `execute`, this is the result object. | | `status` | [`HookActionStatus`](/docs/types#hookresult) | The action current status. | | `reset` | `() => void` | You can programmatically reset the `result` object with this function. | diff --git a/website/docs/usage-from-client/hooks/useoptimisticaction.md b/website/docs/usage-from-client/hooks/useoptimisticaction.md index 206dd401..4ae6b748 100644 --- a/website/docs/usage-from-client/hooks/useoptimisticaction.md +++ b/website/docs/usage-from-client/hooks/useoptimisticaction.md @@ -62,6 +62,7 @@ export default function Home() { 3. Finally, in your Client Component, you can use it like this: ```tsx title=src/app/add-likes.tsx +import { useOptimisticAction } from "next-safe-action/hooks"; import { addLikes } from "@/app/add-likes-action"; type Props = { @@ -103,7 +104,7 @@ export default function AddLikes({ numOfLikes }: Props) { |------------------|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `safeAction` | [`SafeAction`](/docs/types#safeaction) | This is the action that will be called when you use `execute` from hook's return object. | | `initialOptimisticData` | `Data` (return type of the `safeAction` you passed as first argument) | An initializer for the optimistic state. Usually this value comes from the parent Server Component. | -| `reducer` | `(state: Data, input: z.input) => Data` | When you call the action via `execute`, this function determines how the optimistic update is performed. Basically, here you define what happens **immediately** after `execute` is called, and before the actual result comes back from the server. | +| `reducer` | `(state: Data, input: InferIn) => Data` | When you call the action via `execute`, this function determines how the optimistic update is performed. Basically, here you define what happens **immediately** after `execute` is called, and before the actual result comes back from the server. | | `callbacks?` | [`HookCallbacks`](/docs/types#hookcallbacks) | Optional callbacks. More information about them [here](/docs/usage-from-client/hooks/callbacks). | @@ -113,7 +114,7 @@ export default function AddLikes({ numOfLikes }: Props) { | Name | Type | Purpose | |------------------|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `execute` | `(input: z.input) => void` | An action caller with no return. The input is the same as the safe action you passed to the hook. | +| `execute` | `(input: InferIn) => void` | An action caller with no return. The input is the same as the safe action you passed to the hook. | | `result` | [`HookResult`](/docs/types#hookresult) | When the action gets called via `execute`, this is the result object. | | `status` | [`HookActionStatus`](/docs/types#hookresult) | The action current status. | | `reset` | `() => void` | You can programmatically reset the `result` object with this function. | diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 306242cc..a45ae072 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -51,44 +51,42 @@ export default { ], ], - themeConfig: - /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ - { - // Replace with your project's social card - image: "img/social-card.png", - algolia: { - appId: "I6TZS9IBSZ", - apiKey: "87b638e133658cdec7cc633e6c4986c3", - indexName: "next-safe-action", + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + themeConfig: { + // Replace with your project's social card + image: "img/social-card.png", + algolia: { + appId: "I6TZS9IBSZ", + apiKey: "87b638e133658cdec7cc633e6c4986c3", + indexName: "next-safe-action", + }, + navbar: { + title: "next-safe-action", + logo: { + alt: "next-safe-action", + src: "img/logo.svg", }, - navbar: { - title: "next-safe-action", - logo: { - alt: "next-safe-action", - src: "img/logo.svg", + items: [ + { + type: "docSidebar", + sidebarId: "docsSidebar", + position: "left", + label: "Docs", }, - items: [ - { - type: "docSidebar", - sidebarId: "docsSidebar", - position: "left", - label: "Docs", - }, - { - href: "https://github.com/TheEdoRan/next-safe-action", - label: "GitHub", - position: "right", - }, - ], - }, - footer: { - style: "light", - copyright: `Copyleft © ${new Date().getFullYear()} Edoardo Ranghieri`, - }, - prism: { - additionalLanguages: ["typescript"], - theme: themes.vsLight, - darkTheme: themes.vsDark, - }, + { + href: "https://github.com/TheEdoRan/next-safe-action", + label: "GitHub", + position: "right", + }, + ], + }, + footer: { + style: "light", + copyright: `Copyleft © ${new Date().getFullYear()} Edoardo Ranghieri`, }, + prism: { + additionalLanguages: ["typescript"], + theme: themes.vsDark, + }, + }, }; diff --git a/website/src/css/custom.css b/website/src/css/custom.css index fa2cd951..d2660781 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -28,6 +28,14 @@ body { transform: rotate(180deg); } +.navbar__brand::after { + content: "ALPHA"; + margin-left: 0.5rem; + font-weight: 500; + position: relative; + font-size: 70%; +} + :root { --ifm-color-primary: #da6f0c; --ifm-color-primary-dark: #c4640b;