You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Remix.run博客教程:Optimistic UI实现及PostAdmin路由加载问题求助

Remix乐观UI实现问题解决思路

问题根源

直接在NewPost组件里渲染<PostAdmin />路由组件时,PostAdmin对应的loader函数不会自动执行——Remix的路由loader仅在**路由匹配(通过Outlet/Link跳转)**时才会触发,手动嵌套渲染路由组件不会触发loader,导致useLoaderData()返回undefined,解构posts时就会报错。

正确实现步骤

1. 重构组件,抽离可复用的列表展示部分

把PostAdmin中展示posts的逻辑抽成独立组件(比如PostList),避免直接渲染整个路由组件:

// components/PostList.tsx
export function PostList({ posts }) {
  return (
    <aside className="sidebar">
      <h3>所有文章</h3>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </aside>
  );
}

2. 调整路由结构,用嵌套路由实现父/子分工

将PostAdmin设为父路由,NewPost作为它的子路由:

  • 父路由routes/admin/posts.tsx(原PostAdmin)负责加载posts数据,并通过Outlet渲染子路由(NewPost):
import { useLoaderData, Outlet } from "@remix-run/react";
import { PostList } from "~/components/PostList";

// 父路由的loader,负责获取所有posts
export async function loader() {
  const posts = await fetch("/api/posts").then(res => res.json()); // 替换成你的实际数据获取逻辑
  return { posts };
}

export default function PostAdmin() {
  const { posts } = useLoaderData();
  return (
    <div className="admin-layout">
      <Outlet /> {/* 这里渲染NewPost组件 */}
      <PostList posts={posts} />
    </div>
  );
}
  • 子路由routes/admin/posts/new.tsx(NewPost)负责表单提交和乐观更新:
import { useTransition, Form, useLoaderData } from "@remix-run/react";
import { PostList } from "~/components/PostList";

// 子路由的action,负责创建新post
export async function action({ request }) {
  const formData = await request.formData();
  const newPost = await fetch("/api/posts", {
    method: "POST",
    body: formData
  }).then(res => res.json());
  return newPost;
}

export default function NewPost() {
  const transition = useTransition();
  // 获取父路由loader返回的posts数据
  const { posts } = useLoaderData();

  // 构建乐观更新后的posts列表
  let optimisticPosts = posts;
  if (transition.state === "loading" && transition.submission) {
    // 从提交的表单数据中提取临时post
    const formData = transition.submission.formData;
    const tempPost = {
      id: `temp-${Date.now()}`, // 临时ID,后续会被后端返回的真实ID替换
      title: formData.get("title"),
      createdAt: new Date().toLocaleString()
    };
    // 把临时post放到列表最前面
    optimisticPosts = [tempPost, ...posts];
  }

  return (
    <div className="new-post-form">
      <Form method="post">
        <input name="title" placeholder="文章标题" required />
        <textarea name="body" placeholder="文章内容" required />
        <button type="submit">发布文章</button>
      </Form>
      {/* 渲染乐观更新后的侧边栏列表 */}
      <PostList posts={optimisticPosts} />
    </div>
  );
}

3. 关键逻辑说明

  • 父路由的loader正常运行,NewPost通过useLoaderData()可以直接拿到父路由返回的数据(Remix会自动合并嵌套路由的loader数据)。
  • 利用useTransition()获取表单提交状态:当transition.state为loading时,从transition.submission中提取表单数据,生成临时post并插入到posts列表,实现乐观渲染。
  • 表单提交完成后,Remix会自动重新加载父路由的loader,更新列表为真实数据,替换掉临时post。

内容的提问来源于stack exchange,提问作者webknit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 11:25:36