无法从下一个服务器操作中访问值 | Next.js 13.4

huangapple go评论57阅读模式
英文:

Can't Access Values from Next Server Actions | Next.js 13.4

问题

我好奇我们如何能够访问 Next.js 13 alpha 版本的服务器操作的返回值。这里是文档 来自 Next 团队供参考。

假设我有以下示例服务器操作用于执行输入验证:

async function validation(formData: FormData) { 
	'use server';
	
	const data = formData?.get("str");
	
	const str = JSON.stringify(data)
		.replace(/['"]+/g, '');

	if (!str) {
		return { error: "String is Required" }
	} 

	else if (!(/^[a-zA-Z0-9]{8}$/).test(str)) {
		return { error : "Invalid String Format" }
	}

	await redirect(`/scan-code=${str}`)
	return null;
} 

并且该操作挂钩到一个简单的表单:

<form action={validation}>
	<label>
		{error && error}
	</label>
	<input 
		placeholder="Enter String Here"
		required 
		name="str"
	/>
	<button type="submit">
		Submit
	</button>
</form>

在 Remix 中,这将是一个非常简单的情况,使用 action 和 useFetcher() 钩子,操作基本上看起来一样,只是在组件本身中,你会使用类似于

const fetcher = useFetcher()

然后

const error = fetcher.data?.error

并通过类似于

<label style={{ color: error ? 'red' : 'black' }}>
	{error && error}
</label>

处理 UI 中的错误来处理错误。

然而,与能够检索错误值并根据其是否存在进行渲染不同,我基本上只是返回一个错误或重定向到正确的页面,因此在 UI 中没有错误的反馈。

当尝试在第一个地方使用这些操作时,还会有类型错误,并且来自异步函数的承诺与预期的类型不匹配于 <form> 元素的 action 属性,该属性显然是字符串。

TS2322: Type '(formData: FormData) => Promise<{ error: string; } | null>' is not assignable to type 'string'

我很好奇我在这里做错了什么,以及对于 Next 的服务器操作,与 Remix 的 useFetcher()useActionData() 钩子相比,替代方案是什么,因为我觉得在上下文、本地存储或数据库中持久化错误是一个不必要的步骤。

将不胜感激地接受任何帮助。谢谢!

英文:

I'm curious how we'd be able to go about accessing returned values from Next.js 13's alpha release of server actions. Here's the documentation from the Next team for reference.

Let's say I have the following example server action to perform input validation:

async function validation(formData: FormData) { // needs &quot;formData&quot; or else errors
	&#39;use server&#39;;
	
	// Get the data from the submitted form
	const data = formData?.get(&quot;str&quot;);
	
	// Sanitize the data and put it into a &quot;str&quot; const
	const str = JSON.stringify(data)
		.replace(/[&#39;&quot;]+/g, &#39;&#39;)
	
	// Self explanatory
	if (!str) {
		return { error: &quot;String is Required&quot; }
	} 

	else if (!(/^[a-zA-Z0-9]{8}$/).test(str)) {
		return { error : &quot;Invalid String Format&quot; }
	}

	// Using next/navigation, redirect to another dynamic page
	// if the data is validated
	
	await redirect(`/scan-code=${str}`)
	return null;
	}

and that action is hooked to a simple form:

&lt;form action={validation}&gt;
	&lt;label&gt;
		{error &amp;&amp; error}
	&lt;/label&gt;
	&lt;input 
		placeholder=&quot;Enter String Here&quot;
		required 
		name=&quot;str&quot;
	/&gt;
	&lt;button type=&quot;submit&quot;&gt;
		Submit
	&lt;/button&gt;
&lt;/form&gt;

In Remix, this would be a pretty straightforward case of using an action and the useFetcher() hook, and the action would essentially look the same, except in the component itself, you'd use something like

const fetcher = useFetcher()

and then

const error = fetcher.data?.error

and handle errors in the UI by having something like

&lt;label style={{ color: error ? &#39;red&#39; : &#39;black&#39; }}&gt;
{error &amp;&amp; error}
&lt;/label&gt;

However, instead of being able to retrieve the error values and render them based on whether or not they exist, I'm essentially just either returning an error or redirecting to the proper page, so there's no feedback for errors in the UI.

There are also type errors when trying to use the actions in the first place, and promises from the async functions don't match up with the expected type of the action prop on the <form> element which is apparently a string.

(TS2322: Type &#39;(formData: FormData) =&gt; Promise&lt;{ error: string; } | null&gt;&#39; is not assignable to type &#39;string&#39;)

I'm curious what I'm doing wrong here, and what the alternatives to Remix's useFetcher() and useActionData() hooks would be for Next's server actions, as I feel like persisting errors in context, local storage, or a DB would be an unnecessary step.

Any and all help would be appreciated. Thank you!

答案1

得分: 1

根据GitHub讨论中非常简洁的回答,你只能在客户端渲染的表单中执行此操作。在我的情况下,我有一个服务器渲染的页面。该页面定义了内联服务器操作,然后将该操作传递给客户端渲染的表单。客户端渲染的表单使用另一个async函数包装服务器操作,然后将新的包装函数传递给form['action']属性。

// app/whatever/new/client-rendered-form.tsx

"use client";

import { useState } from "react";

export default ClientRenderedForm({
  serverAction
}: {
  serverAction: (data: FormData) => Promise<unknown>
}) {
  const [error, setError] = useState<unknown>();

  return (
    <form
      action={async (data: FormData) => {
        try {
          await serverAction(data);
        } catch (e: unknown) {
          console.error(e);
          setError(e);
        }
      }}
    >
      <label>
        <span>Name</span>
        <input name="name" />
      </label>

      <button type="submit">pull the lever!</button>
    </form>
  );
}
// app/whatever/new/page.tsx

import { redirect } from "next/navigation";

import ClientRenderedForm from "./client-rendered-form";

export default WhateverNewPage() {
  const serverAction = async (data: FormData) => {
    "use server";

    // 我不知道为什么需要'use server'指令,
    // 但确实需要。此文件已在服务器上执行

    // 此函数在服务器上运行。您可以按照自己的方式处理数据

    // 如果此函数引发异常,客户端包装器将捕获

    // 完成后,可能需要执行`redirect()`。我不知道是否需要`return`关键字

    return redirect("/somewhere/else");
  };

  return (
    <div className="fancy-layout">
      <ClientRenderedForm {...{ serverAction }} />
    </div>
  );
}

希望这能帮助你!对于这种新范式,文档尚未完善,所以仍处于早期阶段。

英文:

Based on the very terse responses on this GitHub discussion, you can only do this with a client-rendered form. In my case I have a server-rendered page. The page defines an inline server action, then passes that action to a client-rendered form. The client-rendered form wraps the server action with another async function, then passes the new wrapper function to the form[&#39;action&#39;] prop.

// app/whatever/new/client-rendered-form.tsx

&quot;use client&quot;;

import { useState } from &quot;react&quot;;

export default ClientRenderedForm({
  serverAction
}: {
  serverAction: (data: FormData) =&gt; Promise&lt;unknown&gt;
}) {
  const [error, setError] = useState&lt;unknown&gt;();

  return (
    &lt;form
      action={async (data: FormData) =&gt; {
        try {
          await serverAction(data);
        } catch (e: unknown) {
          console.error(e);

          setError(e);
        }
      }}
    &gt;
      &lt;label&gt;
        &lt;span&gt;Name&lt;/span&gt;
        &lt;input name=&quot;name&quot; /&gt;
      &lt;/label&gt;

      &lt;button type=&quot;submit&quot;&gt;pull the lever!&lt;/button&gt;
    &lt;/form&gt;
  );
}
// app/whatever/new/page.tsx

import { redirect } from &quot;next/navigation&quot;;

import ClientRenderedForm from &quot;./client-rendered-form&quot;;

export default WhateverNewPage() {
  const serverAction = async (data: FormData) =&gt; {
    &quot;use server&quot;;

    // i don&#39;t know why the &#39;use server&#39; directive is required,
    // but it is. this file is already executed on the server
    //
    // this function runs on the server. you may process the data
    // however you&#39;d like
    //
    // if this function throws, the client wrapper will catch
    //
    // when you are finished, you likely need to execute a
    // `redirect()`. i don&#39;t know if the `return` keyword
    // is required

    return redirect(&quot;/somewhere/else&quot;);
  };

  return (
    &lt;div className=&quot;fancy-layout&quot;&gt;
      &lt;ClientRenderedForm {...{ serverAction }} /&gt;
    &lt;/div&gt;
  );
}

I hope this helps! It's still early days for this new paradigm, so the documentation hasn't been filled out yet.

答案2

得分: 0

以下是您要翻译的内容:

"I have also faced this issue when trying to build a login form. When the user submits the data, I need to inform them about the server response if the "email" or "password" do not match.

I spent some time tackling this problem, and here is the solution.

this solution also works when the javascript is disabled in the browser.

You can create a class that manipulates the server response like this:

class AtomicState {
  constructor(public message: string | undefined = undefined) {}

  setMessage(message: string) {
    this.message = message;
  }

  getMessage() {
    const message = this.message;
    this.message = undefined;
    return message;
  }
}

This class is responsible for setting and getting response messages. The message property is initially set to undefined. The setMessage method sets the message property, and the getMessage method saves the current value of the message property in the message variable, sets it to undefined again, and returns the value of the message variable.

You can use this class as follows:

const state = new AtomicState();

and your server action should be looks like that:

async function validation(formData: FormData) { // needs "formData" or else errors
    'use server';
    
    // Get the data from the submitted form
    const data = formData?.get("str");
    
    // Sanitize the data and put it into a "str" const
    const str = JSON.stringify(data)
        .replace(/[&#39;&quot;]+/g, '');
    
    // Self explanatory
    if (!str) {
        state.setMessage("String is Required")
        revalidatePath("CURRENT PATH");
    } 

    else if (!(/^[a-zA-Z0-9]{8}$/).test(str)) {
        state.setMessage("Invalid String Format")
        revalidatePath("CURRENT PATH");
    } else {
        // Using next/navigation, redirect to another dynamic page
        // if the data is validated
    
        await redirect(`/scan-code=${str}`)
    }
}

Using the revalidatePath function, NextJS will reload the page again with the new data.

According to the Official Page of NextJS, revalidatePath allows you to revalidate data associated with a specific path. This is useful for scenarios where you want to update your cached data without waiting for a revalidation period to expire.

You can access the data from the state and render it in the UI like this:

<form action={validation}>
    <label>
        {state.getMessage()}
    </label>
    <input 
        placeholder="Enter String Here"
        required 
        name="str"
    />
    <button type="submit">
        Submit
    </button>
</form>

Here is the whole file:

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

class AtomicState {
  constructor(public message: string | undefined = undefined) {}

  setMessage(message: string) {
    this.message = message;
  }

  getMessage() {
    const message = this.message;
    this.message = undefined;
    return message;
  }
}

const state = new AtomicState();

async function validation(formData: FormData) { // needs "formData" or else errors
    'use server';
    
    // Get the data from the submitted form
    const data = formData?.get("str");
    
    // Sanitize the data and put it into a "str" const
    const str = JSON.stringify(data)
        .replace(/[&#39;&quot;]+/g, '');
    
    // Self explanatory
    if (!str) {
        state.setMessage("String is Required")
        revalidatePath("CURRENT PATH");
    } 

    else if (!(/^[a-zA-Z0-9]{8}$/).test(str)) {
        state.setMessage("Invalid String Format")
        revalidatePath("CURRENT PATH");
    } else {
        // Using next/navigation, redirect to another dynamic page
        // if the data is validated
    
        await redirect(`/scan-code=${str}`)
    }
}

export default function Page() {
  return (
    <form action={validation}>
      <label>{state.getMessage()}</label>
      <input placeholder="Enter String Here" required name="str" />
      <button type="submit">Submit</button>
    </form>
  );
}
英文:

I have also faced this issue when trying to build a login form. When the user submits the data, I need to inform them about the server response if the "email" or "password" do not match.

I spent some time tackling this problem, and here is the solution.

this solution also works when the javascript is disabled in the browser.

You can create a class that manipulates the server response like this:

class AtomicState {
  constructor(public message: string | undefined = undefined) {}

  setMessage(message: string) {
    this.message = message;
  }

  getMessage() {
    const message = this.message;
    this.message = undefined;
    return message;
  }
}

This class is responsible for setting and getting response messages. The message property is initially set to undefined. The setMessage method sets the message property, and the getMessage method saves the current value of the message property in the message variable, sets it to undefined again, and returns the value of the message variable.

You can use this class as follows:

const state = new AtomicState();

and your server action should be looks like that:

async function validation(formData: FormData) { // needs &quot;formData&quot; or else errors
    &#39;use server&#39;;
    
    // Get the data from the submitted form
    const data = formData?.get(&quot;str&quot;);
    
    // Sanitize the data and put it into a &quot;str&quot; const
    const str = JSON.stringify(data)
        .replace(/[&#39;&quot;]+/g, &#39;&#39;)
    
    // Self explanatory
    if (!str) {
        state.setMessage(&quot;String is Required&quot;)
        revalidatePath(&quot;CURRENT PATH&quot;);
    } 

    else if (!(/^[a-zA-Z0-9]{8}$/).test(str)) {
        state.setMessage(&quot;Invalid String Format&quot;)
        revalidatePath(&quot;CURRENT PATH&quot;);
    } else {
        // Using next/navigation, redirect to another dynamic page
        // if the data is validated
    
        await redirect(`/scan-code=${str}`)
    }
}

> Using the revalidatePath function, NextJS will reload the page again with the new data.

> According to the Official Page of NextJS, revalidatePath allows you to revalidate data associated with a specific path. This is useful for scenarios where you want to update your cached data without waiting for a revalidation period to expire.

You can access the data from the state and render it in the UI like this:

&lt;form action={validation}&gt;
    &lt;label&gt;
        {state.getMessage()}
    &lt;/label&gt;
    &lt;input 
        placeholder=&quot;Enter String Here&quot;
        required 
        name=&quot;str&quot;
    /&gt;
    &lt;button type=&quot;submit&quot;&gt;
        Submit
    &lt;/button&gt;
&lt;/form&gt;

Here is the whole file:

import { revalidatePath } from &quot;next/cache&quot;;
import { redirect } from &quot;next/navigation&quot;;

class AtomicState {
  constructor(public message: string | undefined = undefined) {}

  setMessage(message: string) {
    this.message = message;
  }

  getMessage() {
    const message = this.message;
    this.message = undefined;
    return message;
  }
}

const state = new AtomicState();

async function validation(formData: FormData) { // needs &quot;formData&quot; or else errors
    &#39;use server&#39;;
    
    // Get the data from the submitted form
    const data = formData?.get(&quot;str&quot;);
    
    // Sanitize the data and put it into a &quot;str&quot; const
    const str = JSON.stringify(data)
        .replace(/[&#39;&quot;]+/g, &#39;&#39;)
    
    // Self explanatory
    if (!str) {
        state.setMessage(&quot;String is Required&quot;)
        revalidatePath(&quot;CURRENT PATH&quot;);
    } 

    else if (!(/^[a-zA-Z0-9]{8}$/).test(str)) {
        state.setMessage(&quot;Invalid String Format&quot;)
        revalidatePath(&quot;CURRENT PATH&quot;);
    } else {
        // Using next/navigation, redirect to another dynamic page
        // if the data is validated
    
        await redirect(`/scan-code=${str}`)
    }
}

export default function Page() {
  return (
    &lt;form action={validation}&gt;
      &lt;label&gt;{state.getMessage()}&lt;/label&gt;
      &lt;input placeholder=&quot;Enter String Here&quot; required name=&quot;str&quot; /&gt;
      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
}

huangapple
  • 本文由 发表于 2023年5月17日 14:38:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/76269170.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定