首页
/ Modern.js 项目实战:使用容器组件实现状态共享

Modern.js 项目实战:使用容器组件实现状态共享

2025-07-08 06:45:42作者:虞亚竹Luna

前言

在现代前端开发中,如何优雅地管理应用状态和组件间的数据共享是一个重要课题。本文将基于 Modern.js 框架,介绍如何通过容器组件(Container Component)模式来实现跨页面的状态共享,构建一个联系人管理应用。

项目背景

在前面的开发中,我们已经完成了以下工作:

  1. 使用 UI 组件构建了基础界面
  2. 通过 Model 层分离了业务逻辑
  3. 实现了联系人列表的基本功能

现在我们需要解决一个新需求:在"全部联系人"和"已归档联系人"两个页面间共享状态,确保归档操作能实时反映在两个页面上。

核心概念解析

容器组件模式

容器组件是一种设计模式,它将组件分为两类:

  • 展示组件(Presentational Components):只负责 UI 渲染,不关心数据来源
  • 容器组件(Container Components):负责数据获取和状态管理,为展示组件提供数据

在 Modern.js 中,这种模式与框架提供的 Model 系统完美结合。

实现步骤详解

1. 提升数据加载层级

首先,我们将数据加载逻辑从页面组件提升到布局组件中:

// src/routes/layout.data.ts
export type LoaderData = {
  code: number;
  data: {
    name: string;
    avatar: string;
    email: string;
  }[];
};

export const loader = async (): Promise<LoaderData> => {
  const data = new Array(20).fill(0).map(() => {
    const firstName = name.firstName();
    return {
      name: firstName,
      avatar: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${firstName}`,
      email: internet.email(),
    };
  });

  return {
    code: 200,
    data,
  };
};

在布局组件中初始化 Model 数据:

// src/routes/layout.tsx
export default function Layout() {
  const { data } = useLoaderData() as LoaderData;
  const [{ items }, { setItems }] = useModel(contacts);
  
  // 初始化数据
  if (items.length === 0) {
    setItems(data);
  }

  // ...其他布局代码
}

2. 重构页面组件

原来的页面组件现在只需从 Model 获取数据:

// src/routes/page.tsx
function Index() {
  const [{ items }, { archive }] = useModel(contacts);

  return (
    <div className="container lg mx-auto">
      <Helmet>
        <title>All</title>
      </Helmet>
      <List
        dataSource={items}
        renderItem={info => (
          <Item
            key={info.name}
            info={info}
            onArchive={() => {
              archive(info.email);
            }}
          />
        )}
      />
    </div>
  );
}

已归档页面类似,但使用不同的数据源:

// src/routes/archives/page.tsx
function Index() {
  const [{ archived }, { archive }] = useModel(contacts);

  return (
    <div className="container lg mx-auto">
      <Helmet>
        <title>Archives</title>
      </Helmet>
      <List
        dataSource={archived}
        renderItem={info => (
          <Item
            key={info.name}
            info={info}
            onArchive={() => {
              archive(info.email);
            }}
          />
        )}
      />
    </div>
  );
}

3. 提取容器组件

发现两个页面有大量重复代码,我们提取出公共容器组件:

// src/containers/Contacts.tsx
function Contacts({
  title,
  source,
}: {
  title: string;
  source: 'items' | 'archived';
}) {
  const [state, { archive }] = useModel(contacts);

  return (
    <div className="container lg mx-auto">
      <Helmet>
        <title>{title}</title>
      </Helmet>
      <List
        dataSource={state[source]}
        renderItem={info => (
          <Item
            key={info.name}
            info={info}
            onArchive={() => {
              archive(info.email);
            }}
          />
        )}
      />
    </div>
  );
}

然后简化页面组件:

// src/routes/page.tsx
function Index() {
  return <Contacts title="All" source="items" />;
}

// src/routes/archives/page.tsx
function Index() {
  return <Contacts title="Archives" source="archived" />;
}

最佳实践建议

  1. 目录结构规范

    • 展示组件放在 components/ 目录下,使用目录形式(如 Avatar/index.tsx
    • 容器组件放在 containers/ 目录下,使用单文件形式(如 Contacts.tsx
  2. 职责划分原则

    • 展示组件:专注 UI 实现,可以包含复杂内部结构
    • 容器组件:保持简单,仅做数据连接,避免复杂逻辑
  3. 状态管理

    • 共享状态通过 Model 管理
    • 页面特有状态通过组件自身 state 管理

效果验证

完成上述重构后,应用具有以下特点:

  1. 归档操作会实时更新两个页面的显示
  2. 代码结构更清晰,职责分离明确
  3. 易于扩展新功能,如添加新的筛选条件

总结

通过 Modern.js 的容器组件模式,我们实现了:

  • 状态逻辑与 UI 的彻底分离
  • 跨页面状态共享
  • 代码的可维护性和可扩展性提升

这种架构模式特别适合中大型应用开发,能够有效管理复杂度,提高团队协作效率。