2020 年编写 React 组件(带 Hooks)的五个常见错误

2025-05-25

2020 年编写 React 组件(带 Hooks)的五个常见错误

这篇文章最初发布于此处

React 作为框架

React 在 Web 开发领域已经存在了相当长一段时间,近年来,它作为敏捷 Web 开发工具的地位稳步提升。尤其是在新的 hook api/concept发布之后,编写组件变得前所未有的简单。

尽管 React 背后的团队和庞大的社区已经尽力以令人印象深刻的方式培训和解释该框架的概念,但我仍然看到一些在使用过程中容易犯的陷阱和错误。
我记录了过去几年中我所发现的所有与 React 相关的错误,尤其是在使用 Hooks 方面。在本文中,我将向你展示最常见的错误,并尝试详细解释为什么我认为它们是错误,以及如何以更简洁的方式进行处理。

免责声明

在开始列举之前,我必须声明,以下大多数问题并非根本性错误,乍一看也没什么问题,而且大多数问题不太可能影响应用程序的性能或外观。
除了产品开发人员之外,可能没有人会注意到这里出了问题,但我仍然相信,高质量的代码可以带来更好的开发者体验,从而带来更好的产品。

与任何软件框架或库一样,关于它有数百万种不同的观点。您在这里看到的所有内容均基于我的个人观点,不应被视为普遍规则。
如果您对她有不同的看法,我很乐意听取。

1. 不需要重新渲染时使用 useState

React 的核心概念之一是处理状态。你可以通过状态控制整个数据流和渲染。每次树再次渲染时,很可能都与状态的变化有关。

使用useStatehooks,你现在也可以在函数组件中定义状态,这是一种在 React 中处理状态的简洁易行的方法。但它也可能被滥用,正如我们在下面的例子中看到的那样。

对于下一个示例,我们需要一些解释。假设我们有两个按钮,一个按钮是计数器,另一个按钮使用当前计数发送请求或触发操作。但是,当前计数永远不会显示在组件中。只有当您点击第二个按钮时,请求才需要它。

这很危险❌

function ClickButton(props) {
  const [count, setCount] = useState(0);

  const onClickCount = () => {
    setCount((c) => x + 1);
  };

  const onClickRequest = () => {
    apiCall(count);
  };

  return (
    <div>
      <button onClick={onClickCount}>Counter</button>
      <button onClick={onClickRequest}>Submit</button>
    </div>
  );
}

问题

乍一看,你可能会问这到底有什么问题?状态不就是为此而设计的吗?
你当然是对的,这确实能正常工作,可能永远不会有问题。然而,在 React 中,每次状态变化都会强制该组件及其子组件重新渲染,但在上面的例子中,由于我们在渲染部分从未使用过该状态,所以每次设置计数器时都会产生不必要的渲染,这可能会影响性能或产生意想不到的副作用。

解决方案✅

如果您想在组件中使用一个变量,该变量应在渲染期间保留其值,但又不强制重新渲染,则可以使用useRef钩子。它会保留该值,但不会强制组件重新渲染。

function ClickButton(props) {
  const count = useRef(0);

  const onClickCount = () => {
    count.current++;
  };

  const onClickRequest = () => {
    apiCall(count);
  };

  return (
    <div>
      <button onClick={onClickCount}>Counter</button>
      <button onClick={onClickRequest}>Submit</button>
    </div>
  );
}

2. 使用 router.push 代替链接

这可能是一个非常简单和明显的问题,并且与反应本身并没有太大关系,但是当人们编写反应组件时我仍然经常看到它。

假设您要编写一个按钮,点击该按钮后用户将被重定向到另一个页面。由于它是一个单页应用 (SPA),因此此操作将采用客户端路由机制。因此,您需要某种库来实现此目的。
在 React 中,最常用的库是react-router,以下示例将使用该库。

因此添加点击监听器会将用户重定向到所需的页面,对吗?

这很危险❌

function ClickButton(props) {
  const history = useHistory();

  const onClick = () => {
    history.push('/next-page');
  };

  return <button onClick={onClick}>Go to next page</button>;
}

问题

虽然这对大多数用户来说都没什么问题,但在可访问性方面却存在很大问题。该按钮根本不会标记为链接到其他页面,这使得屏幕阅读器几乎无法识别。
另外,你能在新标签页或窗口中打开它吗?很可能不行。

解决方案✅

任何与用户交互的其他页面的链接都应尽可能由<Link>组件或普通<a>标签来处理。

function ClickButton(props) {
  return (
    <Link to="/next-page">
      <button>Go to next page</button>
    </Link>
  );
}

加分点:它还使代码更易读、更简短!

3. 通过 useEffect 处理操作

React 引入的最好、最周到的钩子之一是“useEffect”钩子。它能够处理propstate更改相关的操作。
尽管它功能实用,但也经常在不需要的地方使用。

假设有一个组件,它获取一个列表项并将其渲染到 DOM 中。此外,如果请求成功,我们希望调用“onSuccess”函数,该函数会以 prop 的形式传递给组件。

这很危险❌

