const Posts = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchPosts = async () => {
const result = await PostsApi.getPosts();
setPosts(result.slice(0, 75));
};
fetchPosts();
}, []);
...
Call external APIs
async function myServerAction() {
'use server';
// do something
}
async function myServerAction() {
'use server';
// do something
}
'use server'
async function myServerAction() {
// do something
}
async function anotherServerAction() {
// do something
}
async function oneMoreServerAction() {
// do something
}
export function MyFormComponent() {
function handleFormAction(
formData: FormData
) {
'use server';
const name = formData.get('name');
// do something
}
return (
<form action={handleFormAction}>
<input type={'name'} />
<button type="submit">Save</button>
</form>
);
}
action attribute on a form export function Form() {
async function handleSubmit() {
'use server';
// ...
}
return (
<form>
<input type="text" name="name" />
<button formAction={handleSubmit}>Submit</button>
</form>
);
}
formAction attribute on a button
'use client';
import { useTransition } from 'react';
import { saveData } from '../actions';
function ClientComponent({ id }) {
let [isPending, startTransition] = useTransition();
return (
<button onClick={() => startTransition(() => saveData(id))}>
Save
</button>
);
}
startTransition function of
useTransition hook
const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 50 }),
username: varchar('username', { length: 50 }),
email: varchar('email', { length: 50 })
});
export async function createUser(user: SelectUserWithoutId) {
await db.insert(users).values(user);
}
export async function updateUserById(user: SelectUser) {
await db
.update(users)
.set({ name: user.name, email: user.email, username: user.username })
.where(eq(users.id, user.id));
}
export async function deleteUserById(id: string) {
await db.delete(users).where(eq(users.id, id));
}
export async function addUser(formData: FormData) {
const newUser: SelectUserWithoutId = {
name: String(formData.get('name')) ?? '',
email: String(formData.get('email')) ?? '',
username: String(formData.get('username')) ?? ''
};
await createUser(newUser);
}
export async function updateUser(user: SelectUser, formData: FormData) {
const updatedUser: SelectUser = {
id: user.id,
name: String(formData.get('name')) ?? user.name,
email: String(formData.get('email')) ?? user.email,
username: String(formData.get('username')) ?? user.username
};
await updateUserById(updatedUser);
}
export async function deleteUser(userId: string) {
await deleteUserById(userId);
}
export function UserDialog({ user }: { user?: SelectUser }) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="w-full" size="sm" variant="outline">
{user ? 'Edit' : 'Add'}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
...
<form action={user ? updateUser.bind(null, user) : addUser}>
...
<form action={user ? updateUser.bind(null, user) : addUser}>
<form>
...
<DialogFooter>
<Button
type="submit"
onClick={() => {
setOpen(false);
}}
formAction={user ? updateUser.bind(null, user) : addUser}
>
Save changes
</Button>
</DialogFooter>
</form>
function UserRow({ user }: { user: SelectUser }) {
const userId = user.id;
const deleteUserWithId = deleteUser.bind(null, userId);
const [isPending, startTransition] = useTransition();
return (
<TableRow>
...
<TableCell>
<Button
className="w-full"
size="sm"
variant="outline"
onClick={() => startTransition(() => deleteUserWithId())}
>
...
revalidatePath to the rescueexport async function updateUser(user: SelectUser, formData: FormData) {
const updatedUser: SelectUser = {
id: user.id,
name: String(formData.get('name')) ?? user.name,
email: String(formData.get('email')) ?? user.email,
username: String(formData.get('username')) ?? user.username
};
await updateUserById(updatedUser);
revalidatePath('/');
}
function SubmitButton({ user, setOpen }) {
const { pending } = useFormStatus();
return (
<Button
type="submit"
formAction={user ? updateUser.bind(null, user) : addUser}
disabled={pending}
>
{pending && <ButtonSpinner />}
Save changes
</Button>
);
}
export async function deleteUser(userId: string) {
try {
await deleteUserById(userId);
} catch (e) {
throw new Error('Failed to delete user');
}
revalidatePath('/');
}
export type FieldError = {
name?: string[];
email?: string[];
username?: string[];
};
export type FormState = {
message: string;
status: 'idle' | 'pending' | 'error' | 'success';
errors?: FieldError;
};
const userSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Please enter a valid email address'),
username: z.string().min(3, 'Username must be at least 3 characters')
});
export async function updateUser(
user: SelectUser,
previousState: FormState,
formData: FormData
): Promise<FormState> {
const validatedFields = userSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
username: formData.get('username')
});
if (!validatedFields.success) {
return {
status: 'error',
message: 'Validation failed',
errors: validatedFields.error.flatten().fieldErrors
};
}
...
const [state, formAction] = useFormState(
user ? updateUser.bind(null, user) : addUser,
{ message: '', status: 'idle' }
);
{state && state.status === 'error' && state.errors && (
<div
id="email-error"
aria-live="polite"
className="text-sm text-red-500 text-right grid items-center"
>
{state.errors?.name?.map((error: string) => (
<p key={error}>{error}</p>
))}
{state.errors?.email?.map((error: string) => (
<p key={error}>{error}</p>
))}
{state.errors?.username?.map((error: string) => (
<p key={error}>{error}</p>
))}
</div>
)}
Stay Hungry. Stay Foolish.
— Steve Jobs