---
title: ​完整使用 Model
---

上一章节中，我们初步引入**客户端应用架构**，从【 视图组件 】中拆分出【 业务模型（Model）】，`AllContacts` 中不再包含 UI 无关的业务逻辑实现细节，只需要使用 Model，就能实现同样的功能。

这一章节中，我们要进一步利用 Model 中实现的业务逻辑，让 `AllContacts` 和  `ArchivedContacts` 都从 BFF 获取数据，实现 Archive 按钮，点击按钮能把联系人归档，只显示在 Achives 列表里，不显示在 All 列表里。

先改造 `Item` 组件，增加 Archive 按钮的交互实现：

```tsx title="src/contacts/components/Item/index.tsx"
import Avatar from '../Avatar';

type InfoProps = {
  avatar: string;
  name: string;
  email: string;
  archived?: boolean;
};

const Item = ({
  info,
  onArchive,
}: {
  info: InfoProps;
  onArchive?: () => void;
}) => {
  const { avatar, name, email, archived } = info;
  return (
    <div className="flex p-4 items-center border-gray-200 border-b">
      <Avatar src={avatar} />
      <div className="ml-4 custom-text-gray flex-1 flex justify-between">
        <div className="flex-1">
          <p>{name}</p>
          <p>{email}</p>
        </div>
        <button
          type="button"
          disabled={archived}
          onClick={onArchive}
          className={`text-white font-bold py-2 px-4 rounded-full ${
            archived
              ? 'bg-gray-400 cursor-default'
              : 'bg-blue-500 hover:bg-blue-700'
          }`}>
          {archived ? 'Archived' : 'Archive'}
        </button>
      </div>
    </div>
  );
};

export default Item;
```

`ArchivedContacts` 和 `AllContacts` 需要共用同一套状态（联系人列表数据、联系人是否被归档），并且由于 Achives 列表和 All 列表都可能是第一屏页面（从不同 URL 访问），这两个组件都需要包含加载初始数据的逻辑（如果客户端没有联系人列表数据，就请求 BFF），所以这类两个组件公用的实现逻辑应该合并到一起：

我们删除原有的两个组件，创建一个新的 `Contacts` 组件：

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

<Tabs>
<TabItem value="macOS" label="macOS" default>

```bash
rm -r src/contacts/components/*Contacts
mkdir -p src/contacts/components/Contacts/
touch src/contacts/components/Contacts/index.tsx
```

</TabItem>
<TabItem value="Windows" label="Windows">

```powershell
rm -r src/contacts/components/*Contacts
mkdir -p src/contacts/components/Contacts/
ni src/contacts/components/Contacts/index.tsx
```

</TabItem>
</Tabs>

修改`components/Contacts/index.tsx` ，内容如下：

```tsx title="src/contacts/components/Contacts/index.tsx"
import { useEffect } from 'react';
import { useLocalModel } from '@modern-js/runtime/model';
import { List } from 'antd';
import contacts from '../../models/contacts';
import Item from '../Item';

const Contacts = ({ source }: { source: 'archived' | 'items' }) => {
  const [state, actions] = useLocalModel(contacts);
  const { items, error, pending } = state;
  useEffect(() => {
    if (!items.length && !error && !pending) {
      actions.load();
    }
  });

  const data = state.items.filter(item =>
    source === 'archived' ? item.archived : true,
  );

  return (
    (items.length && (
      <List
        dataSource={data}
        renderItem={info => (
          <Item
            key={info.email}
            info={info}
            onArchive={() => {
              actions.archive(info.email);
            }}
          />
        )}
      />
    )) || (
      <div className="p-4 items-center border-gray-200 border-b border-t custom-text-gray">
        Pending...
      </div>
    )
  );
};

export default Contacts;
```

:::info 注
由于 computed 功能还未提供，这里先在组件里将传入的数据做预处理。
:::

最后改造 `App.tsx`，利用 Contacts 实现 Archives 列表和 All 列表：