function ClickButton(props) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  const fetchData = useCallback(() => {
    setLoading(true);
    callApi()
      .then((res) => setData(res))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, []);

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

  useEffect(() => {
    if (!loading && !error && data) {
      props.onSuccess();
    }
  }, [loading, error, data]);

  return <div>{data}</div>;
}

问题

这里有两个useEffect钩子,第一个钩子用于处理初始渲染时的 API 调用,第二个钩子会调用onSuccess函数,假设当没有加载、没有错误,但状态中有数据时,调用一定是成功的。明白了吗?

当然,对于第一次调用来说,这确实如此,而且可能永远不会失败。但是,你也失去了操作与需要调用的函数之间的直接联系。此外,无法 100% 保证这种情况只会在获取操作成功时发生,这是我们作为开发人员非常不喜欢的。

解决方案✅

一个直接的解决方案是将“onSuccess”函数设置为调用成功的实际位置:

function ClickButton(props) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  const fetchData = useCallback(() => {
    setLoading(true);

    callApi()
      .then((fetchedData) => {
        setData(fetchedData);
        props.onSuccess();
      })
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [props.onSuccess]);

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

  return <div>{data}</div>;
}

现在一眼就很清楚 onSuccess 何时被调用,正是在 api 调用成功的情况下。

4. 单一职责组件

组合组件可能很困难。
什么时候应该将一个组件拆分成几个更小的组件?
如何构建组件树?
所有这些问题在使用基于组件的框架时每天都会出现。
然而,在设计组件时,一个常见的错误是将两个用例合并到一个组件中。
让我们举一个标题栏的例子,它在移动设备上显示汉堡按钮,在桌面屏幕上显示标签页。(这个情况将由一个神奇的isMobile函数处理,但它不在本例中🧙‍)

这很危险❌

function Header(props) {
  return (
    <header>
      <HeaderInner menuItems={menuItems} />
    </header>
  );
}

function HeaderInner({ menuItems }) {
  return isMobile() ? <BurgerButton menuItems={menuItems} /> : <Tabs tabData={menuItems} />;
}

问题

这种方式会让 HeaderInner 组件同时充当两种不同的角色,而Jekyll 先生的经验告诉我们,同时充当多种角色并非理想状态。
此外,这种方式也使得测试或在其他位置复用该组件更加困难。

解决方案✅

将条件提高一级,可以更容易地看到组件的用途,并且它们只有一个职责,即成为一个HeaderTabs或一个BurgerButton,而不是试图同时成为两件事。

function Header(props) {
  return (
    <header>{isMobile() ? <BurgerButton menuItems={menuItems} /> : <Tabs tabData={menuItems} />}</header>
  );
}

5. 单一职责 useEffects

还记得以前我们只能用componentWillReceivePropscomponentDidUpdate方法来钩住 React 组件的渲染过程吗?这不仅勾起了我们一些黑暗的回忆,也让我们体会到使用useEffect钩子的美妙之处,尤其是你可以随心所欲地使用它们。

但有时忘记使用“useEffect”来做某些事情,会勾起那些不愉快的回忆。例如,假设你有一个组件,它以某种方式从后端获取一些数据,并且根据当前位置显示面包屑导航。(再次使用react-router获取当前位置。)

这很危险❌

function Example(props) {
  const location = useLocation();

  const fetchData = useCallback(() => {
    /*  Calling the api */
  }, []);

  const updateBreadcrumbs = useCallback(() => {
    /* Updating the breadcrumbs*/
  }, []);

  useEffect(() => {
    fetchData();
    updateBreadcrumbs();
  }, [location.pathname, fetchData, updateBreadcrumbs]);

  return (
    <div>
      <BreadCrumbs />
    </div>
  );
}

问题

有两个用例:“数据获取”和“显示面包屑”。两者都使用一个useEffect钩子进行更新。当函数或发生变化useEffect时,这个钩子就会运行。现在的主要问题是,当位置发生变化时,我们也会调用这个函数。 这可能是我们没有想到的副作用。fetchDataupdateBreadcrumbslocationfetchData

解决方案✅

拆分效果可确保它们仅用于一种效果,并且不会产生意外的副作用。

function Example(props) {
  const location = useLocation();

  const updateBreadcrumbs = useCallback(() => {
    /* Updating the breadcrumbs*/
  }, []);

  useEffect(() => {
    updateBreadcrumbs();
  }, [location.pathname, updateBreadcrumbs]);

  const fetchData = useCallback(() => {
    /*  Calling the api */
  }, []);

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

  return (
    <div>
      <BreadCrumbs />
    </div>
  );
}

加分点是,用例现在也在组件内按逻辑排序。

结论

在 React 中编写组件时,陷阱重重。你永远不可能 100% 理解整个机制,并避免犯下每一个小错误,甚至大大小小的错误。但在学习框架或编程语言时,犯错也很重要,而且可能没有人能够 100% 避免犯错。

我认为分享您的经验可以为其他人提供很大帮助或防止他们犯同样的错误。

如果您有任何疑问或等待,我不认为这是一个错误,请写信给我,我很想听听您的意见。

文章来源:https://dev.to/loweisz/ Five-common-mistakes-writing-react-components-with-hooks-in-2020-2ac3
PREV
使用 matcha.css 让裸体网站看起来很棒!
NEXT
像专业人士一样加速数据库查询