Level 22 / 阅读练习

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

用 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 题

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