```tsx title="src/contacts/App.tsx"
import { useState } from 'react';
import { Radio, RadioChangeEvent } from 'antd';
import { Route, useHistory } from '@modern-js/runtime/router';
import { Helmet } from '@modern-js/runtime/head';
import 'tailwindcss/base.css';
import 'tailwindcss/components.css';
import 'tailwindcss/utilities.css';
import './styles/utils.css';
import Contacts from './components/Contacts';

function App() {
  const history = useHistory();
  const [currentList, setList] = useState(history.location.pathname || '/');
  const handleSetList = (e: RadioChangeEvent) => {
    const { value } = e.target;
    setList(value);
    history.push(value);
  };

  return (
    <div className="container lg mx-auto">
      <div className="h-16 p-2 flex items-center justify-center">
        <Radio.Group onChange={handleSetList} value={currentList}>
          <Radio value="/">All</Radio>
          <Radio value="/archives">Archives</Radio>
        </Radio.Group>
      </div>
      <Route path="/" exact={true}>
        <Helmet>
          <title>All</title>
        </Helmet>
        <Contacts source="items" />
      </Route>
      <Route path="/archives" exact={true}>
        <Helmet>
          <title>Archives</title>
        </Helmet>
        <Contacts source="archived" />
      </Route>
    </div>
  );
}

export default App;
```

执行 `pnpm run dev`，访问 `http://localhost:8080/contacts/`，点击 Archive 按钮后，可以看到按钮置灰：

![display](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/11/display.png)

接下来点击顶部导航，切换到 Archives 列表，我们预期的时候能看到列表里显示刚才归档的联系人，但实际上列表是空的：

![display7](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/11/display7.png)

出现这个问题的原因是，我们继续沿用了上一节的 `useLocalModel` API 来使用 Model，状态被保存到了组件内部的 state 里，而 `All` 列表和 `Archives` 列表中分别调用的 `Contacts` 组件，是两个各自独立的组件：

```tsx title="src/contacts/App.tsx"
<Route path="/" exact={true}>
  <Helmet>
    <title>All</title>
  </Helmet>
  <Contacts source="items" />
</Route>
<Route path="/archives" exact={true}>
  <Helmet>
    <title>Archives</title>
  </Helmet>
  <Contacts source="archived" />
</Route>
```

所以它们有各自独立的内部 state，互相不共享状态，渲染 `Archives` 列表的时候，`items` 仍然是初始状态。

要解决这个问题，一种方式把 `useLocalModel` 的逻辑提升到父组件里，把状态分别传给两个 `Contacts` 组件。更清晰、完善的方式，是启用全局唯一的「 应用状态 」，两个 `Contacts` 组件「 连接 」应用状态。

在 Modern.js 里实现应用状态管理很简单，只需要把 `useLocalModel` 换成 `useModel`。

修改 `components/Contacts/index.tsx` 的内容：


```tsx title="src/contacts/components/Contacts/index.tsx"
import { useEffect } from 'react';
import { useModel } from '@modern-js/runtime/model';
import { List } from 'antd';
import contacts from '../../models/contacts';
import Item from '../Item';

const Contacts = ({ source }: { source: 'archived' | 'items' }) => {
  const [state, actions] = useModel(contacts);
  const { items, error, pending } = state;
  useEffect(() => {
    if (!items.length && !error && !pending) {
      actions.load();
    }
  });

  const data = state.items.filter(item =>
    source === 'archived' ? item.archived : true,
  );

  return (
    (items.length && (
      <List
        dataSource={data}
        renderItem={info => (
          <Item
            key={info.email}
            info={info}
            onArchive={() => {
              actions.archive(info.email);
            }}
          />
        )}
      />
    )) || (
      <div className="p-4 items-center border-gray-200 border-b border-t custom-text-gray">
        Pending...
      </div>
    )
  );
};

export default Contacts;
```

重新执行 `pnpm run dev`，重复刚才的操作，可以看到 Archives 列表能正常显示了：

![display1](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/11/display1.png)

:::info 注
useModel API 还可以设置 Selector，只连接这个 Model 定义的状态中的局部。
:::

---

> 本小节的代码可以在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/tutorials/c11/hello-modern)。

