跳转至

Connecting to the Backend

useEffect( () => {} )

To execute a piece of code after a component is rendered.

Sending Http Requests#

  • fetch()
  • axios

npm i axios@1.3.4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import axios from 'axios'

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([])
  useEffect(()=>{
    axios.get<User[]>('http://jsonplaceholder.typicode.com/users')
      .then(res => setUsers(res.data));
  }, [])
  return <ul>
    {users.map(user => <li key={user.id}>{user.name}</li>)}
  </ul>

这里 UseState 需要类型标注,否则 users 列表的类型无法得知,其次 axios 返回的数据也需要类型标注,即 axios.get<User[]>

Handling Errors#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");

  useEffect(() => {
    axios
      .get<User[]>("http://jsonplaceholder.typicode.com/xusers")
      .then((res) => setUsers(res.data))
      .catch((err) => setError(err.message));
  }, []);

  return (
    <>
      {error && <p className="text-danger">{error}</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );

解决获取数据时的异常报错需要采用 .catch 方法,通过 useState 使其回显到页面上。

Async & Await (Optional)#

get 会返回一个 promise,有两个状态,resolvedrejected
在 JavaScript 中,如果我们有一个 promise,我们就可以在前面放 await 来获得结果。
由于 React 不允许我们传递 async 函数给 useEffect,我们需要在 useEffect 内部再定义一个 async 函数。

万一获取数据抛出异常报错了呢?这个时候需要在 async 函数内部使用 try...catch 来解决。const fetchUsers = async () => { try { const res = await ... } catch {err} }

其中我们无法在 catch 的形参做类型标注,只能在实参中使用 as 关键字(类型断言)setError((err as AxiosError).message)。React 的这种方法并不优雅,Mosh 更偏向于上一个方法,因为代码更为美观和简洁。

aysnc&await
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { useEffect, useState } from "react";
import axios, { AxiosError } from "axios";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const res = await axios.get<User[]>(
          "http://jsonplaceholder.typicode.com/xusers"
        );
        setUsers(res.data);
      } catch (err) {
        setError((err as AxiosError).message);
      }
    };
    fetchUsers();
  }, []);

  return (
    <>
      {error && <p className="text-danger">{error}</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );

Cancelling the Fetch Requests#

AbortController 是 JavaScript 中一个用于控制和管理异步操作(如网络请求)的接口。它可以帮助我们在不再需要时安全地取消这些操作,以节省资源和避免不必要的处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { useEffect, useState } from "react";
import axios, { CanceledError } from "axios";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");

  useEffect(() => {
    const controller = new AbortController();
    axios
      .get<User[]>("http://jsonplaceholder.typicode.com/users", {
        signal: controller.signal,
      })
      .then((res) => setUsers(res.data))
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
      });
    return () => controller.abort();
  }, []);

  return (
    <>
      {error && <p className="text-danger">{error}</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
  1. 创建 AbortController 实例
1
const controller = new AbortController();

AbortController 是一个内置的浏览器 API。通过调用它的构造函数,我们创建了一个新的 AbortController 实例。这个实例包含一个 signal 对象,我们可以用它来与异步操作进行交互。

  1. 传递信号给请求
1
2
3
axios.get<User[]>("http://jsonplaceholder.typicode.com/users", {
  signal: controller.signal,
})

在发起 axios 请求时,我们将 controller.signal 作为配置项的一部分传递给请求。这使得该请求与 AbortController 关联起来。

  1. 取消请求
1
return () => controller.abort();

useEffect 的返回值是一个清理函数,当组件卸载或 useEffect 再次运行时,该函数将被调用。在这里,我们调用 controller.abort(),这会触发与之关联的请求取消。

  1. 处理错误
1
2
3
4
.catch((err) => {
  if (err instanceof CanceledError) return;
  setError(err.message);
})

如果请求被取消,axios 会抛出一个 CanceledError。我们通过检查错误实例是否为 CanceledError 来决定是否处理该错误。如果是请求取消引起的错误,我们直接返回,否则设置错误信息。

结合组件的生命周期:

  1. 挂载时

    • 当组件首次挂载时,useEffect 内的代码会执行。创建一个 AbortController 实例,并通过 axios 发起网络请求。
    • 组件更新

    • 因为依赖数组是空的 ([]),所以这个 useEffect 只在组件挂载和卸载时运行一次,不会在组件更新时再次运行。

    • 卸载时

    • 当组件即将从 DOM 中移除时,useEffect 返回的函数(清理函数)会被调用。在这个清理函数中,调用 controller.abort() 取消进行中的网络请求。

如果不添加 if (err instanceof CanceledError) return; 这行代码,当请求被取消时,会捕获到 CanceledError,并设置错误消息。这会导致即使请求数据成功,用户列表也不会显示,因为之前的错误消息仍然存在。

Showing a Loading Indicator#

在 React 中,Promise 实例的 finally() 方法不起作用,因此只能在 then()catch() 方法内编写两次重复的代码。

那么如何显示加载动画呢?这里使用了 Bootstrap 中类为 spinner-border 的 div,通过 isLoading 状态控制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { useEffect, useState } from "react";
import axios, { CanceledError } from "axios";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);

    axios
      .get<User[]>("http://jsonplaceholder.typicode.com/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => controller.abort();
  }, []);

  return (
    <>
      {error && <p className="text-danger">{error}</p>}
      {isLoading && <div className="spinner-border"></div>}
      <ul>
        {users.map ((user) => (
          <li key={user.id}>{user. name}</li>
        ))}
      </ul>
    </>
  );
}

Deleting Data#

[[3-Managing Component State#Summary of Updating Objects and Array]]

两种更新数据的方式:

  • Optimistic update
    1. Update the UI
    2. Call the server
  • Pessimistic update
    1. Call the server
    2. Update the UI

Mosh 偏向第一种,因此呢,操作失败的情况下需要还原列表。

  1. 添加列表和列表项的类名

    • 为列表添加 list-group 类名。
    • 为列表项添加 list-group-item 类名。
  2. 添加按钮

    • 为按钮添加 btn btn-outline-danger 类名。
  3. 使用 Flex 布局

    • 为列表项添加 d-flex 类名,以启用 Flex 布局。
    • 添加 justify-content-between 类名,使列表项内容左右分布。
  4. 添加按钮的点击事件

    • 给按钮添加 onClick 事件处理程序,触发 deleteUser 函数。
    • deleteUser 函数首先使用 filter 过滤不与当前用户 ID 一致的用户(等于从列表中删除了该用户),然后发送请求到服务器删除用户。
  5. 如果请求出错,需要还原列表并显示错误信息。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { useEffect, useState } from "react";
import axios, { CanceledError } from "axios";

interface User {
  id: number;
  name: string;
}

function App () {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState ("");
  const [isLoading, setLoading] = useState (false);

  useEffect (() => {
    const controller = new AbortController ();

    setLoading (true);

    axios
      .get<User[]>(" http://jsonplaceholder.typicode.com/users" , {
        signal: controller. signal,
      })
      .then ((res) => {
        setUsers (res. data);
        setLoading (false);
      })
      .catch ((err) => {
        if (err instanceof CanceledError) return;
        setError (err. message);
        setLoading (false);
      });
    return () => controller.abort ();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [... users];
    setUsers (users.filter ((u) => u.id !== user. id));
    axios
      .delete (" http://jsonplaceholder.typicode.com/users/" + user. id)
      .catch ((err) => {
        setError (err. message);
        // 由于我们先操作 UI,如果没有删除服务器上的数据,出现了异常,应该还原列表。
        setUsers (originalUsers);
      });
  };

  return (
    <>
      {error && <p className="text-danger">{error}</p>}
      {isLoading && <div className="spinner-border"></div>}
      <ul className="list-group">
        {users.map ((user) => (
          <li
            key={user. id}
            className="list-group-item d-flex justify-content-between"
          >
            {user. name}
            <button
              className="btn btn-outline-danger"
              onClick={() => deleteUser (user)}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  );

Creating Data#

第 55 行 { data: savedUser }{data} 是解构的属性,而 savedUser 不是类型标注,而是别名。res 代表整个响应对象,不能直接使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { useEffect, useState } from "react";
import axios, { CanceledError } from "axios";

interface User {
  id: number;
  name: string;
}

function App () {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState ("");
  const [isLoading, setLoading] = useState (false);

  useEffect (() => {
    const controller = new AbortController ();

    setLoading (true);

    axios
      .get<User[]>(" http://jsonplaceholder.typicode.com/users" , {
        signal: controller. signal,
      })
      .then ((res) => {
        setUsers (res. data);
        setLoading (false);
      })
      .catch ((err) => {
        if (err instanceof CanceledError) return;
        setError (err. message);
        setLoading (false);
      });
    return () => controller.abort ();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [... users];
    setUsers (users.filter ((u) => u.id !== user. id));
    axios
      .delete (" http://jsonplaceholder.typicode.com/users/" + user. id)
      .catch ((err) => {
        setError (err. message);
        // 由于我们先操作 UI,如果没有删除服务器上的数据,出现了异常,应该还原列表。
        setUsers (originalUsers);
      });
  };

  const addUser = () => {
    const newUser = { id: 0, name: "Mosh" };
    const originalUsers = [... users];
    setUsers ([newUser, ... users]);

    axios
      .post (" http://jsonplaceholder.typicode.com/users/" , newUser)
      // .then ((res) => setUsers ([res. data, ... users]));
      .then (({ data: savedUser }) => setUsers ([savedUser, ... users]))
      .catch ((err) => {
        setUsers (originalUsers);
        setError (err. message);
      });
  };

  return (
    <>
      {error && <p className="text-danger">{error}</p>}
      {isLoading && <div className="spinner-border"></div>}
      {! isLoading && (
        <button className="btn btn-primary mb-3" onClick={() => addUser ()}>
          Add
        </button>
      )}
      <ul className="list-group">
        {users.map ((user) => (
          <li
            key={user. id}
            className="list-group-item d-flex justify-content-between"
          >
            {user. name}
            <button
              className="btn btn-outline-danger"
              onClick={() => deleteUser (user)}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  );

Updating data#

增加了一个更新按钮,其中类名 mx-1 增加左右边距。由于使用了 flex 布局,而且是 space-between,所以需要把两个按钮添加到一个 div 中,。

对于更新数据,可以使用 axiosput() 或者 patch() 方法,这取决于后端的选型。由于这里只更新了对象的一个属性,所以使用了 patch() 方法。

回顾:

这里对 React 如何更新对象不太熟悉,应该是先定义一个常量用来保存手动更改的对象,再使用 setUsers 方法通过 map() 和三元运算符去更新对象。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { useEffect, useState } from "react";
import axios, { CanceledError } from "axios";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);

    axios
      .get<User[]>("http://jsonplaceholder.typicode.com/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => controller.abort();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    setUsers(users.filter((u) => u.id !== user.id));
    axios
      .delete("http://jsonplaceholder.typicode.com/users/" + user.id)
      .catch((err) => {
        setError(err.message);
        // 由于我们先操作 UI,如果没有删除服务器上的数据,出现了异常,应该还原列表。
        setUsers(originalUsers);
      });
  };

  const addUser = () => {
    const newUser = { id: 0, name: "Mosh" };
    const originalUsers = [...users];
    setUsers([newUser, ...users]);

    axios
      .post("http://jsonplaceholder.typicode.com/users/", newUser)
      // .then((res) => setUsers([res.data, ...users]));
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setUsers(originalUsers);
        setError(err.message);
      });
  };

  const updateUser = (user: User) => {
    const updatedUser = { ...user, name: user.name + "!" };
    const originalUsers = [...users];

    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    axios
      .patch("http://jsonplaceholder.typicode.com/users/" + user.id, {
        updatedUser,
      })
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  return (
    <>
      {error && <p className="text-danger">{error}</p>}
      {isLoading && <div className="spinner-border"></div>}
      {!isLoading && (
        <button className="btn btn-primary mb-3" onClick={() => addUser()}>
          Add
        </button>
      )}
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <div>
              <button
                className="btn btn-outline-secondary mx-1"
                onClick={() => updateUser(user)}
              >
                Update
              </button>
              <button
                className="btn btn-outline-danger"
                onClick={() => deleteUser(user)}
              >
                Delete
              </button>
            </div>
          </li>
        ))}
      </ul>
    </>
  );

Extracting a Reusable API Client (Hard)#

1
2
3
4
5
6
7
8
9
└── 📁src
    └── App.tsx
    └── 📁assets
    └── 📁components
    └── index.css
    └── main.tsx
    └── 📁services
        └── api-client.ts
    └── vite-env.d.ts

新建 api-client.ts#

src 文件夹下创建一个 services 文件夹,用于配置 HTTP 请求,并在文件夹内新建一个 api-client.ts 用于定制 axiosuser-service.ts 用于处理用户相关逻辑。

api-client.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import axios, { CanceledError } from "axios";

export default axios.create({
  baseURL: "http://jsonplaceholder.typicode.com",
  headers: {
    // 'api-key': 'xxx'
  },
});

export { CanceledError };

axios 模块导入 axiosCanceledError,这样 main.tsx 不需要再次导入 axios。但是需要把 axios 替换为 apiClient

修改 App.tsx#

App.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import { useEffect, useState } from "react";
import apiClient, { CanceledError } from "./services/api-client";

interface User {
  id: number;
  name: string;
}

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);

    apiClient
      .get<User[]>("/users", {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => controller.abort();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    setUsers(users.filter((u) => u.id !== user.id));
    apiClient.delete("/users/" + user.id).catch((err) => {
      setError(err.message);
      // 由于我们先操作 UI,如果没有删除服务器上的数据,出现了异常,应该还原列表。
      setUsers(originalUsers);
    });
  };

  const addUser = () => {
    const newUser = { id: 0, name: "Mosh" };
    const originalUsers = [...users];
    setUsers([newUser, ...users]);

    apiClient
      .post("/users", newUser)
      // .then((res) => setUsers([res.data, ...users]));
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setUsers(originalUsers);
        setError(err.message);
      });
  };

  const updateUser = (user: User) => {
    const updatedUser = { ...user, name: user.name + "!" };
    const originalUsers = [...users];

    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    apiClient
      .patch("http://jsonplaceholder.typicode.com/users/" + user.id, {
        updatedUser,
      })
      .catch((err) => {
        setError(err.message);
        setUsers(originalUsers);
      });
  };

  return (
    <>
      ......
    </>
  );

新建 user-service.ts#

service 文件夹内新建一个 user-service.ts 用于处理用户相关服务,并导入 user-service.ts,把 App.tsx 内与用户相关的代码整合到当中,主要包括一个用户服务类,包含了获取所有用户、删除、创建和更新用户的方法,最后导出一个类的实例。

方法是一个 Promise,需要返回状态,因此一定要 return

user-service.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import apiClient from "./api-client";

export interface User {
  id: number;
  name: string;
}

class UserService {
  getAllUser() {
    const controller = new AbortController();
    const request = apiClient.get<User[]>("/users", {
      signal: controller.signal,
    });
    return { request, cancel: () => controller.abort() };
  }

  deleteUser(id: number) {
    return apiClient.delete("/users/" + id);
  }

  createUser(newUser: User) {
    return apiClient.post("/users", newUser);
  }

  updateUser(user: User) {
    return apiClient.patch(
      "http://jsonplaceholder.typicode.com/users/" + user.id,
      user
    );
  }
}

export default new UserService();

修改 App.tsx#

user-service.ts 的类实例和类型接口导入到 App.tsx 中,把 apiClient 替换为 userService 的各个方法。

默认导出(随意修改命名)和命名导出(不能修改命名):import userService, { User } from "./services/user-service";

由于 user-service.ts 导入了 apiClient,所以 App.jsx 不需要再重复导入。

App.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { useEffect, useState } from "react";
import { CanceledError } from "./services/api-client";
import userService, { User } from "./services/user-service";

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    const { request, cancel } = userService.getAllUser();
    request
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => cancel();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    setUsers(users.filter((u) => u.id !== user.id));

    userService.deleteUser(user.id).catch((err) => {
      setError(err.message);
      // 由于我们先操作 UI,如果没有删除服务器上的数据,出现了异常,应该还原列表。
      setUsers(originalUsers);
    });
  };

  const addUser = () => {
    const newUser = { id: 0, name: "Mosh" };
    const originalUsers = [...users];
    setUsers([newUser, ...users]);

    userService
      .creatUser(newUser)
      // .then((res) => setUsers([res.data, ...users]));
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setUsers(originalUsers);
        setError(err.message);
      });
  };

  const updateUser = (user: User) => {
    const updatedUser = { ...user, name: user.name + "!" };
    const originalUsers = [...users];

    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    userService.updateUser(updatedUser).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  return (
    <>
  .....
    </>
  );
}

新建 http-service.ts#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
└── 📁src
    └── App.tsx
    └── 📁assets
    └── 📁components
    └── index.css
    └── main.tsx
    └── 📁services
        └── api-client.ts
        └── http-service.ts
        └── user-service.ts
    └── vite-env.d.ts
http-service.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import apiClient from "./api-client";

interface Entity {
  id: number;
}

class HttpService {
  endpoint: string;
  constructor(endpoint: string) {
    this.endpoint = endpoint;
  }

  getAll<T>() {
    const controller = new AbortController();
    const request = apiClient.get<T[]>(this.endpoint, {
      signal: controller.signal,
    });
    return { request, cancel: () => controller.abort() };
  }

  delete(id: number) {
    return apiClient.delete(this.endpoint + "/" + id);
  }

  create<T>(entity: T) {
    return apiClient.post(this.endpoint, entity);
  }

  update<T extends Entity>(entity: T) {
    return apiClient.patch(
      "http://jsonplaceholder.typicode.com/users/" + entity.id,
      entity
    );
  }
}

const create = (endpoint: string) => new HttpService(endpoint);

export default create;

修改 user-service.ts#

user-service.ts
1
2
3
4
5
6
7
8
import create from "./http-service";

export interface User {
  id: number;
  name: string;
}

export default create("/users");

修改 App.tsx#

由于在 http-service.tsgetAll() 方法使用的是通用的类型标注,因此 userService.getAll(); 需要添加类型标注,即 userService.getAll<User>()

此处导入 user-service.ts 时重新命名为 userService 了。(默认导出 create("/user")

App.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { useEffect, useState } from "react";
import { CanceledError } from "./services/api-client";
import userService, { User } from "./services/user-service";

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    const { request, cancel } = userService.getAll<User>();
    request
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => cancel();
  }, []);

  const deleteUser = (user: User) => {
    const originalUsers = [...users];
    setUsers(users.filter((u) => u.id !== user.id));

    userService.delete(user.id).catch((err) => {
      setError(err.message);
      // 由于我们先操作 UI,如果没有删除服务器上的数据,出现了异常,应该还原列表。
      setUsers(originalUsers);
    });
  };

  const addUser = () => {
    const newUser = { id: 0, name: "Mosh" };
    const originalUsers = [...users];
    setUsers([newUser, ...users]);

    userService
      .create(newUser)
      .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
      .catch((err) => {
        setUsers(originalUsers);
        setError(err.message);
      });
  };

  const updateUser = (user: User) => {
    const updatedUser = { ...user, name: user.name + "!" };
    const originalUsers = [...users];

    setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));

    userService.update(updatedUser).catch((err) => {
      setError(err.message);
      setUsers(originalUsers);
    });
  };

  return (
    <>
     ......
    </>
  );
}

Creating a Custom Data Fetching Hook#

为了复用和模块化,所以需要封装 useEffect 钩子。(能跑就行 ^ ^ 防御型编程)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useEffect, useState } from "react";
import { CanceledError } from "../services/api-client";
import userService, { User } from "../services/user-service";

const useUsers = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    const { request, cancel } = userService.getAll<User>();
    request
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => cancel();
  }, []);
  return { users, error, isLoading, setUsers, setError };
};

export default useUsers;

如何使用呢?使用函数解构。

1
const { users, error, isLoading, setUsers, setError } = useUsers();

2024-07-06 00:35 2024-07-10 23:53

评论