Categories
程式開發

编写React组件时常见的5个错误


本文最初发布于lorenzweiss.de网站,经原作者授权由InfoQ中文站翻译并分享。

React框架

React在Web开发领域已经资格不浅了,近年来它作为敏捷Web开发工具的角色愈加深入人心。特别是新的hook API/概念发布之后,用React编写组件变得非常简单。

尽管React背后的团队和庞大的社区在努力推广普及这一框架的相关理念,但很多人在使用它时还是经常会遇到一些陷阱,犯一些常见的错误。我把过去几年中见过的所有hook相关的错误用法总结成了一个列表。在本文中,我想向大家展示其中一些最常见的错误,详细解释为什么我认为这些用法不对,并给出较简洁的正确方法的建议。

免责声明

开始以前我必须声明,下面列举的这些事情大都不是根本性的错误,或者初看上去没什么问题,也不大可能影响应用程序的性能或外观。除了产品的开发人员外,也许没人会注意到这里有些问题。但是我仍然相信,高质量的代码可以带来更好的开发体验,进而打造出更好的产品。

与其他任何软件框架或库一样,这里的不同意见数不胜数。本文的所有内容都基于我的个人观点,不应视为一般性规则。如果你有不同的看法,我洗耳恭听。

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

React的一个核心概念是处理状态。你可以通过状态控制整个数据流和渲染过程。每次树被重新渲染时,很可能是因为状态的变化。

使用useState hook,你现在还可以在函数组件中定义状态,这种方法可以真正简洁地在React中处理状态。但正如以下示例所示,它也可能被滥用。

关于下面这个示例我们需要说明一下。假设我们有两个按钮,一个按钮是计数器,另一个按钮使用当前计数发送请求或触发动作。但是,当前编号永远不会显示在组件内。当你单击第二个按钮时才需要这个请求。

这很危险❌

function ClickButton(props) {
  const [count, setCount] = useState(0);
  const onClickCount = () => {
    setCount((c) => c + 1);
  };
  const onClickRequest = () => {
    apiCall(count);
  };
  return (
    
); }

问题⚡

乍一看,你可能会问这到底有什么问题?状态不就是这样用的吗?你当然没错,它运行很正常,并且可能永远不会出问题,但是在React中,每个状态更改都将强制对该组件,很有可能还有其子级进行重渲染,但在上面的示例中,因为我们从未在渲染部分中使用这个状态,结果每次设置计数器时都会有不必要的重渲染,这可能会影响性能或产生意外的副作用。

解决方案✅

如果要在组件内部使用一个变量,希望该变量在渲染之间保持其值,但又不强制重新渲染,则可以使用useRef hook。它将保留值,但不强制重新渲染组件。

function ClickButton(props) {
  const count = useRef(0);
  const onClickCount = () => {
    count.current++;
  };
  const onClickRequest = () => {
    apiCall(count.current);
  };
  return (
    
); }

2. 使用router.push代替链接

这可能是一个显而易见的错误,其实和React本身没什么关系,但是当人们编写React组件时经常会犯这种错误。

假设你要编写一个按钮,单击该按钮应将用户重定向到另一个页面。由于它是一个SPA,因此这个动作是客户端路由机制。于是你需要某种库来执行此动作。在React中最流行的是react-router,下面的示例就会使用它。

所以,添加一个点击侦听器会将用户重定向到所需的页面,对吗?

这很危险❌

function ClickButton(props) {
  const history = useHistory();
  const onClick = () => {
    history.push('/next-page');
  };
  return ;
}

问题⚡

就算这段代码对于大多数用户来说都可以正常工作,但这里也有严重的可访问性问题。这个按钮根本不会被标记为链接到另一个页面,于是屏幕阅读器几乎无法识别它。而且你能在新标签页或窗口中打开它吗?很可能做不到。

解决方案✅

只要指向其他页面的链接带有某种用户交互,就要尽量用组件或常规的标签处理。

function ClickButton(props) {
  return (
    
      Go to next page
    
  );
}

优点:这也使代码更易读,更短!

3. 通过useEffect处理动作

React引入的最好用,最贴心的一个hook是useEffect。它可以处理与prop或state更改相关的动作。可就算它很好用,人们也不该到处滥用它。

想象一下有一个组件,其获取一个项目列表并将其渲染给dom。另外,如果请求成功,我们将调用“onSuccess”函数,该函数作为一个prop传递给这个组件。

这很危险❌

function DataList({ onSuccess }) {
  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) {
      onSuccess();
    }
  }, [loading, error, data, onSuccess]);
  return 
Data: {data}
; }

问题⚡

一共有两个useEffect hooks,第一个在初始渲染时处理api调用,第二个会调用onSuccess函数,假设当状态没有加载、没有错误但有数据时调用肯定成功。这很有道理是吧?

对第一个调用来说这肯定是正确的,并且可能永远不会失败。但你也失去了动作和需要调用的函数之间的直接联系。同样也没有100%的保证可以说这种情况仅在fetch动作成功后才会发生,而这正是我们开发人员不想看到的。

解决方案✅

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

function DataList({ onSuccess }) {
  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);
        onSuccess();
      })
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [onSuccess]);
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  return 
{data}
; }

现在一目了然了,在api调用成功的情况下才调用onSuccess。

4. 单一责任组件

组合组件可能不是什么轻松的事情。什么时候将一个组件拆分为几个较小的组件?如何构造组件树?使用基于组件的框架时,每天都会遇到这些问题。设计组件时常见的一个错误是将两个用例合并到一个组件中。以一个header为例,其在移动设备上显示一个汉堡按钮,或在桌面屏幕上显示标签。(这里的条件通过神奇的isMobile函数处理,这里就不深入讲解了。)

这很危险❌

function Header(props) {
  return (
    
); } function HeaderInner({ menuItems }) { return isMobile() ? : ; }

问题⚡

使用这种方法时,HeaderInner组件试图同时兼顾两件事情,而我们都知道一心最好不要二用。而且,这种组件很难在其他地方测试或重用。

解决方案✅

将条件提高一级,这样就能更容易看清组件的本来用途,搞明白它们只应该负责一个任务,不管是Header、Tab或BurgerButton也好,总之不要一心多用。

function Header(props) {
  return (
    
{isMobile() ? : }
); }

5. 单一责任的useEffects

还记得以前,我们只能用componentWillReceiveProps或componentDidUpdate方法挂接到React组件的渲染过程吗?那是一段黑暗的回忆,也让我们意识到了useEffect hook的美妙之处,尤其是你可以随意使用这些hooks。

但是有时因为粗心而让“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 (
    
); }

问题⚡

这里有两个用例,即“数据获取”和“显示面包屑”。两者都通过useEffect hook更新。当fetchData和updateBreadcrumbs函数或location更改时,都会运行这个useEffect hook。现在的主要问题是,当位置更改时,我们还调用了fetchData函数。这可能是我们没有想到的副作用。

解决方案✅

把效果拆分开来,确保它们只用于一种效果,意外的副作用也就消失了。

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 (
    
); }

额外的收获是,这些用例现在也在组件内按顺序排好了。

小结

在React中编写组件时有很多陷阱。我们不可能百分百地了解整个机制并避开所有小错,就算是大错误也可能逃不开。但是在学习框架或编程语言时犯错误也是很重要的,可能没有人会100%摆脱这些错误。

我认为与他人分享你的经验是很有意义的,这样别人就可以避开这些坑了。

如果你有任何疑问,请写信给我([email protected]),我很想听听你的意见。

原文链接:

https://www.lorenzweiss.de/common_mistakes_react_hooks/