Level 22 / 阅读练习

React 核心篇:Hooks 表单 请求 用户列表

用户列表的搜索、表单、请求、loading、error 完整实现。

Progress

阅读中

0/4 步完成 / 阅读后完成练习才算通关

Practice

本关怎么过

  • 读完正文后确认完成阅读
  • 完成 3 道选择题练习
  • 答错时看解释,再回到正文补理解

一句话:React 核心能力就是用 Hooks 管状态、管副作用、管表单和接口请求,把静态组件变成真实业务页面。

第 3 篇 / 共 12 篇。

本篇学完你会:用 React 19 + Hooks + TypeScript 写用户列表的搜索、请求、loading、error、新增编辑表单。

1. Hooks 解决什么问题

Hooks 是 React 函数组件里使用“组件能力”的方式。

Hook 大白话
useState 记住一份会变化的数据
useEffect 在渲染之后做副作用,比如请求接口
useMemo 缓存计算结果
useCallback 缓存函数引用
useRef 记住一个不会触发重渲染的值

React Hooks 用户列表请求流程

2. useState 管表单

搜索条件就是一份 state:

type UserQuery = {
  keyword: string;
  status: 'all' | 'active' | 'disabled';
};

const [query, setQuery] = useState<UserQuery>({
  keyword: '',
  status: 'all'
});

更新某一个字段时,不要丢掉其他字段:

setQuery((current) => ({
  ...current,
  keyword: event.target.value
}));

大白话:表单 state 像一张草稿纸,每次改一个字段,都要把整张草稿纸交回给 React。

3. useEffect 管接口请求

接口请求属于副作用。它不是单纯算 UI,而是去外部世界拿数据。

useEffect(() => {
  async function loadUsers() {
    const response = await fetch('/api/users');
    const data = await response.json();
    setUsers(data);
  }

  loadUsers();
}, []);

第二个参数 [] 表示这个 effect 只在组件挂载后跑一次。

如果搜索条件变化就重新请求:

useEffect(() => {
  loadUsers(query);
}, [query.keyword, query.status]);

这里故意没有直接写 [query]。因为 query 是一个对象,对象引用变化时容易让 effect 跑得比你想象中更频繁。初学阶段更推荐把真正影响请求的字段列清楚。

如果 loadUsers 写在组件里面,还要注意依赖关系。最稳的读法是:

useEffect(() => {
  async function load() {
    await loadUsers(query);
  }

  load();
}, [query.keyword, query.status]);

真实搜索框还会加 debounce,避免每输入一个字就请求一次。本文先讲清楚“条件变化 -> 重新请求”的主线。

4. 用户列表请求流程

真实页面不能只管成功,还要管 loading 和 error。

type UserState = {
  data: User[];
  loading: boolean;
  error: string | null;
};

const [userState, setUserState] = useState<UserState>({
  data: [],
  loading: false,
  error: null
});

请求时:

async function loadUsers(query: UserQuery) {
  setUserState((current) => ({ ...current, loading: true, error: null }));

  try {
    const search = new URLSearchParams({
      keyword: query.keyword,
      status: query.status
    });
    const response = await fetch(`/api/users?${search.toString()}`);

    if (!response.ok) {
      throw new Error('用户列表加载失败');
    }

    const data = (await response.json()) as User[];
    setUserState({ data, loading: false, error: null });
  } catch (error) {
    setUserState({
      data: [],
      loading: false,
      error: error instanceof Error ? error.message : '未知错误'
    });
  }
}

页面渲染:

{userState.loading && <p>加载中...</p>}
{userState.error && <p>{userState.error}</p>}
{!userState.loading && !userState.error && <UserList users={userState.data} />}

5. 新增和编辑表单

新增和编辑可以共用一个表单组件:

type UserFormValue = {
  name: string;
  email: string;
  role: 'admin' | 'member';
  status: 'active' | 'disabled';
};

type UserFormProps = {
  initialValue: UserFormValue;
  onSubmit: (value: UserFormValue) => Promise<void>;
};

表单组件内部管理草稿:

function UserForm({ initialValue, onSubmit }: UserFormProps) {
  const [value, setValue] = useState(initialValue);
  const [submitting, setSubmitting] = useState(false);

  async function handleSubmit(event: React.FormEvent) {
    event.preventDefault();
    setSubmitting(true);
    await onSubmit(value);
    setSubmitting(false);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={value.name} onChange={(event) => setValue({ ...value, name: event.target.value })} />
      <input value={value.email} onChange={(event) => setValue({ ...value, email: event.target.value })} />
      <button disabled={submitting}>{submitting ? '保存中...' : '保存'}</button>
    </form>
  );
}

6. useMemo 和 useCallback 什么时候用

不要一上来就乱用。

适合 useMemo 的情况:

const activeUsers = useMemo(() => users.filter((user) => user.status === 'active'), [users]);

适合 useCallback 的情况:

const handleDelete = useCallback(async (id: number) => {
  await deleteUser(id);
  await loadUsers(query);
}, [query]);

大白话:只有当计算比较贵、函数会传给子组件、或者依赖稳定性真的重要时再用。

7. 自定义 Hook

用户列表逻辑可以抽成 useUsers

function useUsers() {
  const [query, setQuery] = useState<UserQuery>({ keyword: '', status: 'all' });
  const [state, setState] = useState<UserState>({ data: [], loading: false, error: null });

  const reload = useCallback(async () => {
    setState((current) => ({ ...current, loading: true, error: null }));

    try {
      const data = await requestUsers(query);
      setState({ data, loading: false, error: null });
    } catch (error) {
      setState({
        data: [],
        loading: false,
        error: error instanceof Error ? error.message : '未知错误'
      });
    }
  }, [query.keyword, query.status]);

  useEffect(() => {
    reload();
  }, [reload]);

  return { query, setQuery, state, reload };
}

这里的 requestUsers(query) 代表真正请求接口的函数。它可以放在 api/users.ts 里,专门负责 fetch('/api/users?...')。这样 Hook 负责页面状态,接口函数负责和后端通信,边界会更清楚。

这样页面组件就更像在描述布局:

const { query, setQuery, state, reload } = useUsers();

8. 常见错误

错误 后果 修正
useEffect 依赖漏写 页面用旧数据 按规则补依赖
请求没有 loading 用户不知道发生了什么 增加 loading 状态
请求失败不处理 页面静默坏掉 增加 error 状态
表单直接改 props 数据来源混乱 用本地草稿 state
过早 useMemo 代码变复杂 先写清楚,再优化

9. 下一步怎么学

本篇重点是把用户管理页从“能显示”推进到“能交互”。

能力 对应任务
useState 搜索条件、表单草稿
useEffect 加载用户列表
loading/error 真实请求体验
自定义 Hook 抽出业务逻辑
表单组件 新增和编辑用户

下一篇建议:大白话讲解——React 进阶篇:路由 状态管理 权限 工程化.md

Checkpoint

读完后做题闯关

读完后做选择题检验理解,答错会显示解释。

0/3
先完成阅读

读完正文后点击“我已读完,进入练习”,题目会变成可答状态。

第 1 题

读完这一类文章后,最应该形成哪种记忆?

第 2 题

React/Vue 入门时,最应该先看懂哪条线?

第 3 题

工程化相关的前端文章,应该优先联系到什么?