Skip to content

Instantly share code, notes, and snippets.

@mishushakov
Last active June 5, 2024 04:40
Show Gist options
  • Save mishushakov/c03f16942ba4af4c304996de22d72730 to your computer and use it in GitHub Desktop.
Save mishushakov/c03f16942ba4af4c304996de22d72730 to your computer and use it in GitHub Desktop.
A React hook for calling Next.js Server Actions from client components
'use client'
import { test } from './server'
import { useServerAction } from './hook'
export default function Home() {
const { data, loading, error, execute: testAction } = useServerAction(test)
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<>
<form action={testAction}>
<input type="text" name='name' />
<button type="submit">Submit</button>
</form>
<div>{data}</div>
</>
)
}
'use server'
export async function test(form: FormData) {
const name = form.get('name')
return name?.toString()
}
import { useState } from 'react'
type AsyncFunction = (...args: any) => Promise<any>
export function useServerAction<T extends AsyncFunction>(action: T) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null | undefined>(null)
const [data, setData] = useState<Awaited<ReturnType<T>> | null | undefined>(null)
async function execute (...payload: Parameters<T>) {
setLoading(true)
setError(null)
setData(null)
try {
const res = await action(payload)
setData(res)
} catch (e: any) {
setError(e)
}
setLoading(false)
}
return {
data,
loading,
error,
execute
}
}
@dev-SR
Copy link

dev-SR commented May 11, 2023

formdata validation options, something like this???

import { useState } from 'react';
import { z } from 'zod';
function formDataToObject(formData: FormData): { [key: string]: any } {
	const object: { [key: string]: any } = {};
	formData.forEach((value, key) => {
		if (!object.hasOwnProperty(key)) {
			object[key] = value;
		} else if (Array.isArray(object[key])) {
			object[key].push(value);
		} else {
			object[key] = [object[key], value];
		}
	});
	return object;
}

type AsyncFunction = (...args: any) => Promise<any>;
export function getPayloadSchema() {
	return z.object({
		// Define your payload schema using Zod's validation functions
		// For example:
		name: z.string().min(3).max(20)
	});
}

export function useServerAction<T extends AsyncFunction>(action: T, validator: z.Schema) {
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState<Error | null | undefined>(null);
	const [data, setData] = useState<Awaited<ReturnType<T>> | null | undefined>(null);

	async function execute(payload: FormData) {
		setLoading(true);
		setError(null);
		setData(null);

		try {
			// Validate the payload using the provided Zod schema
			const objectData = formDataToObject(payload);

			const validatedPayload = validator.parse(objectData);
			const res = await action(validatedPayload);
			setData(res);
		} catch (e: any) {
			setError(e);
		}

		setLoading(false);
	}

	return {
		data,
		loading,
		error,
		execute
	};
}
'use client';

import { useServerAction } from '@/lib/useServerAction';
import { test } from './action';
import { z } from 'zod';

export default function Form() {
	const {
		data,
		loading,
		error,
		execute: testAction
	} = useServerAction(
		test,
		z.object({
			name: z.string().min(3).max(20)
		})
	);
	if (loading) return <div>Loading...</div>;
	if (error) return <div>Error: {error.message}</div>;

	return (
		<form action={testAction} className='flex flex-col justify-center items-center h-full'>
			<input placeholder='Enter a value' name='name' />
			<button type='submit'>Add</button>
			<div>{JSON.stringify(data, null, 2)}</div>
		</form>
	);
}
'use server';
export async function test({ name }: { name: string }) {
	
	return name ;
}

@amosbastian
Copy link

I think it's basically the same as this? https://github.com/pingdotgg/zact

@mishushakov
Copy link
Author

@dev-SR keep in mind that server actions can accept anything, not just FormData

@mishushakov
Copy link
Author

@amosbastian similar concept, yes

@ChrisVilches
Copy link

I have a question. If you execute revalidatePath in the action, does await action(payload) wait for the new data (e.g. if you reload a list)?

This isn't explained in the docs, but I think it doesn't. I think that's why it recommends wrapping it in startTransition, which presumably somehow waits for it.

I have had some cases where there's a mismatch between my loading state and the timing where the data is updated (which is clear after using a sleep in the page that renders the new data).

@mishushakov
Copy link
Author

@ChrisVilches good question. I'm not an expert, so maybe ask @dan_abramov on twitter

@taylor-lindores-reeves
Copy link

I think it's basically the same as this? https://github.com/pingdotgg/zact

This package is good. Great work by @t3dotgg

@wacanam
Copy link

wacanam commented Sep 19, 2023

formdata validation options, something like this???

import { useState } from 'react';
import { z } from 'zod';
function formDataToObject(formData: FormData): { [key: string]: any } {
	const object: { [key: string]: any } = {};
	formData.forEach((value, key) => {
		if (!object.hasOwnProperty(key)) {
			object[key] = value;
		} else if (Array.isArray(object[key])) {
			object[key].push(value);
		} else {
			object[key] = [object[key], value];
		}
	});
	return object;
}

type AsyncFunction = (...args: any) => Promise<any>;
export function getPayloadSchema() {
	return z.object({
		// Define your payload schema using Zod's validation functions
		// For example:
		name: z.string().min(3).max(20)
	});
}

export function useServerAction<T extends AsyncFunction>(action: T, validator: z.Schema) {
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState<Error | null | undefined>(null);
	const [data, setData] = useState<Awaited<ReturnType<T>> | null | undefined>(null);

	async function execute(payload: FormData) {
		setLoading(true);
		setError(null);
		setData(null);

		try {
			// Validate the payload using the provided Zod schema
			const objectData = formDataToObject(payload);

			const validatedPayload = validator.parse(objectData);
			const res = await action(validatedPayload);
			setData(res);
		} catch (e: any) {
			setError(e);
		}

		setLoading(false);
	}

	return {
		data,
		loading,
		error,
		execute
	};
}
'use client';

import { useServerAction } from '@/lib/useServerAction';
import { test } from './action';
import { z } from 'zod';

export default function Form() {
	const {
		data,
		loading,
		error,
		execute: testAction
	} = useServerAction(
		test,
		z.object({
			name: z.string().min(3).max(20)
		})
	);
	if (loading) return <div>Loading...</div>;
	if (error) return <div>Error: {error.message}</div>;

	return (
		<form action={testAction} className='flex flex-col justify-center items-center h-full'>
			<input placeholder='Enter a value' name='name' />
			<button type='submit'>Add</button>
			<div>{JSON.stringify(data, null, 2)}</div>
		</form>
	);
}
'use server';
export async function test({ name }: { name: string }) {
	
	return name ;
}

the payload formData is most likely empyt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment