Truely Full Stack with next.js + server component + sqlite + tailwindcss(二)

next.js全栈开发一个重要的环节就是处理表单交互,比如用户登陆、新建数据等。

首先安装tailwindcss Form插件

npm install -D @tailwindcss/forms

使用Form插件,修改tailwind.config.js

/tailwind.config.js
const config: Config = {
  ...,
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

然后新建一个Form表单组件

表单组件:/app/components/forms.tsx

'use client'

import { useFormState } from "react-dom";
import { SubmitButton } from "@/app/components/buttons";

const initialState = {
    message: '',
}

export function UserForm(props: {action: any}) {
    const {action} = props;
    const [state, formAction] = useFormState(action, initialState);
    
    return (
        <div className="w-[200px] text-left justify-items-start leading-8">
            <form action={formAction}>
                <div>
                    <span>phone number:</span>
                    <input type="tel" name="phone" size={32} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" />
                </div>
                <div>
                    <span>password:</span>
                    <input type="email" name="email" size={32} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" />
                </div>
                <div aria-live="polite" className="text-xl">
                    {state?.message}
                </div>
                <div>
                    <SubmitButton className="text-xl bg-indigo-50 opacity-70 border border-gray-300 rounded-md shadow-md mt-2 p-2" />
                </div>
            </form>
        </div>
    );
}

useFormState是React18新增的Hook,可以根据某个表单动作的结果更新 state。原型参考

useFormState(action, initialState, permalink?)

其中action为表单处理函数,initialState为初始state,我们可以将数据包装在state中,通过state来在客户端组件和服务器组件之间传递通信。

action: /app/components/actions.ts

import { addNewUser } from "@/app/components/server";
import { redirect } from "next/navigation";

export async function createNewUser(prevState: any, formData:FormData) {
    'use server'

    const phone = formData.get('phone')?.toString();
    if (!phone || phone === "") {
        return {
            'message': "phone can not be empty"
        }
    }
    const email = formData.get('email')?.toString() || "";

    let result;

    try {
        result = await addNewUser(phone, email);
        console.log(result);
    } catch(e) {
        return {
            'message': (e as Error).message
        };
    }
    if (!result) {
        return {
            'message': "error"
        }
    }
    
    redirect('/data');
}

处理表单的action中,第一个参数prevState是客户端组件传入的state,第二个参数是Form表单数据。

‘use server’表明该组件是运行于服务端。

处理表单逻辑或业务逻辑时发生任何异常,都可以通过返回一个state来告知客户端组件。例如

return {
    'message': "phone number can not be empty"
}

处理成功后,可以通过redirect重定向至成功页面,也可以通过返回成功消息的state告知客户端组件。

新建页面

添加/app/data/add目录,然后在该目录下新建page.tsx文件

/app/data/add/page.tsx

import { UserForm } from "@/app/components/forms";
import { createNewUser } from "@/app/components/actions";


export default function Page() {
    return (
        <div className="flex flex-col w-full h-full items-center text-center overflow-hidden">
            <div className="w-full">
                Form
            </div>
            <div>
                <UserForm action={createNewUser} />
            </div>
        </div>
    );
}

效果预览

http://localhost:3000/data/add

插入数据库

在/app/components/server.ts中新增


import sqlite3 from "sqlite3";

sqlite3.verbose();
const db = new sqlite3.Database('./db.sqlite');

export async function addNewUser(phone: string, email: string): Promise<any> {
    return new Promise((resolve, reject) => {
        db.serialize(() => {
            const stmt = db.prepare("INSERT INTO users(phone, email) VALUES (?, ?);")
            stmt.bind(phone, email);
            stmt.run((err) => {
                if (err) {
                    reject(err);
                }
            });
            stmt.finalize();

            db.get("SELECT *, rowid FROM users WHERE rowid=LAST_INSERT_ROWID();", (err, row) => {
                if (err) {
                    reject(err);
                }
                resolve(row);
            });
        });
    });
}
Copyright @lionared