Hello World - Loader与Action联系人管理

知识库
知识库文档
/tech-stacks/remix/examples/Hello World - Loader与Action联系人管理.md

文档

Remix Loader/Action 联系人管理

目标

展示 Remix 核心模式:loader 加载数据、action 处理表单、无需 useState 管理状态。

完整代码

app/routes/_index.tsx

import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/node';
import { Form, useLoaderData, useActionData, useNavigation } from '@remix-run/react';

type Contact = { id: number; name: string; phone: string; email: string };
let contacts: Contact[] = [
  { id: 1, name: '张三', phone: '13800138000', email: 'zs@example.com' },
  { id: 2, name: '李四', phone: '13900139000', email: 'ls@example.com' },
];
let nextId = 3;

export async function loader({ request }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const q = url.searchParams.get('q') || '';
  const filtered = q ? contacts.filter(c => c.name.includes(q) || c.phone.includes(q)) : contacts;
  return json({ contacts: filtered, search: q });
}

export async function action({ request }: ActionFunctionArgs) {
  const fd = await request.formData();
  const intent = fd.get('_intent');

  if (intent === 'delete') {
    const id = Number(fd.get('id'));
    contacts = contacts.filter(c => c.id !== id);
    return json({ ok: true, msg: '已删除' });
  }

  if (intent === 'add') {
    const name = fd.get('name') as string;
    const phone = fd.get('phone') as string;
    const email = (fd.get('email') as string) || '';
    if (!name || !phone) {
      return json({ error: '姓名和电话为必填项' }, { status: 400 });
    }
    contacts.push({ id: nextId++, name, phone, email });
    return json({ ok: true, msg: '添加成功' });
  }
  return null;
}

export default function Index() {
  const { contacts: list, search } = useLoaderData<typeof loader>();
  const actionData = useActionData<typeof action>();
  const nav = useNavigation();
  const busy = nav.state === 'submitting';

  return (
    <main style={{ maxWidth: 600, margin: '40px auto', fontFamily: 'system-ui' }}>
      <h1>📞 联系人管理</h1>

      <Form method="get" style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        <input type="text" name="q" defaultValue={search} placeholder="搜索..." style={inputStyle} />
        <button type="submit" style={btnStyle}>搜索</button>
      </Form>

      {actionData?.ok && <p style={{ color: 'green' }}>{actionData.msg}</p>}
      {actionData?.error && <p style={{ color: 'red' }}>{actionData.error}</p>}

      <Form method="post" style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
        <input type="hidden" name="_intent" value="add" />
        <input name="name" placeholder="姓名*" required style={inputStyle} />
        <input name="phone" placeholder="电话*" required style={inputStyle} />
        <input name="email" placeholder="邮箱" style={inputStyle} />
        <button type="submit" disabled={busy} style={btnPrimaryStyle}>
          {busy ? '...' : '添加'}
        </button>
      </Form>

      <div>
        {list.map(c => (
          <div key={c.id} style={{ display: 'flex', justifyContent: 'space-between', padding: 12, background: 'white', borderRadius: 8, marginBottom: 8, boxShadow: '0 1px 3px rgba(0,0,0,.1)' }}>
            <div>
              <strong>{c.name}</strong>
              <div style={{ color: '#666', fontSize: '0.9em' }}>{c.phone} · {c.email}</div>
            </div>
            <Form method="post">
              <input type="hidden" name="_intent" value="delete" />
              <input type="hidden" name="id" value={c.id} />
              <button type="submit" style={{ background: '#ef4444', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>删除</button>
            </Form>
          </div>
        ))}
      </div>
    </main>
  );
}

const inputStyle = { flex: 1, padding: 8, border: '1px solid #ddd', borderRadius: 4 };
const btnStyle = { padding: '8px 16px', background: '#6b7280', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' };
const btnPrimaryStyle = { padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' };

运行步骤

npx create-remix@latest remix-contacts
cd remix-contacts
npm run dev

预期输出

  • 页面显示预设联系人,搜索框实时过滤
  • 填写表单添加联系人,提交后自动刷新(无需手动 setState)
  • 删除按钮直接提交 Form,Remix 自动重新验证 loader

信息

路径
/tech-stacks/remix/examples/Hello World - Loader与Action联系人管理.md
更新时间
2026/5/30